diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a9dc1af8..4b5e05db 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -2,10 +2,7 @@ plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
-
id("com.google.dagger.hilt.android")
- kotlin("kapt")
-
id("kotlin-parcelize")
}
@@ -18,7 +15,7 @@ android {
minSdk = 26
targetSdk = 34
versionCode = 1
- versionName = "0.8.0"
+ versionName = "0.9.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -62,35 +59,35 @@ android {
}
dependencies {
- val composeVersion = "1.5.7"
// Default
implementation("androidx.core:core-ktx:1.12.0")
- implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
+ // Compose BOM libraries
implementation(platform("androidx.compose:compose-bom:2024.01.00"))
- implementation("androidx.compose.ui:ui:$composeVersion")
- implementation("androidx.compose.ui:ui-graphics:$composeVersion")
- implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion")
- implementation("androidx.compose.ui:ui-android:1.6.0")
- implementation("androidx.compose.material3:material3:1.1.2")
- implementation("androidx.compose.material:material:1.6.0")
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-graphics")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.ui:ui-android")
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.material3:material3-window-size-class")
+ implementation("androidx.compose.material:material")
// Unit tests
testImplementation("androidx.test:core:latest.release")
testImplementation("junit:junit:latest.release")
testImplementation("androidx.arch.core:core-testing:latest.release")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:latest.release")
- testImplementation("com.google.truth:truth:1.2.0")
+ testImplementation("com.google.truth:truth:1.4.2")
testImplementation("androidx.test.ext:truth:1.5.0")
testImplementation("com.squareup.okhttp3:mockwebserver:latest.release")
testImplementation("io.mockk:mockk:latest.release")
- debugImplementation("androidx.compose.ui:ui-test-manifest:1.5.4")
+ debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.3")
// Instrumentation tests
androidTestImplementation("com.google.dagger:hilt-android-testing:latest.release")
- kaptAndroidTest("com.google.dagger:hilt-android-compiler:latest.release")
androidTestImplementation("junit:junit:latest.release")
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:latest.release")
androidTestImplementation("androidx.arch.core:core-testing:latest.release")
@@ -102,17 +99,16 @@ dependencies {
androidTestImplementation("androidx.test:runner:latest.release")
// All dependencies
- implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
- implementation("androidx.compose.material:material-icons-extended:1.5.4")
- implementation("androidx.activity:activity-compose:1.8.2")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
+ implementation("androidx.compose.material:material-icons-extended")
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
// Dagger - Hilt
- implementation("com.google.dagger:hilt-android:2.50")
- kapt("com.google.dagger:hilt-android-compiler:2.50")
+ implementation("com.google.dagger:hilt-android:2.51")
+ ksp("com.google.dagger:hilt-android-compiler:2.51")
implementation("com.google.dagger:hilt-compiler:latest.release")
- kapt("androidx.hilt:hilt-compiler:latest.release")
- implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
+ ksp("androidx.hilt:hilt-compiler:latest.release")
+ implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
// Room
implementation("androidx.room:room-runtime:2.6.1")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b438d2f9..7d1dab8b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,6 +7,21 @@
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/app_icon-playstore.png b/app/src/main/app_icon-playstore.png
new file mode 100644
index 00000000..62d5d399
Binary files /dev/null and b/app/src/main/app_icon-playstore.png differ
diff --git a/app/src/main/app_icon_monochrome-playstore.png b/app/src/main/app_icon_monochrome-playstore.png
new file mode 100644
index 00000000..26220dc8
Binary files /dev/null and b/app/src/main/app_icon_monochrome-playstore.png differ
diff --git a/app/src/main/java/ua/acclorite/book_story/MainActivity.kt b/app/src/main/java/ua/acclorite/book_story/Activity.kt
similarity index 58%
rename from app/src/main/java/ua/acclorite/book_story/MainActivity.kt
rename to app/src/main/java/ua/acclorite/book_story/Activity.kt
index e33f3564..b8ccfa15 100644
--- a/app/src/main/java/ua/acclorite/book_story/MainActivity.kt
+++ b/app/src/main/java/ua/acclorite/book_story/Activity.kt
@@ -1,67 +1,104 @@
package ua.acclorite.book_story
+import android.annotation.SuppressLint
import android.content.res.Configuration
+import android.database.CursorWindow
import android.os.Bundle
+import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
-import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.slideInHorizontally
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import dagger.hilt.android.AndroidEntryPoint
import ua.acclorite.book_story.domain.model.Book
-import ua.acclorite.book_story.presentation.NavigationHost
-import ua.acclorite.book_story.presentation.Screen
-import ua.acclorite.book_story.presentation.components.BottomNavigationBar
+import ua.acclorite.book_story.presentation.components.bottom_navigation_bar.BottomNavigationBar
+import ua.acclorite.book_story.presentation.components.custom_navigation_rail.CustomNavigationRail
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel
+import ua.acclorite.book_story.presentation.data.NavigationHost
+import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreen
import ua.acclorite.book_story.presentation.screens.browse.BrowseScreen
+import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.HistoryScreen
+import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.LibraryScreen
+import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.screens.reader.ReaderScreen
import ua.acclorite.book_story.presentation.screens.settings.SettingsScreen
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettings
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings
+import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettings
import ua.acclorite.book_story.ui.BooksHistoryResurrectionTheme
+import ua.acclorite.book_story.ui.DarkTheme
+import ua.acclorite.book_story.ui.Theme
import ua.acclorite.book_story.ui.Transitions
import ua.acclorite.book_story.ui.isDark
+import java.lang.reflect.Field
+
+@SuppressLint("DiscouragedPrivateApi")
+@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@AndroidEntryPoint
-class MainActivity : AppCompatActivity() {
+class Activity : ComponentActivity() {
private val mainViewModel: MainViewModel by viewModels()
+ private val libraryViewModel: LibraryViewModel by viewModels()
+ private val historyViewModel: HistoryViewModel by viewModels()
+ private val browseViewModel: BrowseViewModel by viewModels()
+
override fun onCreate(savedInstanceState: Bundle?) {
-
super.onCreate(savedInstanceState)
- mainViewModel.init(this)
+ // New Cursor size for Room
+ try {
+ val field: Field = CursorWindow::class.java.getDeclaredField("sCursorWindowSize")
+ field.isAccessible = true
+ field.set(null, 100 * 1024 * 1024)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ // Initializing all variables
+ mainViewModel.init(
+ activity = this,
+ libraryViewModel,
+ )
+
+ // Splash screen
installSplashScreen().apply {
setKeepOnScreenCondition {
!mainViewModel.isReady.value
}
}
+ // Edge to edge
enableEdgeToEdge()
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
+ val windowClass = calculateWindowSizeClass(activity = this)
val updating = mainViewModel.updating.collectAsState().value
- val theme = mainViewModel.theme.collectAsState().value!!
- val darkTheme = mainViewModel.darkTheme.collectAsState().value!!
+
+ val theme = mainViewModel.theme.collectAsState().value ?: Theme.BLUE
+ val darkTheme =
+ mainViewModel.darkTheme.collectAsState().value ?: DarkTheme.FOLLOW_SYSTEM
+ val tabletUI = windowClass.widthSizeClass != WindowWidthSizeClass.Compact
BooksHistoryResurrectionTheme(
theme = theme,
@@ -70,28 +107,36 @@ class MainActivity : AppCompatActivity() {
if (!updating) {
NavigationHost(startScreen = Screen.LIBRARY) {
val currentScreen by this.getCurrentScreen().collectAsState()
+
AnimatedVisibility(
visible = currentScreen == Screen.LIBRARY ||
currentScreen == Screen.HISTORY ||
currentScreen == Screen.BROWSE,
- enter = fadeIn(tween(200)) +
- slideInHorizontally(tween(200)) { it / 15 },
+ enter = Transitions.BackSlidingTransitionIn,
exit = Transitions.SlidingTransitionOut
) {
Scaffold(
bottomBar = {
- BottomNavigationBar(navigator = this@NavigationHost)
+ if (!tabletUI) {
+ BottomNavigationBar(navigator = this@NavigationHost)
+ }
},
containerColor = MaterialTheme.colorScheme.surface
) {
Box(
modifier = Modifier
.fillMaxSize()
- .padding(bottom = it.calculateBottomPadding())
+ .padding(
+ start = if (tabletUI) 80.dp else 0.dp,
+ bottom = it.calculateBottomPadding()
+ )
) {
composable(screen = Screen.LIBRARY) {
@Suppress("UNCHECKED_CAST")
LibraryScreen(
+ viewModel = libraryViewModel,
+ historyViewModel = historyViewModel,
+ browseViewModel = browseViewModel,
navigator = this@NavigationHost,
addedBooks = retrieveArgument("added_books") as? List
?: emptyList()
@@ -99,13 +144,25 @@ class MainActivity : AppCompatActivity() {
}
composable(screen = Screen.HISTORY) {
- HistoryScreen(navigator = this@NavigationHost)
+ HistoryScreen(
+ viewModel = historyViewModel,
+ libraryViewModel = libraryViewModel,
+ navigator = this@NavigationHost
+ )
}
composable(screen = Screen.BROWSE) {
- BrowseScreen(navigator = this@NavigationHost)
+ BrowseScreen(
+ viewModel = browseViewModel,
+ libraryViewModel = libraryViewModel,
+ navigator = this@NavigationHost
+ )
}
}
+
+ if (tabletUI) {
+ CustomNavigationRail(navigator = this@NavigationHost)
+ }
}
}
@@ -116,6 +173,9 @@ class MainActivity : AppCompatActivity() {
exitAnim = Transitions.SlidingTransitionOut
) {
BookInfoScreen(
+ libraryViewModel = libraryViewModel,
+ browseViewModel = browseViewModel,
+ historyViewModel = historyViewModel,
navigator = this@NavigationHost
)
}
@@ -125,6 +185,9 @@ class MainActivity : AppCompatActivity() {
exitAnim = Transitions.SlidingTransitionOut
) {
ReaderScreen(
+ mainViewModel = mainViewModel,
+ libraryViewModel = libraryViewModel,
+ historyViewModel = historyViewModel,
navigator = this@NavigationHost
)
}
@@ -147,6 +210,7 @@ class MainActivity : AppCompatActivity() {
exitAnim = Transitions.SlidingTransitionOut
) {
GeneralSettings(
+ mainViewModel = mainViewModel,
navigator = this@NavigationHost
)
}
@@ -156,9 +220,25 @@ class MainActivity : AppCompatActivity() {
exitAnim = Transitions.SlidingTransitionOut
) {
AppearanceSettings(
+ mainViewModel = mainViewModel,
navigator = this@NavigationHost
)
}
+ composable(
+ screen = Screen.READER_SETTINGS,
+ enterAnim = Transitions.SlidingTransitionIn,
+ exitAnim = Transitions.SlidingTransitionOut
+ ) {
+ ReaderSettings(
+ mainViewModel = mainViewModel,
+ navigator = this@NavigationHost
+ )
+ }
+
+// Start screen (later)
+// composable(screen = Screen.START) {
+// StartScreen()
+// }
}
} else {
Box(
diff --git a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt
index a129eccf..bf178666 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt
@@ -6,10 +6,10 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ua.acclorite.book_story.data.local.data_store.DataStore
import ua.acclorite.book_story.data.local.data_store.DataStoreImpl
-import ua.acclorite.book_story.data.mapper.BookMapper
-import ua.acclorite.book_story.data.mapper.BookMapperImpl
-import ua.acclorite.book_story.data.mapper.HistoryMapper
-import ua.acclorite.book_story.data.mapper.HistoryMapperImpl
+import ua.acclorite.book_story.data.mapper.book.BookMapper
+import ua.acclorite.book_story.data.mapper.book.BookMapperImpl
+import ua.acclorite.book_story.data.mapper.history.HistoryMapper
+import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl
import ua.acclorite.book_story.data.repository.BookRepositoryImpl
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Singleton
diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt b/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt
index 08addf36..719c5dc2 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt
@@ -8,9 +8,11 @@ import ua.acclorite.book_story.domain.model.Category
data class BookEntity(
@PrimaryKey(true) val id: Int? = null,
val title: String,
+ val author: String?,
+ val description: String?,
+ val text: String,
val filePath: String,
val progress: Float,
- var lastOpened: Long? = null,
val image: ByteArray = byteArrayOf(),
val category: Category
) {
diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt
index eb3f9fec..e110fb27 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt
@@ -19,8 +19,7 @@ interface BookDao {
@Query(
"""
- SELECT *
- FROM bookentity
+ SELECT * FROM bookentity
WHERE LOWER(title) LIKE '%' || LOWER(:query) || '%'
"""
)
@@ -31,6 +30,9 @@ interface BookDao {
)
suspend fun findBookById(id: Int): BookEntity
+ @Query("SELECT * FROM bookentity WHERE id IN (:ids)")
+ suspend fun findBooksById(ids: List): List
+
@Delete
suspend fun deleteBooks(books: List)
diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/BookMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/BookMapperImpl.kt
deleted file mode 100644
index f72eb061..00000000
--- a/app/src/main/java/ua/acclorite/book_story/data/mapper/BookMapperImpl.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package ua.acclorite.book_story.data.mapper
-
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import ua.acclorite.book_story.R
-import ua.acclorite.book_story.data.local.dto.BookEntity
-import ua.acclorite.book_story.data.parser.EpubFileParser
-import ua.acclorite.book_story.data.parser.PdfFileParser
-import ua.acclorite.book_story.data.parser.TxtFileParser
-import ua.acclorite.book_story.domain.model.Book
-import ua.acclorite.book_story.util.UIText
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
-import java.io.File
-import javax.inject.Inject
-
-class BookMapperImpl @Inject constructor(
- private val txtFileParser: TxtFileParser,
- private val pdfFileParser: PdfFileParser,
- private val epubFileParser: EpubFileParser
-) : BookMapper {
- override suspend fun toBookEntity(book: Book): BookEntity {
- val stream = ByteArrayOutputStream()
- book.coverImage?.compress(Bitmap.CompressFormat.JPEG, 30, stream)
-
- return BookEntity(
- id = book.id,
- title = book.title,
- filePath = book.filePath,
- progress = book.progress,
- lastOpened = book.lastOpened,
- image = stream.toByteArray(),
- category = book.category
- )
- }
-
- override suspend fun toBook(bookEntity: BookEntity): Book {
- val file = File(bookEntity.filePath)
- val inputStream = ByteArrayInputStream(bookEntity.image)
- val image = BitmapFactory.decodeStream(inputStream)
-
- val parsedBook = if (file.name.endsWith(".txt")) {
- txtFileParser.parse(file)
- } else if (file.name.endsWith(".pdf")) {
- pdfFileParser.parse(file)
- } else if (file.name.endsWith(".epub")) {
- epubFileParser.parse(file)
- } else {
- null
- }
-
- return if (parsedBook != null)
- Book(
- id = bookEntity.id,
- title = bookEntity.title,
- author = parsedBook.author,
- description = parsedBook.description,
- text = emptyList(),
- progress = bookEntity.progress,
- file = file,
- filePath = bookEntity.filePath,
- lastOpened = bookEntity.lastOpened,
- category = bookEntity.category,
- coverImage = if (bookEntity.image.isNotEmpty()) image else null
- )
- else
- Book(
- id = bookEntity.id,
- title = bookEntity.title,
- author = UIText.StringResource(R.string.unknown_author),
- text = emptyList(),
- description = null,
- progress = bookEntity.progress,
- file = null,
- filePath = bookEntity.filePath,
- lastOpened = bookEntity.lastOpened,
- category = bookEntity.category,
- coverImage = if (bookEntity.image.isNotEmpty()) image else null
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/BookMapper.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapper.kt
similarity index 82%
rename from app/src/main/java/ua/acclorite/book_story/data/mapper/BookMapper.kt
rename to app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapper.kt
index e3fe7bce..358807a3 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/mapper/BookMapper.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapper.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.data.mapper
+package ua.acclorite.book_story.data.mapper.book
import ua.acclorite.book_story.data.local.dto.BookEntity
import ua.acclorite.book_story.domain.model.Book
diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt
new file mode 100644
index 00000000..f314e27d
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt
@@ -0,0 +1,74 @@
+package ua.acclorite.book_story.data.mapper.book
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.os.Build
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.data.local.dto.BookEntity
+import ua.acclorite.book_story.domain.model.Book
+import ua.acclorite.book_story.domain.model.StringWithId
+import ua.acclorite.book_story.util.UIText
+import java.io.ByteArrayOutputStream
+import java.io.File
+import javax.inject.Inject
+
+class BookMapperImpl @Inject constructor() : BookMapper {
+ override suspend fun toBookEntity(book: Book): BookEntity {
+ val legacyAPI = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
+
+ val stream = ByteArrayOutputStream()
+ book.coverImage?.asAndroidBitmap()?.compress(
+ if (legacyAPI) Bitmap.CompressFormat.WEBP
+ else Bitmap.CompressFormat.WEBP_LOSSLESS,
+ if (legacyAPI) 30 else 100,
+ stream
+ )
+
+ val text = book.text.joinToString("\n") {
+ it.line.trim()
+ }
+
+ return BookEntity(
+ id = book.id,
+ title = book.title,
+ filePath = book.filePath,
+ progress = book.progress,
+ author = book.author.string,
+ text = text,
+ description = book.description,
+ image = stream.toByteArray(),
+ category = book.category
+ )
+ }
+
+ override suspend fun toBook(bookEntity: BookEntity): Book {
+ val file = File(bookEntity.filePath)
+ var image: ImageBitmap? = null
+
+ if (bookEntity.image.isNotEmpty()) {
+ image = BitmapFactory
+ .decodeByteArray(bookEntity.image, 0, bookEntity.image.size)
+ .asImageBitmap()
+ image.prepareToDraw()
+ }
+
+ return Book(
+ id = bookEntity.id,
+ title = bookEntity.title,
+ author = bookEntity.author?.let { UIText.StringValue(it) } ?: UIText.StringResource(
+ R.string.unknown_author
+ ),
+ description = bookEntity.description,
+ progress = bookEntity.progress,
+ file = if (file.exists()) file else null,
+ text = bookEntity.text.split("\n").map { StringWithId(it) },
+ filePath = bookEntity.filePath,
+ lastOpened = null,
+ category = bookEntity.category,
+ coverImage = image
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/HistoryMapper.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt
similarity index 54%
rename from app/src/main/java/ua/acclorite/book_story/data/mapper/HistoryMapper.kt
rename to app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt
index 0648b0d0..2d607770 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/mapper/HistoryMapper.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt
@@ -1,11 +1,10 @@
-package ua.acclorite.book_story.data.mapper
+package ua.acclorite.book_story.data.mapper.history
import ua.acclorite.book_story.data.local.dto.HistoryEntity
-import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History
interface HistoryMapper {
suspend fun toHistoryEntity(history: History): HistoryEntity
- suspend fun toHistory(historyEntity: HistoryEntity, book: Book): History
+ suspend fun toHistory(historyEntity: HistoryEntity): History
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/HistoryMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt
similarity index 66%
rename from app/src/main/java/ua/acclorite/book_story/data/mapper/HistoryMapperImpl.kt
rename to app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt
index 81c8ef76..ff6292f8 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/mapper/HistoryMapperImpl.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt
@@ -1,7 +1,6 @@
-package ua.acclorite.book_story.data.mapper
+package ua.acclorite.book_story.data.mapper.history
import ua.acclorite.book_story.data.local.dto.HistoryEntity
-import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History
import javax.inject.Inject
@@ -9,15 +8,15 @@ class HistoryMapperImpl @Inject constructor() : HistoryMapper {
override suspend fun toHistoryEntity(history: History): HistoryEntity {
return HistoryEntity(
id = history.id,
- bookId = history.book.id ?: 0,
+ bookId = history.bookId,
time = history.time
)
}
- override suspend fun toHistory(historyEntity: HistoryEntity, book: Book): History {
+ override suspend fun toHistory(historyEntity: HistoryEntity): History {
return History(
historyEntity.id,
- book = book,
+ bookId = historyEntity.bookId,
time = historyEntity.time
)
}
diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt
index 50be3cec..fcee309e 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt
@@ -1,8 +1,9 @@
package ua.acclorite.book_story.data.parser
import ua.acclorite.book_story.domain.model.StringWithId
+import ua.acclorite.book_story.util.Resource
import java.io.File
interface TextParser {
- suspend fun parse(file: File): List
+ suspend fun parse(file: File): Resource>
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TxtTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TxtTextParser.kt
deleted file mode 100644
index 1964463a..00000000
--- a/app/src/main/java/ua/acclorite/book_story/data/parser/TxtTextParser.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package ua.acclorite.book_story.data.parser
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import ua.acclorite.book_story.domain.model.StringWithId
-import java.io.BufferedReader
-import java.io.File
-import java.io.FileReader
-import javax.inject.Inject
-
-class TxtTextParser @Inject constructor() : TextParser {
-
- override suspend fun parse(file: File): List {
- if (!file.name.endsWith(".txt")) {
- return emptyList()
- }
-
- return try {
- val formattedLines = mutableListOf()
-
- withContext(Dispatchers.IO) {
- BufferedReader(FileReader(file)).forEachLine { line ->
- formattedLines.add(
- StringWithId(line)
- )
- }
- }
-
- formattedLines
- } catch (e: Exception) {
- emptyList()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/EpubFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt
similarity index 89%
rename from app/src/main/java/ua/acclorite/book_story/data/parser/EpubFileParser.kt
rename to app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt
index 3547b05a..482745c7 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/parser/EpubFileParser.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt
@@ -1,9 +1,11 @@
-package ua.acclorite.book_story.data.parser
+package ua.acclorite.book_story.data.parser.epub
import android.graphics.BitmapFactory
+import androidx.compose.ui.graphics.asImageBitmap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import nl.siegmann.epublib.epub.EpubReader
+import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.util.UIText
@@ -51,7 +53,7 @@ class EpubFileParser @Inject constructor() : FileParser {
filePath = file.path,
lastOpened = null,
category = Category.entries[0],
- coverImage = coverImage
+ coverImage = coverImage?.asImageBitmap()
)
} catch (e: Exception) {
return null
diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/EpubTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt
similarity index 66%
rename from app/src/main/java/ua/acclorite/book_story/data/parser/EpubTextParser.kt
rename to app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt
index c8ee1158..35688701 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/parser/EpubTextParser.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt
@@ -1,10 +1,14 @@
-package ua.acclorite.book_story.data.parser
+package ua.acclorite.book_story.data.parser.epub
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import nl.siegmann.epublib.epub.EpubReader
import org.jsoup.Jsoup
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.model.StringWithId
+import ua.acclorite.book_story.util.Resource
+import ua.acclorite.book_story.util.UIText
import java.io.BufferedReader
import java.io.File
import java.io.FileInputStream
@@ -14,9 +18,9 @@ import javax.inject.Inject
class EpubTextParser @Inject constructor() : TextParser {
- override suspend fun parse(file: File): List {
+ override suspend fun parse(file: File): Resource> {
if (!file.name.endsWith(".epub")) {
- return emptyList()
+ return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
}
try {
@@ -47,9 +51,18 @@ class EpubTextParser @Inject constructor() : TextParser {
}
}
- return stringWithIds
+ if (stringWithIds.isEmpty()) {
+ return Resource.Error(UIText.StringResource(R.string.error_file_empty))
+ }
+
+ return Resource.Success(stringWithIds)
} catch (e: Exception) {
- return emptyList()
+ return Resource.Error(
+ UIText.StringResource(
+ R.string.error_query,
+ e.message?.take(40)?.trim() ?: ""
+ )
+ )
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/PdfFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt
similarity index 94%
rename from app/src/main/java/ua/acclorite/book_story/data/parser/PdfFileParser.kt
rename to app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt
index 36badf5c..5b894e1e 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/parser/PdfFileParser.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt
@@ -1,9 +1,10 @@
-package ua.acclorite.book_story.data.parser
+package ua.acclorite.book_story.data.parser.pdf
import android.app.Application
import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
import com.tom_roush.pdfbox.pdmodel.PDDocument
import ua.acclorite.book_story.R
+import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.StringWithId
diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/PdfTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt
similarity index 62%
rename from app/src/main/java/ua/acclorite/book_story/data/parser/PdfTextParser.kt
rename to app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt
index 42391f12..19759293 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/parser/PdfTextParser.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt
@@ -1,18 +1,22 @@
-package ua.acclorite.book_story.data.parser
+package ua.acclorite.book_story.data.parser.pdf
import android.app.Application
import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
import com.tom_roush.pdfbox.pdmodel.PDDocument
import com.tom_roush.pdfbox.text.PDFTextStripper
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.model.StringWithId
+import ua.acclorite.book_story.util.Resource
+import ua.acclorite.book_story.util.UIText
import java.io.File
import javax.inject.Inject
class PdfTextParser @Inject constructor(private val application: Application) : TextParser {
- override suspend fun parse(file: File): List {
+ override suspend fun parse(file: File): Resource> {
if (!file.name.endsWith(".pdf")) {
- return emptyList()
+ return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
}
try {
@@ -24,6 +28,11 @@ class PdfTextParser @Inject constructor(private val application: Application) :
val pdfStripper = PDFTextStripper()
pdfStripper.paragraphStart = "_new_line_"
+ if (document.isEncrypted) {
+ document.close()
+ return Resource.Error(UIText.StringResource(R.string.error_file_encrypted))
+ }
+
val oldText = pdfStripper.getText(document)
.replace("\r", "")
@@ -38,7 +47,7 @@ class PdfTextParser @Inject constructor(private val application: Application) :
}
val unformattedLines = text.split("${pdfStripper.paragraphStart}|\\n".toRegex())
- .filter { it.isNotEmpty() }
+ .filter { it.isNotBlank() }
val lines = mutableListOf()
unformattedLines.forEachIndexed { index, string ->
@@ -52,10 +61,18 @@ class PdfTextParser @Inject constructor(private val application: Application) :
stringWithIds.add(StringWithId(line.trim()))
}
- return stringWithIds
+ if (stringWithIds.isEmpty()) {
+ return Resource.Error(UIText.StringResource(R.string.error_file_empty))
+ }
+ return Resource.Success(stringWithIds)
} catch (e: Exception) {
- return emptyList()
+ return Resource.Error(
+ UIText.StringResource(
+ R.string.error_query,
+ e.message?.take(40)?.trim() ?: ""
+ )
+ )
}
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TxtFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt
similarity index 91%
rename from app/src/main/java/ua/acclorite/book_story/data/parser/TxtFileParser.kt
rename to app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt
index bb7f0fe1..f2b9485d 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/parser/TxtFileParser.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt
@@ -1,6 +1,7 @@
-package ua.acclorite.book_story.data.parser
+package ua.acclorite.book_story.data.parser.txt
import ua.acclorite.book_story.R
+import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.StringWithId
diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt
new file mode 100644
index 00000000..d1756dda
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt
@@ -0,0 +1,49 @@
+package ua.acclorite.book_story.data.parser.txt
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.data.parser.TextParser
+import ua.acclorite.book_story.domain.model.StringWithId
+import ua.acclorite.book_story.util.Resource
+import ua.acclorite.book_story.util.UIText
+import java.io.BufferedReader
+import java.io.File
+import java.io.FileReader
+import javax.inject.Inject
+
+class TxtTextParser @Inject constructor() : TextParser {
+
+ override suspend fun parse(file: File): Resource> {
+ if (!file.name.endsWith(".txt")) {
+ return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
+ }
+
+ return try {
+ val formattedLines = mutableListOf()
+
+ withContext(Dispatchers.IO) {
+ BufferedReader(FileReader(file)).forEachLine { line ->
+ if (line.isNotBlank()) {
+ formattedLines.add(
+ StringWithId(line)
+ )
+ }
+ }
+ }
+
+ if (formattedLines.isEmpty()) {
+ return Resource.Error(UIText.StringResource(R.string.error_file_empty))
+ }
+
+ Resource.Success(formattedLines)
+ } catch (e: Exception) {
+ Resource.Error(
+ UIText.StringResource(
+ R.string.error_query,
+ e.message?.take(40)?.trim() ?: ""
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt
index 30a15dd3..915605c7 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt
@@ -4,24 +4,27 @@ import android.os.Environment
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
+import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.local.data_store.DataStore
import ua.acclorite.book_story.data.local.dto.BookEntity
import ua.acclorite.book_story.data.local.room.BookDao
-import ua.acclorite.book_story.data.mapper.BookMapper
-import ua.acclorite.book_story.data.mapper.HistoryMapper
-import ua.acclorite.book_story.data.parser.EpubFileParser
-import ua.acclorite.book_story.data.parser.EpubTextParser
-import ua.acclorite.book_story.data.parser.PdfFileParser
-import ua.acclorite.book_story.data.parser.PdfTextParser
-import ua.acclorite.book_story.data.parser.TxtFileParser
-import ua.acclorite.book_story.data.parser.TxtTextParser
+import ua.acclorite.book_story.data.mapper.book.BookMapper
+import ua.acclorite.book_story.data.mapper.history.HistoryMapper
+import ua.acclorite.book_story.data.parser.epub.EpubFileParser
+import ua.acclorite.book_story.data.parser.epub.EpubTextParser
+import ua.acclorite.book_story.data.parser.pdf.PdfFileParser
+import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
+import ua.acclorite.book_story.data.parser.txt.TxtFileParser
+import ua.acclorite.book_story.data.parser.txt.TxtTextParser
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.repository.BookRepository
+import ua.acclorite.book_story.presentation.data.calculateFamiliarity
import ua.acclorite.book_story.util.Constants
import ua.acclorite.book_story.util.Resource
+import ua.acclorite.book_story.util.UIText
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
@@ -49,9 +52,20 @@ class BookRepositoryImpl @Inject constructor(
emit(Resource.Loading(true))
val books = database.searchBooks(query)
+ val history = database.getHistory()
+
emit(
Resource.Success(
- data = books.map { bookMapper.toBook(it) }
+ data = books.map { entity ->
+ val book = bookMapper.toBook(entity)
+ val lastHistory = history
+ .filter { it.bookId == book.id }
+ .maxByOrNull { it.time }
+
+ book.copy(
+ lastOpened = lastHistory?.time
+ )
+ }
)
)
}
@@ -59,7 +73,21 @@ class BookRepositoryImpl @Inject constructor(
override suspend fun fastGetBooks(query: String): List {
val books = database.searchBooks(query)
- return books.map { bookMapper.toBook(it) }
+ val history = database.getHistory()
+
+ return books.map { entity ->
+ val book = bookMapper.toBook(entity)
+ val lastHistory =
+ history.filter { it.bookId == book.id }.maxByOrNull { it.time }
+
+ book.copy(
+ lastOpened = lastHistory?.time
+ )
+ }
+ }
+
+ override suspend fun getBooksById(ids: List): List {
+ return database.findBooksById(ids).map { bookMapper.toBook(it) }
}
override suspend fun findBook(
@@ -154,7 +182,10 @@ class BookRepositoryImpl @Inject constructor(
filteredFiles = if (query.trim().isNotEmpty()) {
filteredFiles.sortedByDescending {
- it.name.lowercase().startsWith(query.trim().lowercase())
+ calculateFamiliarity(
+ query,
+ it.name.lowercase().trim()
+ )
}.toMutableList()
} else {
filteredFiles.sortedByDescending {
@@ -182,61 +213,113 @@ class BookRepositoryImpl @Inject constructor(
} else if (file.name.endsWith(".epub")) {
epubTextParser.parse(file)
} else {
+ emit(Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)))
return@flow
}
- emit(Resource.Success(text))
- }
- }
-
- override suspend fun getBooksFromFiles(files: List): Flow>> {
- return flow {
- emit(Resource.Loading(true))
-
- val books = mutableListOf()
- for (file in files) {
-
- val parsedBook = if (file.name.endsWith(".txt")) {
- txtFileParser.parse(file)
- } else if (file.name.endsWith(".pdf")) {
- pdfFileParser.parse(file)
- } else if (file.name.endsWith(".epub")) {
- epubFileParser.parse(file)
- } else {
- books.add(NullableBook.Null(file.name))
- continue
- }
-
- val parsedText = if (file.name.endsWith(".txt")) {
- txtTextParser.parse(file)
- } else if (file.name.endsWith(".pdf")) {
- pdfTextParser.parse(file)
- } else if (file.name.endsWith(".epub")) {
- epubTextParser.parse(file)
- } else {
- books.add(NullableBook.Null(file.name))
- continue
- }
-
- if (parsedBook == null || parsedText.isEmpty()) {
- books.add(NullableBook.Null(file.name))
- continue
- }
-
- books.add(
- NullableBook.NotNull(
- Pair(
- parsedBook.copy(text = parsedText),
- true
- )
+ if (text is Resource.Success && text.data == null) {
+ emit(
+ Resource.Error(
+ text.message
+ ?: UIText.StringResource(R.string.error_file_empty)
)
)
}
- emit(Resource.Success(books))
+ when (text) {
+ is Resource.Success -> emit(Resource.Success(text.data))
+ is Resource.Error -> emit(
+ Resource.Error(
+ text.message
+ ?: UIText.StringResource(R.string.error_something_went_wrong)
+ )
+ )
+
+ is Resource.Loading -> Unit
+ }
+
}
}
+ override suspend fun getBooksFromFiles(files: List): List {
+ val books = mutableListOf()
+
+ for (file in files) {
+ val parsedBook = if (file.name.endsWith(".txt")) {
+ txtFileParser.parse(file)
+ } else if (file.name.endsWith(".pdf")) {
+ pdfFileParser.parse(file)
+ } else if (file.name.endsWith(".epub")) {
+ epubFileParser.parse(file)
+ } else {
+ books.add(
+ NullableBook.Null(
+ file.name,
+ UIText.StringResource(R.string.error_wrong_file_format)
+ )
+ )
+ continue
+ }
+
+ val parsedText = if (file.name.endsWith(".txt")) {
+ txtTextParser.parse(file)
+ } else if (file.name.endsWith(".pdf")) {
+ pdfTextParser.parse(file)
+ } else if (file.name.endsWith(".epub")) {
+ epubTextParser.parse(file)
+ } else {
+ books.add(
+ NullableBook.Null(
+ file.name,
+ UIText.StringResource(R.string.error_wrong_file_format)
+ )
+ )
+ continue
+ }
+
+ if (parsedBook == null) {
+ books.add(
+ NullableBook.Null(
+ file.name,
+ UIText.StringResource(R.string.error_something_went_wrong)
+ )
+ )
+ continue
+ }
+
+ if (parsedText is Resource.Error) {
+ books.add(
+ NullableBook.Null(
+ file.name,
+ parsedText.message
+ )
+ )
+ continue
+ }
+
+ if (parsedText.data == null) {
+ books.add(
+ NullableBook.Null(
+ file.name,
+ UIText.StringResource(R.string.error_file_empty)
+ )
+ )
+ continue
+ }
+
+ books.add(
+ NullableBook.NotNull(
+ Pair(
+ parsedBook.copy(text = parsedText.data),
+ true
+ )
+ )
+ )
+ }
+
+ return books
+ }
+
override suspend fun insertHistory(history: List) {
database.insertHistory(history.map { historyMapper.toHistoryEntity(it) })
}
@@ -246,11 +329,13 @@ class BookRepositoryImpl @Inject constructor(
emit(Resource.Loading(true))
val history = database.getHistory()
+
emit(
Resource.Success(
data = history.map {
- val book = database.findBookById(it.bookId)
- historyMapper.toHistory(it, bookMapper.toBook(book))
+ historyMapper.toHistory(
+ it
+ )
}
)
)
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt
index 985463a3..0d72a8b0 100644
--- a/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt
+++ b/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt
@@ -1,7 +1,7 @@
package ua.acclorite.book_story.domain.model
-import android.graphics.Bitmap
import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.ImageBitmap
import ua.acclorite.book_story.util.UIText
import java.io.File
@@ -17,5 +17,5 @@ data class Book(
val filePath: String,
val lastOpened: Long?,
val category: Category,
- val coverImage: Bitmap?
+ val coverImage: ImageBitmap?
)
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/Category.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/Category.kt
index dfbb5321..acab1534 100644
--- a/app/src/main/java/ua/acclorite/book_story/domain/model/Category.kt
+++ b/app/src/main/java/ua/acclorite/book_story/domain/model/Category.kt
@@ -1,5 +1,8 @@
package ua.acclorite.book_story.domain.model
+import androidx.compose.runtime.Immutable
+
+@Immutable
enum class Category {
READING, ALREADY_READ, PLANNING, DROPPED
}
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt
index 1e9d8ada..4bf92477 100644
--- a/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt
+++ b/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt
@@ -5,6 +5,6 @@ import androidx.compose.runtime.Immutable
@Immutable
data class History(
val id: Int?,
- val book: Book,
+ val bookId: Int,
val time: Long
)
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/NavigationBarItem.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/NavigationItem.kt
similarity index 88%
rename from app/src/main/java/ua/acclorite/book_story/domain/model/NavigationBarItem.kt
rename to app/src/main/java/ua/acclorite/book_story/domain/model/NavigationItem.kt
index ff897133..3e62e80c 100644
--- a/app/src/main/java/ua/acclorite/book_story/domain/model/NavigationBarItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/domain/model/NavigationItem.kt
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.painter.Painter
@Immutable
-data class NavigationBarItem(
+data class NavigationItem(
val title: String,
val selectedIcon: Painter,
val unselectedIcon: Painter
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt
index 014b0940..7af9a63c 100644
--- a/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt
+++ b/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt
@@ -1,6 +1,17 @@
package ua.acclorite.book_story.domain.model
-sealed class NullableBook(val book: Pair?, val fileName: String?) {
- class NotNull(book: Pair) : NullableBook(book, null)
- class Null(fileName: String) : NullableBook(null, fileName)
-}
\ No newline at end of file
+import androidx.compose.runtime.Immutable
+import ua.acclorite.book_story.util.UIText
+
+@Immutable
+sealed class NullableBook(
+ val book: Pair?,
+ val fileName: String?,
+ val message: UIText?
+) {
+ class NotNull(book: Pair) : NullableBook(book, null, null)
+ class Null(
+ fileName: String,
+ message: UIText?
+ ) : NullableBook(null, fileName, message)
+}
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt
index b2bf1844..edf27f7d 100644
--- a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt
+++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt
@@ -20,6 +20,10 @@ interface BookRepository {
query: String
): List
+ suspend fun getBooksById(
+ ids: List
+ ): List
+
suspend fun findBook(
id: Int
): BookEntity
@@ -50,7 +54,7 @@ interface BookRepository {
suspend fun getBookTextFromFile(file: File): Flow>>
- suspend fun getBooksFromFiles(files: List): Flow>>
+ suspend fun getBooksFromFiles(files: List): List
suspend fun insertHistory(history: List)
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBookFromFile.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBookFromFile.kt
new file mode 100644
index 00000000..b98c325e
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBookFromFile.kt
@@ -0,0 +1,15 @@
+package ua.acclorite.book_story.domain.use_case
+
+import ua.acclorite.book_story.domain.model.NullableBook
+import ua.acclorite.book_story.domain.repository.BookRepository
+import java.io.File
+import javax.inject.Inject
+
+class GetBookFromFile @Inject constructor(
+ private val repository: BookRepository
+) {
+
+ suspend fun execute(file: File): NullableBook {
+ return repository.getBooksFromFiles(listOf(file)).first()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksById.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksById.kt
new file mode 100644
index 00000000..f854cf2d
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksById.kt
@@ -0,0 +1,12 @@
+package ua.acclorite.book_story.domain.use_case
+
+import ua.acclorite.book_story.domain.model.Book
+import ua.acclorite.book_story.domain.repository.BookRepository
+import javax.inject.Inject
+
+class GetBooksById @Inject constructor(private val repository: BookRepository) {
+
+ suspend fun execute(ids: List): List {
+ return repository.getBooksById(ids)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksFromFiles.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksFromFiles.kt
index b8d0374d..48bf6413 100644
--- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksFromFiles.kt
+++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooksFromFiles.kt
@@ -1,9 +1,7 @@
package ua.acclorite.book_story.domain.use_case
-import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.repository.BookRepository
-import ua.acclorite.book_story.util.Resource
import java.io.File
import javax.inject.Inject
@@ -11,7 +9,7 @@ class GetBooksFromFiles @Inject constructor(
private val repository: BookRepository
) {
- suspend fun execute(files: List): Flow>> {
+ suspend fun execute(files: List): List {
return repository.getBooksFromFiles(files)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt
index 275ee0a6..f68d490c 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt
@@ -26,6 +26,14 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.ui.DefaultTransition
import ua.acclorite.book_story.ui.elevation
+/**
+ * Animated top app bar. Has up to 3 different top bars inside it. Follow [TopAppBar] for more info.
+ *
+ * @param isTopBarScrolled Whether isScrolled state should be forced or not.
+ * @param content1Visibility Whether first top app bar should be shown.
+ * @param content2Visibility Whether second top app bar should be shown.
+ * @param content3Visibility Whether third top app bar should be shown.
+ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AnimatedTopAppBar(
@@ -50,6 +58,7 @@ fun AnimatedTopAppBar(
content3Title: @Composable () -> Unit = {},
content3Actions: @Composable RowScope.() -> Unit = {}
) {
+ //todo fix lags.
Box(modifier = Modifier.fillMaxWidth()) {
if ((scrollBehavior != null || isTopBarScrolled != null) && scrolledContainerColor != null) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CategoryTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CategoryTitle.kt
index c553bf4c..be7079a7 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CategoryTitle.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CategoryTitle.kt
@@ -14,6 +14,9 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+/**
+ * Category Title. Used to name a category.
+ */
@Composable
fun CategoryTitle(
modifier: Modifier = Modifier,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCheckbox.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCheckbox.kt
index addfddca..b63df96b 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCheckbox.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCheckbox.kt
@@ -16,6 +16,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.ui.elevation
+/**
+ * Custom Checkbox. Has a Circle shape.
+ */
@Composable
fun CustomCheckbox(selected: Boolean) {
Icon(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDropDownMenuItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDropDownMenuItem.kt
index f4353a5b..b6dc35b1 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDropDownMenuItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDropDownMenuItem.kt
@@ -11,6 +11,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
+/**
+ * Custom Dropdown Menu Item. Used in Dropdown.
+ */
@Composable
fun CustomDropDownMenuItem(
leadingIcon: ImageVector,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt
new file mode 100644
index 00000000..e67b4435
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt
@@ -0,0 +1,236 @@
+package ua.acclorite.book_story.presentation.components
+
+import android.content.ClipboardManager
+import android.content.Context
+import android.view.ActionMode
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalTextToolbar
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.TextToolbarStatus
+import ua.acclorite.book_story.R
+
+
+private const val MENU_ITEM_COPY = 0
+private const val MENU_ITEM_TRANSLATE = 1
+private const val MENU_ITEM_DICTIONARY = 2
+
+/**
+ * Custom Text ActionMode callback. Used in pair with [CustomSelectionToolbar]. Follow [TextToolbar] for more info.
+ */
+private class CustomTextActionModeCallback(
+ private val context: Context,
+ var rect: Rect = Rect.Zero,
+ var onCopyRequested: (() -> Unit)? = null,
+ var onTranslateRequested: (() -> Unit)? = null,
+ var onDictionaryRequested: (() -> Unit)? = null
+) : ActionMode.Callback {
+ override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
+ requireNotNull(menu)
+ requireNotNull(mode)
+
+ onCopyRequested?.let {
+ menu.add(0, MENU_ITEM_COPY, 0, context.getString(R.string.copy))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
+ }
+
+ onTranslateRequested?.let {
+ menu.add(0, MENU_ITEM_TRANSLATE, 1, context.getString(R.string.translate))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
+ }
+
+ onDictionaryRequested?.let {
+ menu.add(0, MENU_ITEM_DICTIONARY, 2, context.getString(R.string.dictionary))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
+ }
+
+ return true
+ }
+
+ override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
+ return false
+ }
+
+ override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
+ when (item!!.itemId) {
+ MENU_ITEM_COPY -> onCopyRequested?.invoke()
+ MENU_ITEM_TRANSLATE -> onTranslateRequested?.invoke()
+ MENU_ITEM_DICTIONARY -> onDictionaryRequested?.invoke()
+ else -> return false
+ }
+ mode?.finish()
+ return true
+ }
+
+ override fun onDestroyActionMode(mode: ActionMode?) {}
+}
+
+/**
+ * Floating [CustomTextActionModeCallback].
+ */
+private class FloatingTextActionModeCallback(
+ val callback: CustomTextActionModeCallback
+) : ActionMode.Callback2() {
+ override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
+ return callback.onActionItemClicked(mode, item)
+ }
+
+ override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
+ return callback.onCreateActionMode(mode, menu)
+ }
+
+ override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
+ return callback.onPrepareActionMode(mode, menu)
+ }
+
+ override fun onDestroyActionMode(mode: ActionMode?) {
+ callback.onDestroyActionMode(mode)
+ }
+
+ override fun onGetContentRect(mode: ActionMode?, view: View?, outRect: android.graphics.Rect?) {
+ val rect = callback.rect
+ outRect?.set(
+ rect.left.toInt(),
+ rect.top.toInt(),
+ rect.right.toInt(),
+ rect.bottom.toInt()
+ )
+ }
+}
+
+/**
+ * Custom Selection Toolbar. Used in pair with [CustomSelectionContainer] to display custom toolbar.
+ */
+private class CustomSelectionToolbar(
+ private val view: View,
+ context: Context,
+ private val onCopyRequest: (() -> Unit)?,
+ private val onTranslateRequest: ((String) -> Unit)?,
+ private val onDictionaryRequest: ((String) -> Unit)?
+) : TextToolbar {
+ private var actionMode: ActionMode? = null
+ private val callback = CustomTextActionModeCallback(context = context)
+
+ val clipboardManager =
+ context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+
+ override var status: TextToolbarStatus by mutableStateOf(TextToolbarStatus.Hidden)
+ private set
+
+ override fun showMenu(
+ rect: Rect,
+ onCopyRequested: (() -> Unit)?,
+ onPasteRequested: (() -> Unit)?,
+ onCutRequested: (() -> Unit)?,
+ onSelectAllRequested: (() -> Unit)?
+ ) {
+ callback.rect = rect
+ callback.onCopyRequested = {
+ onCopyRequested?.invoke()
+ onCopyRequest?.invoke()
+ }
+ callback.onTranslateRequested = {
+ val previousClipboard = clipboardManager.primaryClip
+ onCopyRequested?.invoke()
+ val currentClipboard = clipboardManager.text
+
+ onTranslateRequest?.invoke(currentClipboard.toString())
+
+ if (previousClipboard != null) {
+ clipboardManager.setPrimaryClip(
+ previousClipboard
+ )
+ }
+ }
+ callback.onDictionaryRequested = {
+ val previousClipboard = clipboardManager.primaryClip
+ onCopyRequested?.invoke()
+ val currentClipboard = clipboardManager.text
+
+ onDictionaryRequest?.invoke(currentClipboard.toString())
+
+ if (previousClipboard != null) {
+ clipboardManager.setPrimaryClip(
+ previousClipboard
+ )
+ }
+ }
+
+ if (actionMode == null) {
+ status = TextToolbarStatus.Shown
+ actionMode = view.startActionMode(
+ FloatingTextActionModeCallback(callback),
+ ActionMode.TYPE_FLOATING
+ )
+ } else {
+ actionMode?.invalidate()
+ }
+ }
+
+ override fun hide() {
+ status = TextToolbarStatus.Hidden
+ actionMode?.finish()
+ actionMode = null
+ }
+}
+
+/**
+ * Custom selection container.
+ *
+ * @param onCopyRequested Callback for when the copy option is clicked.
+ * @param onTranslateRequested Callback for when the translate option is clicked.
+ * @param onDictionaryRequested Callback for when the dictionary option is clicked.
+ * @param content Selection container content.
+ */
+@Composable
+fun CustomSelectionContainer(
+ onCopyRequested: (() -> Unit),
+ onTranslateRequested: ((String) -> Unit),
+ onDictionaryRequested: ((String) -> Unit),
+ content: @Composable (Boolean) -> Unit
+) {
+ val view = LocalView.current
+ val context = LocalContext.current
+
+ val customSelectionToolbar = remember {
+ CustomSelectionToolbar(
+ view = view,
+ context = context,
+
+ onCopyRequest = {
+ onCopyRequested()
+ },
+ onTranslateRequest = {
+ onTranslateRequested(it)
+ },
+ onDictionaryRequest = {
+ onDictionaryRequested(it)
+ }
+ )
+ }
+ val isToolbarHidden = remember(customSelectionToolbar.status) {
+ derivedStateOf {
+ customSelectionToolbar.status == TextToolbarStatus.Hidden
+ }
+ }
+
+ CompositionLocalProvider(
+ LocalTextToolbar provides customSelectionToolbar
+ ) {
+ SelectionContainer {
+ content(isToolbarHidden.value)
+ }
+ }
+}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackBar.kt
deleted file mode 100644
index b23809c6..00000000
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackBar.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package ua.acclorite.book_story.presentation.components
-
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.DismissValue
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.SwipeToDismiss
-import androidx.compose.material.rememberDismissState
-import androidx.compose.material3.Snackbar
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-
-@OptIn(ExperimentalMaterialApi::class)
-@Composable
-fun CustomSnackbar(snackbarState: SnackbarHostState) {
- val dismissState = rememberDismissState { value ->
- if (value != DismissValue.Default) {
- snackbarState.currentSnackbarData?.dismiss()
- true
- } else false
- }
-
- LaunchedEffect(dismissState.currentValue) {
- if (dismissState.currentValue != DismissValue.Default) {
- dismissState.reset()
- }
- }
-
- SwipeToDismiss(
- modifier = Modifier
- .padding(vertical = 4.dp)
- .fillMaxWidth(),
- state = dismissState,
- background = {}
- ) {
- SnackbarHost(
- hostState = snackbarState,
- snackbar = { data ->
- Snackbar(
- data
- )
- }
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackbar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackbar.kt
new file mode 100644
index 00000000..46e76aac
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackbar.kt
@@ -0,0 +1,100 @@
+package ua.acclorite.book_story.presentation.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsDraggedAsState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Snackbar
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
+
+/**
+ * Custom Snackbar with vertical Swipe-To-Dismiss and animation.
+ * It is recommended to put it in Scaffold bottomBar(this way padding is the same at all sides)
+ *
+ * @param snackbarState Regular [SnackbarHostState], use ..remember { SnackbarHostState() }
+ */
+@Composable
+fun CustomSnackbar(modifier: Modifier = Modifier, snackbarState: SnackbarHostState) {
+ var showSnackbar by remember { mutableStateOf(false) }
+ var snackbarData by remember { mutableStateOf(snackbarState.currentSnackbarData) }
+ var offset by remember { mutableFloatStateOf(0f) }
+
+ val density = LocalDensity.current
+ val mutableInteractionSource = remember { MutableInteractionSource() }
+ val isDragged by mutableInteractionSource.collectIsDraggedAsState()
+
+ LaunchedEffect(offset, isDragged) {
+ val offsetDp = with(density) { offset.toDp() }
+ if (offsetDp > 35.dp && !isDragged) {
+ snackbarState.currentSnackbarData?.dismiss()
+ return@LaunchedEffect
+ }
+
+ if (offsetDp <= 35.dp && !isDragged) {
+ offset = 0f
+ }
+ }
+ LaunchedEffect(snackbarState.currentSnackbarData) {
+ val data = snackbarState.currentSnackbarData
+
+ if (data == null) {
+ showSnackbar = false
+ return@LaunchedEffect
+ }
+
+ offset = 0f
+ snackbarData = data
+ showSnackbar = true
+ }
+
+ AnimatedVisibility(
+ visible = showSnackbar,
+ enter = slideInVertically(tween(250)) { it } + fadeIn(tween(250)),
+ exit = slideOutVertically(tween(100)) { it } + fadeOut(tween(100))
+ ) {
+ Box(
+ modifier = modifier
+ .offset { IntOffset(0, offset.roundToInt()) }
+ .padding(vertical = 4.dp)
+ .fillMaxWidth()
+ .draggable(
+ interactionSource = mutableInteractionSource,
+ orientation = Orientation.Vertical,
+ state = rememberDraggableState {
+ offset = (offset + it).coerceAtLeast(0f)
+ }
+ )
+ ) {
+ Snackbar(
+ modifier = Modifier.align(Alignment.Center),
+ snackbarData = snackbarData!!,
+ shape = MaterialTheme.shapes.medium
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/GoBackButton.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/GoBackButton.kt
new file mode 100644
index 00000000..70afc399
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/GoBackButton.kt
@@ -0,0 +1,38 @@
+package ua.acclorite.book_story.presentation.components
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.stringResource
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.presentation.data.Navigator
+
+/**
+ * Go back arrow button. Prevents double or triple clicking on it while going back action is performed.
+ */
+@Composable
+fun GoBackButton(navigator: Navigator, customOnClick: () -> Unit = {}) {
+ var isClicked by remember { mutableStateOf(false) }
+
+ IconButton(
+ enabled = !isClicked,
+ onClick = {
+ isClicked = true
+ navigator.navigateBack()
+ customOnClick()
+ }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(id = R.string.go_back_content_desc),
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt
index c5cd1e4b..f71eb974 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt
@@ -21,9 +21,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.Screen
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
+/**
+ * More drop down. Navigates to Settings, About and Help screens.
+ */
@Composable
fun MoreDropDown(navigator: Navigator) {
var showDropDown by remember { mutableStateOf(false) }
@@ -58,7 +61,7 @@ fun MoreDropDown(navigator: Navigator) {
)
},
onClick = {
- navigator.navigate(Screen.SETTINGS)
+ navigator.navigate(Screen.SETTINGS, false)
showDropDown = false
},
contentPadding = PaddingValues(start = startPadding, end = endPadding)
@@ -72,7 +75,7 @@ fun MoreDropDown(navigator: Navigator) {
)
},
onClick = {
- navigator.navigate(Screen.HELP)
+ navigator.navigate(Screen.HELP, false)
showDropDown = false
},
contentPadding = PaddingValues(start = startPadding, end = endPadding)
@@ -86,7 +89,7 @@ fun MoreDropDown(navigator: Navigator) {
)
},
onClick = {
- navigator.navigate(Screen.ABOUT)
+ navigator.navigate(Screen.ABOUT, false)
showDropDown = false
},
contentPadding = PaddingValues(start = startPadding, end = endPadding)
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/BottomNavigationBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt
similarity index 80%
rename from app/src/main/java/ua/acclorite/book_story/presentation/components/BottomNavigationBar.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt
index b50bc246..e2cb079d 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/BottomNavigationBar.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.components
+package ua.acclorite.book_story.presentation.components.bottom_navigation_bar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
@@ -11,9 +11,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.domain.model.NavigationBarItem
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.Screen
+import ua.acclorite.book_story.domain.model.NavigationItem
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.ui.elevation
/**
@@ -37,34 +37,34 @@ fun BottomNavigationBar(
containerColor = MaterialTheme.elevation()
) {
BottomNavigationBarItem(
- item = NavigationBarItem(
+ item = NavigationItem(
stringResource(id = R.string.library_screen),
selectedIcon = painterResource(id = R.drawable.library_screen_filled),
unselectedIcon = painterResource(id = R.drawable.library_screen_outlined)
),
isSelected = currentScreen == Screen.LIBRARY
) {
- navigator.navigate(Screen.LIBRARY)
+ navigator.navigate(Screen.LIBRARY, false)
}
BottomNavigationBarItem(
- item = NavigationBarItem(
+ item = NavigationItem(
stringResource(id = R.string.history_screen),
selectedIcon = painterResource(id = R.drawable.history_screen_filled),
unselectedIcon = painterResource(id = R.drawable.history_screen_outlined)
),
isSelected = currentScreen == Screen.HISTORY
) {
- navigator.navigate(Screen.HISTORY)
+ navigator.navigate(Screen.HISTORY, false)
}
BottomNavigationBarItem(
- item = NavigationBarItem(
+ item = NavigationItem(
stringResource(id = R.string.browse_screen),
selectedIcon = painterResource(id = R.drawable.browse_screen_filled),
unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined)
),
isSelected = currentScreen == Screen.BROWSE
) {
- navigator.navigate(Screen.BROWSE)
+ navigator.navigate(Screen.BROWSE, false)
}
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/BottomNavigationItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt
similarity index 91%
rename from app/src/main/java/ua/acclorite/book_story/presentation/components/BottomNavigationItem.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt
index 94c8edac..eda6f49f 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/BottomNavigationItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.components
+package ua.acclorite.book_story.presentation.components.bottom_navigation_bar
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.size
@@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
-import ua.acclorite.book_story.domain.model.NavigationBarItem
+import ua.acclorite.book_story.domain.model.NavigationItem
/**
* Bottom Navigation Bar Item, uses default [NavigationBarItem].
@@ -19,7 +19,7 @@ import ua.acclorite.book_story.domain.model.NavigationBarItem
@Composable
fun RowScope.BottomNavigationBarItem(
modifier: Modifier = Modifier,
- item: NavigationBarItem,
+ item: NavigationItem,
isSelected: Boolean,
onClick: () -> Unit
) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDialogWithContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt
similarity index 87%
rename from app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDialogWithContent.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt
index a7751c64..0fc92a81 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDialogWithContent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.components
+package ua.acclorite.book_story.presentation.components.custom_dialog
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
@@ -35,6 +35,23 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.ElevationDefaults
import ua.acclorite.book_story.ui.elevation
+/**
+ * Custom Dialog with custom content.
+ *
+ * @param modifier Modifier to be applied.
+ * @param properties Dialog properties.
+ * @param drawableIcon Drawable icon to show(1/2).
+ * @param imageVectorIcon Image vector icon to show(2/2).
+ * @param backgroundTransparency The transparency of the background behind the dialog.
+ * @param title The title of the dialog.
+ * @param description The description of the dialog.
+ * @param actionText The action text of the dialog.
+ * @param isActionEnabled Whether action is enabled or not,
+ * @param onDismiss Code to execute when the dialog is dismissed.
+ * @param onAction Code to execute when action is clicked.
+ * @param withDivider Should divider be shown or not.
+ * @param customContent Custom content(not necessary, dialog also can be use just as alert dialog).
+ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomDialogWithContent(
@@ -54,7 +71,7 @@ fun CustomDialogWithContent(
) {
AlertDialog(
onDismissRequest = { onDismiss() },
- properties = properties,
+ properties = properties
) {
(LocalView.current.parent as DialogWindowProvider).window.setDimAmount(
backgroundTransparency
@@ -118,8 +135,8 @@ fun CustomDialogWithContent(
if (withDivider) {
Spacer(modifier = Modifier.height(12.dp))
Divider(
- color = MaterialTheme.colorScheme.outlineVariant,
- modifier = Modifier.padding(horizontal = 24.dp)
+ modifier = Modifier.padding(horizontal = 24.dp),
+ color = MaterialTheme.colorScheme.outlineVariant
)
Spacer(modifier = Modifier.height(12.dp))
} else {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDialogWithLazyColumn.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt
similarity index 88%
rename from app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDialogWithLazyColumn.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt
index 51cf8bb1..0e7ea033 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomDialogWithLazyColumn.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.components
+package ua.acclorite.book_story.presentation.components.custom_dialog
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
@@ -36,6 +36,24 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.ElevationDefaults
import ua.acclorite.book_story.ui.elevation
+
+/**
+ * Custom Dialog with lazy column.
+ *
+ * @param modifier Modifier to be applied.
+ * @param properties Dialog properties.
+ * @param drawableIcon Drawable icon to show(1/2).
+ * @param imageVectorIcon Image vector icon to show(2/2).
+ * @param backgroundTransparency The transparency of the background behind the dialog.
+ * @param title The title of the dialog.
+ * @param description The description of the dialog.
+ * @param actionText The action text of the dialog.
+ * @param isActionEnabled Whether action is enabled or not,
+ * @param onDismiss Code to execute when the dialog is dismissed.
+ * @param onAction Code to execute when action is clicked.
+ * @param withDivider Should divider be shown or not.
+ * @param items The items of this dialog, use items(..) { .. } or item { .. }.
+ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomDialogWithLazyColumn(
@@ -55,7 +73,7 @@ fun CustomDialogWithLazyColumn(
) {
AlertDialog(
onDismissRequest = { onDismiss() },
- properties = properties,
+ properties = properties
) {
(LocalView.current.parent as DialogWindowProvider).window.setDimAmount(
backgroundTransparency
@@ -119,8 +137,8 @@ fun CustomDialogWithLazyColumn(
if (withDivider) {
Spacer(modifier = Modifier.height(12.dp))
Divider(
- color = MaterialTheme.colorScheme.outlineVariant,
- modifier = Modifier.padding(horizontal = 24.dp)
+ modifier = Modifier.padding(horizontal = 24.dp),
+ color = MaterialTheme.colorScheme.outlineVariant
)
Spacer(modifier = Modifier.height(12.dp))
} else {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/SelectableDialogItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/SelectableDialogItem.kt
similarity index 91%
rename from app/src/main/java/ua/acclorite/book_story/presentation/components/SelectableDialogItem.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/SelectableDialogItem.kt
index 7c638013..7e476e36 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/SelectableDialogItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/SelectableDialogItem.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.components
+package ua.acclorite.book_story.presentation.components.custom_dialog
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
@@ -17,6 +17,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+/**
+ * Selectable dialog item. Perfectly combines with [CustomDialogWithLazyColumn].
+ */
@Composable
fun SelectableDialogItem(selected: Boolean, title: String, onClick: () -> Unit) {
Row(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt
new file mode 100644
index 00000000..939d68ea
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt
@@ -0,0 +1,90 @@
+package ua.acclorite.book_story.presentation.components.custom_navigation_rail
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationRail
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.domain.model.NavigationItem
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
+import ua.acclorite.book_story.ui.elevation
+
+/**
+ * Custom Navigation Rail. It is used to be shown on Tablets.
+ */
+@Composable
+fun CustomNavigationRail(
+ navigator: Navigator
+) {
+ var currentScreen: Screen? by remember { mutableStateOf(null) }
+
+ LaunchedEffect(Unit) {
+ navigator.getCurrentScreen().collect {
+ if (it == Screen.LIBRARY || it == Screen.HISTORY || it == Screen.BROWSE) {
+ currentScreen = it
+ }
+ }
+ }
+
+ NavigationRail(
+ modifier = Modifier
+ .width(80.dp)
+ .background(MaterialTheme.elevation())
+ .padding(horizontal = 12.dp),
+ containerColor = MaterialTheme.elevation()
+ ) {
+ Column(
+ Modifier
+ .fillMaxHeight(),
+ verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ CustomNavigationRailItem(
+ item = NavigationItem(
+ stringResource(id = R.string.library_screen),
+ selectedIcon = painterResource(id = R.drawable.library_screen_filled),
+ unselectedIcon = painterResource(id = R.drawable.library_screen_outlined)
+ ),
+ isSelected = currentScreen == Screen.LIBRARY
+ ) {
+ navigator.navigate(Screen.LIBRARY, false)
+ }
+ CustomNavigationRailItem(
+ item = NavigationItem(
+ stringResource(id = R.string.history_screen),
+ selectedIcon = painterResource(id = R.drawable.history_screen_filled),
+ unselectedIcon = painterResource(id = R.drawable.history_screen_outlined)
+ ),
+ isSelected = currentScreen == Screen.HISTORY
+ ) {
+ navigator.navigate(Screen.HISTORY, false)
+ }
+ CustomNavigationRailItem(
+ item = NavigationItem(
+ stringResource(id = R.string.browse_screen),
+ selectedIcon = painterResource(id = R.drawable.browse_screen_filled),
+ unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined)
+ ),
+ isSelected = currentScreen == Screen.BROWSE
+ ) {
+ navigator.navigate(Screen.BROWSE, false)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt
new file mode 100644
index 00000000..80ed8e28
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt
@@ -0,0 +1,54 @@
+package ua.acclorite.book_story.presentation.components.custom_navigation_rail
+
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationRailItem
+import androidx.compose.material3.NavigationRailItemDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import ua.acclorite.book_story.domain.model.NavigationItem
+
+/**
+ * Custom Navigation Rail item.
+ */
+@Composable
+fun CustomNavigationRailItem(
+ modifier: Modifier = Modifier,
+ item: NavigationItem,
+ isSelected: Boolean,
+ onClick: () -> Unit
+) {
+ NavigationRailItem(
+ label = {
+ Text(
+ text = item.title,
+ style = MaterialTheme.typography.labelMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ selected = isSelected,
+ enabled = !isSelected,
+ onClick = { onClick() },
+ icon = {
+ Icon(
+ painter =
+ if (isSelected) item.selectedIcon
+ else item.unselectedIcon,
+ contentDescription = item.title,
+ modifier = Modifier.size(24.dp)
+ )
+ },
+ modifier = modifier,
+ colors = NavigationRailItemDefaults.colors(
+ unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ disabledIconColor = MaterialTheme.colorScheme.onSurface,
+ disabledTextColor = MaterialTheme.colorScheme.onSurface
+ )
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/IsEmpty.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/is_messages/IsEmpty.kt
similarity index 87%
rename from app/src/main/java/ua/acclorite/book_story/presentation/components/IsEmpty.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/components/is_messages/IsEmpty.kt
index 0a4d67a5..311afe73 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/IsEmpty.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/is_messages/IsEmpty.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.components
+package ua.acclorite.book_story.presentation.components.is_messages
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -20,6 +20,16 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+
+/**
+ * Is Empty message. It usually placed when the list is empty.
+ *
+ * @param modifier Modifier to be applied.
+ * @param message The message.
+ * @param icon Icon to be displayed.
+ * @param actionTitle Optional action.
+ * @param action On click action.
+ */
@Composable
fun IsEmpty(
modifier: Modifier = Modifier,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/IsError.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/is_messages/IsError.kt
similarity index 85%
rename from app/src/main/java/ua/acclorite/book_story/presentation/components/IsError.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/components/is_messages/IsError.kt
index 61bf90b1..25e5e20b 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/IsError.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/is_messages/IsError.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.components
+package ua.acclorite.book_story.presentation.components.is_messages
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -20,6 +20,15 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+/**
+ * Is Empty message. It usually placed when something went wrong and you need to display large error message.
+ *
+ * @param modifier Modifier to be applied.
+ * @param errorMessage The error message.
+ * @param icon Icon to be displayed.
+ * @param actionTitle Optional action.
+ * @param action On click action.
+ */
@Composable
fun IsError(
modifier: Modifier = Modifier,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/ArithmeticOperations.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/ArithmeticOperations.kt
index 71b426bd..e80b1fc2 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/data/ArithmeticOperations.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/ArithmeticOperations.kt
@@ -1,5 +1,9 @@
package ua.acclorite.book_story.presentation.data
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Path
+import kotlin.math.abs
+
fun String.removeTrailingZero(): String {
if (!this.contains('.'))
return this
@@ -8,4 +12,19 @@ fun String.removeTrailingZero(): String {
.dropLastWhile { it == '.' }
}
-fun Double.removeDigits(digits: Int) = "%.${digits}f".format(this).replace(",", ".")
\ No newline at end of file
+fun Double.removeDigits(digits: Int) = "%.${digits}f".format(this).replace(",", ".")
+
+fun calculateFamiliarity(string: String, target: String): Int {
+ val targetCounts = target.lowercase().trim().groupingBy { it }.eachCount()
+ val familiarity = string.lowercase().trim().sumOf { targetCounts.getOrDefault(it, 0) }
+ return familiarity
+}
+
+fun Path.standardQuadFromTo(from: Offset, to: Offset) {
+ quadraticTo(
+ from.x,
+ from.y,
+ abs(from.x + to.x) / 2f,
+ abs(from.y + to.y) / 2f
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt
index 73b2fffe..08020779 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt
@@ -19,6 +19,7 @@ import kotlinx.coroutines.launch
import ua.acclorite.book_story.domain.use_case.ChangeLanguage
import ua.acclorite.book_story.domain.use_case.GetDatastore
import ua.acclorite.book_story.domain.use_case.SetDatastore
+import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.DarkTheme
import ua.acclorite.book_story.ui.Theme
import ua.acclorite.book_story.ui.toDarkTheme
@@ -264,19 +265,47 @@ class MainViewModel @Inject constructor(
}
}
- fun init(activity: ComponentActivity) {
+ fun init(
+ activity: ComponentActivity,
+ libraryViewModel: LibraryViewModel,
+ ) {
+ val isViewModelsReady = combine(
+ libraryViewModel.isReady,
+ ) { values ->
+ values.all { it }
+ }
+
val isDataReady = combine(
- language, theme, darkTheme, showStartScreen, backgroundColor,
- fontColor, fontFamily, isItalic, fontSize, lineHeight,
- paragraphHeight, paragraphIndentation
+ language,
+ theme,
+ darkTheme,
+ showStartScreen,
+ backgroundColor,
+ fontColor,
+ fontFamily,
+ isItalic,
+ fontSize,
+ lineHeight,
+ paragraphHeight,
+ paragraphIndentation
) { values ->
values.all { it != null }
}
+ val isReady = combine(
+ isViewModelsReady,
+ isDataReady
+ ) { values ->
+ values.all { it }
+ }
+
viewModelScope.launch {
- isDataReady.collect { bool ->
+ isReady.collect { bool ->
if (bool) {
- _isReady.value = true
+ _isReady.update {
+ true
+ }
+ return@collect
}
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/Navigation.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt
similarity index 81%
rename from app/src/main/java/ua/acclorite/book_story/presentation/Navigation.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt
index ac384499..c9cf246e 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/Navigation.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation
+package ua.acclorite.book_story.presentation.data
import android.annotation.SuppressLint
import androidx.activity.ComponentActivity
@@ -33,6 +33,7 @@ import ua.acclorite.book_story.ui.Transitions
private const val CURRENT_SCREEN = "current_screen"
private const val BACKSTACK = "back_stack"
+private const val USE_BACK_ANIM = "use_back_animation"
/**
* All screens are listed here, later each screen will be passed as a param for [Navigator.composable] function.
@@ -48,9 +49,11 @@ enum class Screen {
SETTINGS,
GENERAL_SETTINGS,
APPEARANCE_SETTINGS,
+ READER_SETTINGS,
ABOUT,
- HELP
+ HELP,
+ START
}
data class Argument(
@@ -72,6 +75,7 @@ class Navigator @AssistedInject constructor(
) : ViewModel() {
private val currentScreen = savedStateHandle.getStateFlow(CURRENT_SCREEN, startScreen)
+ private val useBackAnimation = savedStateHandle.getStateFlow(USE_BACK_ANIM, false)
private val backStack = savedStateHandle.getStateFlow(BACKSTACK, mutableListOf())
private val arguments = mutableStateListOf()
@@ -104,7 +108,7 @@ class Navigator @AssistedInject constructor(
arguments.removeIf { it.key == key }
}
- fun navigate(screen: Screen, vararg args: Argument) =
+ fun navigate(screen: Screen, useBackAnimation: Boolean, vararg args: Argument) =
viewModelScope.launch(Dispatchers.Default) {
backStack.value.add(currentScreen.value)
@@ -112,10 +116,11 @@ class Navigator @AssistedInject constructor(
putArgument(it)
}
+ savedStateHandle[USE_BACK_ANIM] = useBackAnimation
savedStateHandle[CURRENT_SCREEN] = screen
}
- fun navigateWithoutBackStack(screen: Screen, vararg args: Argument) =
+ fun navigateWithoutBackStack(screen: Screen, useBackAnimation: Boolean, vararg args: Argument) =
viewModelScope.launch(Dispatchers.Default) {
if (backStack.value.last() == screen) {
backStack.value.removeLast()
@@ -125,15 +130,18 @@ class Navigator @AssistedInject constructor(
putArgument(it)
}
+ savedStateHandle[USE_BACK_ANIM] = useBackAnimation
savedStateHandle[CURRENT_SCREEN] = screen
}
- fun navigateBack() = viewModelScope.launch(Dispatchers.Default) {
- if (canGoBack()) {
- savedStateHandle[CURRENT_SCREEN] = backStack.value.last()
- backStack.value.removeLast()
+ fun navigateBack(useBackAnimation: Boolean = true) =
+ viewModelScope.launch(Dispatchers.Default) {
+ if (canGoBack()) {
+ savedStateHandle[USE_BACK_ANIM] = useBackAnimation
+ savedStateHandle[CURRENT_SCREEN] = backStack.value.last()
+ backStack.value.removeLast()
+ }
}
- }
fun canGoBack(): Boolean {
return backStack.value.isNotEmpty()
@@ -148,7 +156,9 @@ class Navigator @AssistedInject constructor(
*
* @param screen The [Screen] that represents [content].
* @param enterAnim Enter Animation.
+ * @param backEnterAnim Enter Animation for navigating back.
* @param exitAnim Exit Animation.
+ * @param backExitAnim Exit Animation for navigating back.
* @param content The Screen content to show when [Navigator.currentScreen] equals [screen].
*/
@SuppressLint("ComposableNaming")
@@ -156,14 +166,16 @@ class Navigator @AssistedInject constructor(
fun composable(
screen: Screen,
enterAnim: EnterTransition = Transitions.FadeTransitionIn,
+ backEnterAnim: EnterTransition = Transitions.BackSlidingTransitionIn,
exitAnim: ExitTransition = Transitions.FadeTransitionOut,
+ backExitAnim: ExitTransition = Transitions.BackSlidingTransitionOut,
content: @Composable () -> Unit
) {
AnimatedVisibility(
visible = getCurrentScreen().collectAsState().value == screen,
- enter = enterAnim,
- exit = exitAnim,
- label = screen.toString()
+ enter = if (!useBackAnimation.collectAsState().value) enterAnim else backEnterAnim,
+ exit = if (!useBackAnimation.collectAsState().value) exitAnim else backExitAnim,
+ label = ""
) {
content()
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt
index 140a1ce6..9e417482 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt
@@ -21,10 +21,15 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.ExperimentalMaterialApi
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.material.icons.filled.Refresh
+import androidx.compose.material.pullrefresh.PullRefreshDefaults
+import androidx.compose.material.pullrefresh.PullRefreshIndicator
+import androidx.compose.material.pullrefresh.pullRefresh
+import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
@@ -32,6 +37,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
@@ -43,32 +49,33 @@ import androidx.compose.runtime.remember
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
import dagger.hilt.android.lifecycle.withCreationCallback
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
-import ua.acclorite.book_story.presentation.Argument
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.Screen
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
+import ua.acclorite.book_story.presentation.components.CustomSnackbar
+import ua.acclorite.book_story.presentation.components.GoBackButton
+import ua.acclorite.book_story.presentation.data.Argument
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoBackground
-import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoChangeCoverBottomSheet
-import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDeleteDialog
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDescriptionSection
-import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDetailsBottomSheet
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoInfoSection
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoMoreDropDown
-import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoMoveDialog
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoStatisticSection
+import ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet.BookInfoChangeCoverBottomSheet
+import ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet.BookInfoDetailsBottomSheet
+import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoDeleteDialog
+import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoMoveDialog
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
+import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
@@ -77,12 +84,12 @@ import ua.acclorite.book_story.ui.Transitions
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.util.Constants
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
fun BookInfoScreen(
- libraryViewModel: LibraryViewModel = hiltViewModel(),
- browseViewModel: BrowseViewModel = hiltViewModel(),
- historyViewModel: HistoryViewModel = hiltViewModel(),
+ libraryViewModel: LibraryViewModel,
+ browseViewModel: BrowseViewModel,
+ historyViewModel: HistoryViewModel,
navigator: Navigator
) {
val context = LocalContext.current
@@ -104,22 +111,29 @@ fun BookInfoScreen(
val state by viewModel.state.collectAsState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val listState = rememberLazyListState()
-
+ val snackbarState = remember { SnackbarHostState() }
+ val refreshState = rememberPullRefreshState(
+ refreshing = state.isRefreshing,
+ refreshThreshold = PullRefreshDefaults.RefreshThreshold + 32.dp,
+ refreshingOffset = PullRefreshDefaults.RefreshingOffset + 64.dp,
+ onRefresh = {
+ viewModel.onEvent(
+ BookInfoEvent.OnUpdateBook(
+ refreshList = {
+ libraryViewModel.onEvent(LibraryEvent.OnLoadList)
+ historyViewModel.onEvent(HistoryEvent.OnLoadList)
+ },
+ snackbarState,
+ context
+ )
+ )
+ }
+ )
val firstVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } }
LaunchedEffect(Unit) {
viewModel.init(
- navigator,
- failure = {
- Toast.makeText(
- context,
- context.getString(
- R.string.file_not_found,
- it.takeLast(40)
- ),
- Toast.LENGTH_LONG
- ).show()
- }
+ navigator
)
}
@@ -153,8 +167,9 @@ fun BookInfoScreen(
Scaffold(
Modifier
.fillMaxSize()
- .nestedScroll(scrollBehavior.nestedScrollConnection)
- .windowInsetsPadding(WindowInsets.navigationBars),
+ .windowInsetsPadding(WindowInsets.navigationBars)
+ .pullRefresh(refreshState)
+ .nestedScroll(scrollBehavior.nestedScrollConnection),
containerColor = MaterialTheme.colorScheme.surface,
topBar = {
AnimatedTopAppBar(
@@ -164,15 +179,7 @@ fun BookInfoScreen(
isTopBarScrolled = null,
content1NavigationIcon = {
- IconButton(onClick = {
- navigator.navigateBack()
- }) {
- Icon(
- imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
- contentDescription = "Go back",
- tint = MaterialTheme.colorScheme.onSurface
- )
- }
+ GoBackButton(navigator = navigator)
},
content1Title = {
DefaultTransition(visible = firstVisibleItemIndex > 0) {
@@ -186,11 +193,37 @@ fun BookInfoScreen(
}
},
content1Actions = {
+ IconButton(
+ enabled = !state.isRefreshing,
+ onClick = {
+ viewModel.onEvent(
+ BookInfoEvent.OnUpdateBook(
+ refreshList = {
+ libraryViewModel.onEvent(LibraryEvent.OnLoadList)
+ historyViewModel.onEvent(HistoryEvent.OnLoadList)
+ },
+ snackbarState,
+ context
+ )
+ )
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Default.Refresh,
+ contentDescription = stringResource(id = R.string.refresh_book_content_desc),
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
Box {
DefaultTransition(
visible = !state.editTitle
) {
- BookInfoMoreDropDown(viewModel = viewModel)
+ BookInfoMoreDropDown(
+ viewModel = viewModel,
+ snackbarState = snackbarState
+ )
}
androidx.compose.animation.AnimatedVisibility(
visible = state.editTitle,
@@ -230,6 +263,12 @@ fun BookInfoScreen(
}
}
)
+ },
+ bottomBar = {
+ CustomSnackbar(
+ modifier = Modifier.padding(bottom = 70.dp),
+ snackbarState = snackbarState
+ )
}
) { paddingValues ->
Box(
@@ -246,7 +285,7 @@ fun BookInfoScreen(
if (state.book.coverImage != null) {
BookInfoBackground(
height = paddingValues.calculateTopPadding() + 12.dp + 195.dp,
- image = state.book.coverImage!!.asImageBitmap()
+ image = state.book.coverImage!!
)
}
@@ -271,48 +310,54 @@ fun BookInfoScreen(
}
}
- if (state.book.file != null) {
- FloatingActionButton(
- onClick = {
- navigator.navigate(
- Screen.READER, Argument(
- "book", state.book
- )
+ FloatingActionButton(
+ onClick = {
+ navigator.navigate(
+ Screen.READER, false, 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)
+ )
+ },
+ shape = MaterialTheme.shapes.large,
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(16.dp),
+ 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 = stringResource(id = R.string.continue_reading_content_desc),
+ Modifier.size(24.dp)
+ )
+ AnimatedVisibility(
+ visible = !listState.canScrollBackward
+ ) {
+ 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)
)
- AnimatedVisibility(
- visible = !listState.canScrollBackward
- ) {
- 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))
}
+ Spacer(modifier = Modifier.width(16.dp))
}
- )
- }
+ }
+ )
+
+ PullRefreshIndicator(
+ state.isRefreshing,
+ refreshState,
+ Modifier.align(Alignment.TopCenter),
+ backgroundColor = MaterialTheme.colorScheme.inverseSurface,
+ contentColor = MaterialTheme.colorScheme.inverseOnSurface
+ )
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt
index 31d3dc3f..722e60ed 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt
@@ -15,6 +15,9 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+/**
+ * Background of the BookInfoScreen.
+ */
@Composable
fun BookInfoBackground(height: Dp, image: ImageBitmap) {
val background = MaterialTheme.colorScheme.background
@@ -34,7 +37,7 @@ fun BookInfoBackground(height: Dp, image: ImageBitmap) {
)
)
}
- .blur(2.dp),
+ .blur(3.dp),
alpha = 0.3f,
contentScale = ContentScale.Crop
)
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt
index f4f4ab4b..8c06c645 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt
@@ -14,12 +14,15 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
+/**
+ * Description section.
+ */
@Composable
fun BookInfoDescriptionSection(book: Book) {
SelectionContainer {
Text(
- book.description
- ?: stringResource(id = R.string.error_no_description),
+ if (book.description?.isNotBlank() == true) book.description
+ else stringResource(id = R.string.error_no_description),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Start,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt
index 61c385a9..06af60bf 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt
@@ -34,7 +34,6 @@ 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
@@ -46,6 +45,9 @@ import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.ui.elevation
+/**
+ * Info section.
+ */
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
@@ -84,7 +86,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
if (book.coverImage != null) {
Image(
- bitmap = book.coverImage.asImageBitmap(),
+ bitmap = book.coverImage,
contentDescription = "Cover",
modifier = Modifier
.fillMaxSize()
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt
index 756cac9e..61f1ef0a 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt
@@ -11,6 +11,7 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -23,8 +24,11 @@ import ua.acclorite.book_story.presentation.components.CustomDropDownMenuItem
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
+/**
+ * Book info more dropdown. Has three options: Delete, Move and Details.
+ */
@Composable
-fun BookInfoMoreDropDown(viewModel: BookInfoViewModel) {
+fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHostState) {
val state by viewModel.state.collectAsState()
Box {
@@ -64,6 +68,7 @@ fun BookInfoMoreDropDown(viewModel: BookInfoViewModel) {
leadingIcon = Icons.Outlined.Info,
text = stringResource(id = R.string.details_book)
) {
+ snackbarState.currentSnackbarData?.dismiss()
viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown)
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt
index a08305bb..4d0410ba 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt
@@ -19,6 +19,9 @@ import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero
+/**
+ * Statistic section.
+ */
@Composable
fun BookInfoStatisticSection(book: Book) {
Column(
@@ -41,16 +44,16 @@ fun BookInfoStatisticSection(book: Book) {
LinearProgressIndicator(
book.progress.coerceAtLeast(0.01f),
- strokeCap = StrokeCap.Round,
modifier = Modifier
.fillMaxWidth(),
color = MaterialTheme.colorScheme.primary,
- trackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(0.05f)
+ trackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(0.05f),
+ strokeCap = StrokeCap.Round,
)
Spacer(modifier = Modifier.height(6.dp))
Text(
- text = stringResource(
+ text = if (book.progress < 1f) stringResource(
id = R.string.you_read_query,
(book.progress * 100)
.toDouble()
@@ -59,7 +62,7 @@ fun BookInfoStatisticSection(book: Book) {
) + " " + stringResource(
if (book.progress > 0.2f) R.string.read_keep
else R.string.read_more
- ),
+ ) else stringResource(id = R.string.read_done),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt
similarity index 97%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheet.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt
index 5e6e4eca..072a1d45 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheet.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.book_info.components
+package ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
@@ -32,6 +32,9 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
import ua.acclorite.book_story.ui.ElevationDefaults
import ua.acclorite.book_story.ui.elevation
+/**
+ * Change cover bottom sheet. Lets user select photo from the gallery and replaces old one.
+ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BookInfoChangeCoverBottomSheet(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheetItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt
similarity index 96%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheetItem.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt
index 7778d6a9..bbd0080e 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheetItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.book_info.components
+package ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -21,6 +21,9 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+/**
+ * Change cover bottom sheet item.
+ */
@Composable
fun BookInfoChangeCoverBottomSheetItem(
icon: ImageVector,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDetailsBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt
similarity index 97%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDetailsBottomSheet.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt
index 6a460b8a..7797eab8 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDetailsBottomSheet.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.book_info.components
+package ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet
import android.widget.Toast
import androidx.compose.foundation.layout.Spacer
@@ -25,6 +25,9 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
+/**
+ * Details bottom sheet. Displays name, path, last opened and size of the file.
+ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BookInfoDetailsBottomSheet(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDetailsBottomSheetItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt
similarity index 96%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDetailsBottomSheetItem.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt
index 662ff4d3..22cf689f 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDetailsBottomSheetItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.book_info.components
+package ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
@@ -18,6 +18,9 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+/**
+ * Details bottom sheet item.
+ */
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun BookInfoDetailsBottomSheetItem(
@@ -51,7 +54,6 @@ fun BookInfoDetailsBottomSheetItem(
Spacer(modifier = Modifier.height(2.dp))
Text(
description,
- maxLines = 3,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDeleteDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt
similarity index 91%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDeleteDialog.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt
index 84921428..e395d483 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDeleteDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.book_info.components
+package ua.acclorite.book_story.presentation.screens.book_info.components.dialog
import android.widget.Toast
import androidx.compose.material.icons.Icons
@@ -7,8 +7,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.components.CustomDialogWithContent
+import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
+import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
@@ -18,6 +18,9 @@ import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewMode
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
+/**
+ * Delete dialog. Deletes current book.
+ */
@Composable
fun BookInfoDeleteDialog(
libraryViewModel: LibraryViewModel,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt
similarity index 90%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoveDialog.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt
index 92411f5d..12355b58 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoveDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.book_info.components
+package ua.acclorite.book_story.presentation.screens.book_info.components.dialog
import android.widget.Toast
import androidx.compose.foundation.lazy.items
@@ -13,14 +13,17 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Category
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.components.CustomDialogWithLazyColumn
-import ua.acclorite.book_story.presentation.components.SelectableDialogItem
+import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn
+import ua.acclorite.book_story.presentation.components.custom_dialog.SelectableDialogItem
+import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
+/**
+ * Move dialog. Moves current book to the selected category.
+ */
@Composable
fun BookInfoMoveDialog(
libraryViewModel: LibraryViewModel,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt
index dce99736..f30828d2 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt
@@ -2,9 +2,10 @@ package ua.acclorite.book_story.presentation.screens.book_info.data
import android.content.Context
import android.net.Uri
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Category
-import ua.acclorite.book_story.presentation.Navigator
+import ua.acclorite.book_story.presentation.data.Navigator
sealed class BookInfoEvent {
data object OnShowHideChangeCoverBottomSheet : BookInfoEvent()
@@ -33,4 +34,18 @@ sealed class BookInfoEvent {
val text: String,
val success: () -> Unit
) : BookInfoEvent()
+
+ data class OnShowSnackbar(
+ val text: String,
+ val action: String?,
+ val onAction: () -> Unit = {},
+ val durationMillis: Long,
+ val snackbarState: SnackbarHostState
+ ) : BookInfoEvent()
+
+ data class OnUpdateBook(
+ val refreshList: () -> Unit,
+ val snackbarState: SnackbarHostState,
+ val context: Context
+ ) : BookInfoEvent()
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt
index 7c580f2b..9e6f8a28 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt
@@ -7,6 +7,7 @@ import ua.acclorite.book_story.domain.model.Category
@Immutable
data class BookInfoState(
val book: Book,
+ val isRefreshing: Boolean = false,
val editTitle: Boolean = false,
val hasFocused: Boolean = false,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt
index 559cd40b..48511fe1 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt
@@ -5,6 +5,8 @@ import android.content.ClipboardManager
import android.content.Context.CLIPBOARD_SERVICE
import android.graphics.BitmapFactory
import android.os.Build
+import androidx.compose.material3.SnackbarResult
+import androidx.compose.ui.graphics.asImageBitmap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
@@ -12,28 +14,36 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
+import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.use_case.DeleteBooks
+import ua.acclorite.book_story.domain.use_case.GetBookFromFile
import ua.acclorite.book_story.domain.use_case.UpdateBooks
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.Screen
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
@HiltViewModel(assistedFactory = BookInfoViewModel.Factory::class)
class BookInfoViewModel @AssistedInject constructor(
@Assisted book: Book,
private val updateBooks: UpdateBooks,
- private val deleteBooks: DeleteBooks
+ private val deleteBooks: DeleteBooks,
+ private val getBookFromFile: GetBookFromFile
) : ViewModel() {
private val _state = MutableStateFlow(BookInfoState(book))
val state = _state.asStateFlow()
+ var job: Job? = null
+
fun onEvent(event: BookInfoEvent) {
when (event) {
is BookInfoEvent.OnShowHideChangeCoverBottomSheet -> {
@@ -47,9 +57,11 @@ class BookInfoViewModel @AssistedInject constructor(
is BookInfoEvent.OnChangeCover -> {
viewModelScope.launch {
val image = event.context.contentResolver?.openInputStream(event.uri)?.use {
- BitmapFactory.decodeStream(it)
+ BitmapFactory.decodeStream(it).asImageBitmap()
} ?: return@launch
+ image.prepareToDraw()
+
updateBooks.execute(
listOf(
_state.value.book.copy(coverImage = image)
@@ -221,7 +233,7 @@ class BookInfoViewModel @AssistedInject constructor(
it != _state.value.selectedCategory
}.size - 1
)
- event.navigator.navigate(Screen.LIBRARY)
+ event.navigator.navigate(Screen.LIBRARY, true)
}
}
@@ -243,10 +255,139 @@ class BookInfoViewModel @AssistedInject constructor(
event.success()
}
}
+
+ is BookInfoEvent.OnShowSnackbar -> {
+ viewModelScope.launch {
+ job?.cancel()
+ event.snackbarState.currentSnackbarData?.dismiss()
+
+ if (event.durationMillis > 0) {
+ job = viewModelScope.launch(Dispatchers.IO) {
+ delay(event.durationMillis)
+ event.snackbarState.currentSnackbarData?.dismiss()
+ }
+ }
+
+ val snackbar = event.snackbarState.showSnackbar(
+ event.text,
+ actionLabel = event.action
+ )
+
+ when (snackbar) {
+ SnackbarResult.Dismissed -> Unit
+ SnackbarResult.ActionPerformed -> {
+ event.onAction()
+ }
+ }
+ }
+ }
+
+ is BookInfoEvent.OnUpdateBook -> {
+ viewModelScope.launch(Dispatchers.IO) {
+ _state.update {
+ it.copy(
+ isRefreshing = true
+ )
+ }
+
+ if (_state.value.book.file == null) {
+ onEvent(
+ BookInfoEvent.OnShowSnackbar(
+ text = event.context.getString(
+ R.string.file_not_found,
+ _state.value.book.filePath
+ .substringAfterLast("/")
+ .takeLast(25)
+ ),
+ action = event.context.getString(R.string.retry),
+ onAction = {
+ onEvent(
+ BookInfoEvent.OnUpdateBook(
+ refreshList = event.refreshList,
+ snackbarState = event.snackbarState,
+ context = event.context
+ )
+ )
+ },
+ durationMillis = 4000L,
+ snackbarState = event.snackbarState
+ )
+ )
+ delay(500)
+ _state.update {
+ it.copy(
+ isRefreshing = false
+ )
+ }
+ return@launch
+ }
+
+ val nullableBook = getBookFromFile.execute(_state.value.book.file!!)
+
+ if (nullableBook is NullableBook.Null) {
+ onEvent(
+ BookInfoEvent.OnShowSnackbar(
+ text = nullableBook.message?.asString(event.context)
+ ?: event.context.getString(R.string.error_something_went_wrong),
+ action = event.context.getString(R.string.retry),
+ onAction = {
+ onEvent(
+ BookInfoEvent.OnUpdateBook(
+ refreshList = event.refreshList,
+ snackbarState = event.snackbarState,
+ context = event.context
+ )
+ )
+ },
+ durationMillis = 4000L,
+ snackbarState = event.snackbarState
+ )
+ )
+
+ delay(500)
+ _state.update {
+ it.copy(
+ isRefreshing = false
+ )
+ }
+ return@launch
+ }
+
+ val updatedBook = nullableBook.book?.first ?: return@launch
+
+ _state.update {
+ it.copy(
+ book = it.book.copy(
+ author = updatedBook.author,
+ description = updatedBook.description,
+ text = updatedBook.text
+ )
+ )
+ }
+ updateBooks.execute(listOf(_state.value.book))
+ event.refreshList()
+
+ onEvent(
+ BookInfoEvent.OnShowSnackbar(
+ event.context.getString(R.string.book_updated),
+ action = null,
+ durationMillis = 4000L,
+ snackbarState = event.snackbarState
+ )
+ )
+
+ delay(500)
+ _state.update {
+ it.copy(
+ isRefreshing = false
+ )
+ }
+ }
+ }
}
}
- fun init(navigator: Navigator, failure: (String) -> Unit) {
+ fun init(navigator: Navigator) {
viewModelScope.launch {
val book = navigator.retrieveArgument("book") as? Book
@@ -255,10 +396,6 @@ class BookInfoViewModel @AssistedInject constructor(
return@launch
}
- if (book.file == null) {
- failure(book.filePath)
- }
-
_state.update {
BookInfoState(book = book)
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt
index ca5d96ab..36f6a1fa 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt
@@ -53,24 +53,24 @@ 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 androidx.hilt.navigation.compose.hiltViewModel
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
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.Screen
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
-import ua.acclorite.book_story.presentation.components.IsEmpty
-import ua.acclorite.book_story.presentation.components.IsError
import ua.acclorite.book_story.presentation.components.MoreDropDown
-import ua.acclorite.book_story.presentation.screens.browse.components.BrowseAddingDialog
+import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
+import ua.acclorite.book_story.presentation.components.is_messages.IsError
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.browse.components.BrowseFileItem
import ua.acclorite.book_story.presentation.screens.browse.components.BrowseStoragePermissionDialog
+import ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog.BrowseAddingDialog
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
+import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.DefaultTransition
import ua.acclorite.book_story.ui.Transitions
import ua.acclorite.book_story.ui.elevation
@@ -83,7 +83,8 @@ import ua.acclorite.book_story.ui.elevation
)
@Composable
fun BrowseScreen(
- viewModel: BrowseViewModel = hiltViewModel(),
+ viewModel: BrowseViewModel,
+ libraryViewModel: LibraryViewModel,
navigator: Navigator
) {
val permissionState = rememberPermissionState(
@@ -127,7 +128,11 @@ fun BrowseScreen(
BrowseStoragePermissionDialog(viewModel, permissionState)
}
if (state.showAddingDialog) {
- BrowseAddingDialog(viewModel = viewModel, navigator = navigator)
+ BrowseAddingDialog(
+ viewModel = viewModel,
+ navigator = navigator,
+ libraryViewModel = libraryViewModel
+ )
}
Scaffold(
@@ -270,13 +275,14 @@ fun BrowseScreen(
columns = GridCells.Adaptive(170.dp),
contentPadding = PaddingValues(12.dp)
) {
- items(state.selectableFiles, key = { it.first.path }) { selectableFile ->
+ items(
+ state.selectableFiles,
+ key = { it.first.path }
+ ) { selectableFile ->
BrowseFileItem(
file = selectableFile,
modifier = Modifier
- .animateItemPlacement(
- animationSpec = tween(300)
- ),
+ .animateItemPlacement(),
onClick = {
viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile))
}
@@ -317,7 +323,7 @@ fun BrowseScreen(
message = stringResource(id = R.string.browse_empty),
icon = painterResource(id = R.drawable.empty_browse),
actionTitle = stringResource(id = R.string.get_help),
- action = { navigator.navigate(Screen.HELP) }
+ action = { navigator.navigate(Screen.HELP, false) }
)
}
@@ -325,9 +331,8 @@ fun BrowseScreen(
state.isRefreshing,
refreshState,
Modifier.align(Alignment.TopCenter),
- backgroundColor = MaterialTheme.elevation(),
- contentColor = MaterialTheme.colorScheme.primary,
- scale = true
+ backgroundColor = MaterialTheme.colorScheme.inverseSurface,
+ contentColor = MaterialTheme.colorScheme.inverseOnSurface
)
}
}
@@ -343,7 +348,7 @@ fun BrowseScreen(
return@BackHandler
}
- navigator.navigate(Screen.LIBRARY)
+ navigator.navigate(Screen.LIBRARY, false)
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt
index fd538230..af035842 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt
@@ -37,6 +37,9 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
+/**
+ * Browse list element item. Can be selected.
+ */
@Composable
fun BrowseFileItem(file: Pair, modifier: Modifier, onClick: () -> Unit) {
val fileExtension: String = file.first.name.substringAfterLast(".", "")
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt
index 2dddfea9..887ac8cb 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt
@@ -10,10 +10,13 @@ import androidx.compose.ui.res.stringResource
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.components.CustomDialogWithContent
+import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
+/**
+ * Storage permission dialog.
+ */
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun BrowseStoragePermissionDialog(viewModel: BrowseViewModel, permissionState: PermissionState) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseAddingDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt
similarity index 77%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseAddingDialog.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt
index 92cef92b..6e50c3c1 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseAddingDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.browse.components
+package ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog
import android.widget.Toast
import androidx.compose.foundation.layout.Box
@@ -19,18 +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 ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.components.CustomDialogWithLazyColumn
+import ua.acclorite.book_story.domain.model.NullableBook
+import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn
+import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
+/**
+ * Adding dialog. Adds all selected books to the Library.
+ */
@Composable
fun BrowseAddingDialog(
- libraryViewModel: LibraryViewModel = hiltViewModel(),
+ libraryViewModel: LibraryViewModel,
viewModel: BrowseViewModel,
navigator: Navigator
) {
@@ -42,7 +45,10 @@ fun BrowseAddingDialog(
imageVectorIcon = Icons.Default.AddChart,
description = stringResource(id = R.string.add_books_description),
actionText = stringResource(id = R.string.add),
- isActionEnabled = !state.isBooksLoading,
+ isActionEnabled = !state.isBooksLoading &&
+ state.selectedBooks
+ .filterIsInstance()
+ .isNotEmpty(),
onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) },
onAction = {
viewModel.onEvent(
@@ -80,7 +86,15 @@ fun BrowseAddingDialog(
} else {
items(state.selectedBooks) { book ->
BrowseAddingDialogItem(result = book) {
- viewModel.onEvent(BrowseEvent.OnSelectBook(book))
+ if (it) {
+ viewModel.onEvent(BrowseEvent.OnSelectBook(book))
+ } else {
+ Toast.makeText(
+ context,
+ book.message?.asString(context),
+ Toast.LENGTH_LONG
+ ).show()
+ }
}
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseAddingDialogItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt
similarity index 81%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseAddingDialogItem.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt
index 55776d10..9bfd271c 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseAddingDialogItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.browse.components
+package ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -10,27 +10,32 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material.icons.filled.Error
+import androidx.compose.material.icons.filled.Lock
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.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.presentation.components.CustomCheckbox
+/**
+ * Adding dialog item.
+ */
@Composable
-fun BrowseAddingDialogItem(result: NullableBook, onClick: () -> Unit) {
-
+fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
if (result is NullableBook.NotNull) {
Row(
Modifier
.fillMaxWidth()
.clickable {
- onClick()
+ onClick(true)
}
.padding(vertical = 12.dp, horizontal = 24.dp),
verticalAlignment = Alignment.CenterVertically
@@ -58,14 +63,24 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: () -> Unit) {
}
}
} else {
+ val icon =
+ if (result.message?.asString() == stringResource(R.string.error_file_encrypted)) {
+ Icons.Default.Lock
+ } else {
+ Icons.Default.Error
+ }
+
Row(
Modifier
.fillMaxWidth()
+ .clickable {
+ onClick(false)
+ }
.padding(vertical = 12.dp, horizontal = 24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
- imageVector = Icons.Default.Warning,
+ imageVector = icon,
contentDescription = "Error",
modifier = Modifier.size(26.dp),
tint = MaterialTheme.colorScheme.error
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt
index c5ca39a8..9992bd75 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt
@@ -7,7 +7,7 @@ import androidx.compose.ui.focus.FocusRequester
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import ua.acclorite.book_story.domain.model.NullableBook
-import ua.acclorite.book_story.presentation.Navigator
+import ua.acclorite.book_story.presentation.data.Navigator
import java.io.File
sealed class BrowseEvent {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt
index 62db5c49..bd4e0df8 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt
@@ -22,8 +22,8 @@ import ua.acclorite.book_story.domain.use_case.FastGetBooks
import ua.acclorite.book_story.domain.use_case.GetBooksFromFiles
import ua.acclorite.book_story.domain.use_case.GetFilesFromDownloads
import ua.acclorite.book_story.domain.use_case.InsertBooks
-import ua.acclorite.book_story.presentation.Argument
-import ua.acclorite.book_story.presentation.Screen
+import ua.acclorite.book_story.presentation.data.Argument
+import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.util.Resource
import javax.inject.Inject
@@ -42,7 +42,14 @@ class BrowseViewModel @Inject constructor(
private var job: Job? = null
init {
- onEvent(BrowseEvent.OnLoadList)
+ viewModelScope.launch(Dispatchers.IO) {
+ _state.update {
+ it.copy(
+ isLoading = true
+ )
+ }
+ getFilesFromDownloads()
+ }
}
fun onEvent(event: BrowseEvent) {
@@ -268,34 +275,23 @@ class BrowseViewModel @Inject constructor(
is BrowseEvent.OnGetBooksFromFiles -> {
viewModelScope.launch(Dispatchers.IO) {
- getBooksFromFiles.execute(
+ _state.update {
+ it.copy(
+ isBooksLoading = true
+ )
+ }
+
+ val books = getBooksFromFiles.execute(
_state.value.selectableFiles
.filter { it.second }
.map { it.first }
- ).collect { result ->
- when (result) {
- is Resource.Success -> {
- if (result.data?.isEmpty() ?: return@collect) {
- return@collect
- }
- _state.update {
- it.copy(
- selectedBooks = result.data,
- isBooksLoading = false
- )
- }
- }
+ )
- is Resource.Loading -> {
- _state.update {
- it.copy(
- isBooksLoading = result.isLoading
- )
- }
- }
-
- is Resource.Error -> Unit
- }
+ _state.update {
+ it.copy(
+ selectedBooks = books,
+ isBooksLoading = false
+ )
}
}
}
@@ -323,7 +319,11 @@ class BrowseViewModel @Inject constructor(
onEvent(BrowseEvent.OnClearSelectedFiles)
onEvent(BrowseEvent.OnLoadList)
- event.navigator.navigate(Screen.LIBRARY, Argument("added_books", books))
+ event.navigator.navigate(
+ Screen.LIBRARY,
+ false,
+ Argument("added_books", books)
+ )
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt
index 7d6740bb..c8973853 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt
@@ -54,23 +54,24 @@ 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 androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.Argument
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.Screen
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.CustomSnackbar
-import ua.acclorite.book_story.presentation.components.IsEmpty
import ua.acclorite.book_story.presentation.components.MoreDropDown
+import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
+import ua.acclorite.book_story.presentation.data.Argument
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.history.components.HistoryDeleteWholeHistoryDialog
import ua.acclorite.book_story.presentation.screens.history.components.HistoryItem
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
+import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
+import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.Transitions
import ua.acclorite.book_story.ui.elevation
import java.util.UUID
@@ -81,11 +82,14 @@ import java.util.UUID
)
@Composable
fun HistoryScreen(
- viewModel: HistoryViewModel = hiltViewModel(),
+ viewModel: HistoryViewModel,
+ libraryViewModel: LibraryViewModel,
navigator: Navigator
) {
val context = LocalContext.current
val state by viewModel.state.collectAsState()
+ val books = libraryViewModel.state.collectAsState().value.books.map { it.first }
+
val refreshState = rememberPullRefreshState(
refreshing = state.isRefreshing,
onRefresh = {
@@ -118,7 +122,7 @@ fun HistoryScreen(
}
if (state.showDeleteWholeHistoryDialog) {
- HistoryDeleteWholeHistoryDialog(viewModel = viewModel)
+ HistoryDeleteWholeHistoryDialog(viewModel = viewModel, libraryViewModel = libraryViewModel)
}
Scaffold(
@@ -144,7 +148,10 @@ fun HistoryScreen(
)
},
content1Actions = {
- IconButton(onClick = { viewModel.onEvent(HistoryEvent.OnSearchShowHide) }) {
+ IconButton(
+ enabled = !state.isRefreshing,
+ onClick = { viewModel.onEvent(HistoryEvent.OnSearchShowHide) }
+ ) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = "Search history",
@@ -153,7 +160,9 @@ fun HistoryScreen(
)
}
IconButton(
- enabled = !state.isLoading && !state.isRefreshing && state.history.isNotEmpty(),
+ enabled = !state.isLoading
+ && !state.isRefreshing
+ && state.history.isNotEmpty(),
onClick = {
viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog)
}
@@ -248,7 +257,7 @@ fun HistoryScreen(
}
CategoryTitle(
- modifier = Modifier.animateItemPlacement(tween(300)),
+ modifier = Modifier.animateItemPlacement(),
title = when (groupedHistory.title) {
"today" -> stringResource(id = R.string.today)
"yesterday" -> stringResource(id = R.string.yesterday)
@@ -262,27 +271,36 @@ fun HistoryScreen(
items(
groupedHistory.history, key = { it.id ?: UUID.randomUUID() }
) {
+ val book = books.find { book -> book.id == it.bookId } ?: return@items
+
HistoryItem(
- modifier = Modifier.animateItemPlacement(tween(300)),
+ modifier = Modifier.animateItemPlacement(),
history = it,
+ book = book,
onBodyClick = {
navigator.navigate(
Screen.BOOK_INFO,
- Argument("book", it.book)
+ false,
+ Argument("book", book)
)
},
onTitleClick = {
navigator.navigate(
Screen.READER,
- Argument("book", it.book)
+ false,
+ Argument("book", book)
)
},
+ isDeleteEnabled = !state.isRefreshing,
onDeleteClick = {
viewModel.onEvent(
HistoryEvent.OnDeleteHistoryElement(
- it,
- snackbarState,
- context
+ historyToDelete = it,
+ snackbarState = snackbarState,
+ context = context,
+ refreshList = {
+ libraryViewModel.onEvent(LibraryEvent.OnLoadList)
+ }
)
)
}
@@ -318,9 +336,8 @@ fun HistoryScreen(
state.isRefreshing,
refreshState,
Modifier.align(Alignment.TopCenter),
- backgroundColor = MaterialTheme.elevation(),
- contentColor = MaterialTheme.colorScheme.primary,
- scale = true
+ backgroundColor = MaterialTheme.colorScheme.inverseSurface,
+ contentColor = MaterialTheme.colorScheme.inverseOnSurface
)
}
}
@@ -331,7 +348,7 @@ fun HistoryScreen(
return@BackHandler
}
- navigator.navigate(Screen.LIBRARY)
+ navigator.navigate(Screen.LIBRARY, false)
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt
index 0474860f..a71b7afa 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt
@@ -7,13 +7,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.components.CustomDialogWithContent
+import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
+import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
+import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
+/**
+ * Delete whole history dialog.
+ */
@Composable
fun HistoryDeleteWholeHistoryDialog(
- viewModel: HistoryViewModel
+ viewModel: HistoryViewModel,
+ libraryViewModel: LibraryViewModel
) {
val context = LocalContext.current
@@ -29,7 +35,9 @@ fun HistoryDeleteWholeHistoryDialog(
isActionEnabled = true,
onAction = {
viewModel.onEvent(
- HistoryEvent.OnDeleteWholeHistory
+ HistoryEvent.OnDeleteWholeHistory {
+ libraryViewModel.onEvent(LibraryEvent.OnLoadList)
+ }
)
Toast.makeText(
context,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt
index f1cb83f0..b890e7a7 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt
@@ -26,22 +26,29 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.ui.elevation
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
+/**
+ * History list element item.
+ */
@Composable
fun HistoryItem(
modifier: Modifier = Modifier,
history: History,
+ book: Book,
onBodyClick: () -> Unit,
onTitleClick: () -> Unit,
+ isDeleteEnabled: Boolean,
onDeleteClick: () -> Unit
) {
val date = Date(history.time)
@@ -77,10 +84,10 @@ fun HistoryItem(
tint = MaterialTheme.elevation(12.dp)
)
- if (history.book.coverImage != null) {
+ if (book.coverImage != null) {
Image(
- bitmap = history.book.coverImage.asImageBitmap(),
- contentDescription = "Cover",
+ bitmap = book.coverImage,
+ contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.medium),
@@ -96,7 +103,7 @@ fun HistoryItem(
modifier = Modifier.fillMaxHeight()
) {
Text(
- history.book.title,
+ book.title,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
@@ -109,7 +116,7 @@ fun HistoryItem(
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
- Spacer(modifier = Modifier.height(4.dp))
+ Spacer(modifier = Modifier.height(2.dp))
Text(
pattern.format(date),
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -123,6 +130,7 @@ fun HistoryItem(
}
Box(modifier = Modifier.weight(0.1f), contentAlignment = Alignment.CenterEnd) {
IconButton(
+ enabled = isDeleteEnabled,
onClick = {
onDeleteClick()
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt
index ff091295..668978bd 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt
@@ -9,10 +9,11 @@ sealed class HistoryEvent {
data object OnRefreshList : HistoryEvent()
data object OnLoadList : HistoryEvent()
data object OnShowHideDeleteWholeHistoryDialog : HistoryEvent()
- data object OnDeleteWholeHistory : HistoryEvent()
+ data class OnDeleteWholeHistory(val refreshList: () -> Unit) : HistoryEvent()
data class OnDeleteHistoryElement(
val historyToDelete: History,
val snackbarState: SnackbarHostState,
+ val refreshList: () -> Unit,
val context: Context
) : HistoryEvent()
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt
index 8217099f..055a05d4 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt
@@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.screens.history.data
-import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarResult
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -17,6 +16,7 @@ import ua.acclorite.book_story.domain.model.GroupedHistory
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.use_case.DeleteHistory
import ua.acclorite.book_story.domain.use_case.DeleteWholeHistory
+import ua.acclorite.book_story.domain.use_case.GetBooksById
import ua.acclorite.book_story.domain.use_case.GetHistory
import ua.acclorite.book_story.domain.use_case.InsertHistory
import ua.acclorite.book_story.util.Resource
@@ -30,16 +30,25 @@ class HistoryViewModel @Inject constructor(
private val insertHistory: InsertHistory,
private val getHistory: GetHistory,
private val deleteWholeHistory: DeleteWholeHistory,
- private val deleteHistory: DeleteHistory
+ private val deleteHistory: DeleteHistory,
+ private val getBooksById: GetBooksById
) : ViewModel() {
private val _state = MutableStateFlow(HistoryState())
val state = _state.asStateFlow()
private var job: Job? = null
+ private var job2: Job? = null
init {
- onEvent(HistoryEvent.OnLoadList)
+ viewModelScope.launch(Dispatchers.IO) {
+ _state.update {
+ it.copy(
+ isLoading = true
+ )
+ }
+ getHistoryFromDatabase()
+ }
}
fun onEvent(event: HistoryEvent) {
@@ -85,6 +94,7 @@ class HistoryViewModel @Inject constructor(
}
deleteWholeHistory.execute()
+ event.refreshList()
getHistoryFromDatabase("")
}
@@ -104,20 +114,27 @@ class HistoryViewModel @Inject constructor(
listOf(event.historyToDelete)
)
onEvent(HistoryEvent.OnRefreshList)
+ event.refreshList()
+ job2?.cancel()
event.snackbarState.currentSnackbarData?.dismiss()
+
+ job2 = viewModelScope.launch(Dispatchers.IO) {
+ delay(10000)
+ event.snackbarState.currentSnackbarData?.dismiss()
+ }
val snackbar = event.snackbarState.showSnackbar(
event.context.getString(R.string.history_element_deleted),
- event.context.getString(R.string.undo),
- duration = SnackbarDuration.Long
+ event.context.getString(R.string.undo)
)
when (snackbar) {
- SnackbarResult.Dismissed -> return@launch
+ SnackbarResult.Dismissed -> Unit
SnackbarResult.ActionPerformed -> {
insertHistory.execute(
listOf(event.historyToDelete)
)
+ event.refreshList()
onEvent(HistoryEvent.OnRefreshList)
}
}
@@ -197,7 +214,7 @@ class HistoryViewModel @Inject constructor(
}
fun filterMaxElementsById(elements: List): List {
- val groupedById = elements.groupBy { it.book.id }
+ val groupedById = elements.groupBy { it.bookId }
val maxElementsById = groupedById.map { (_, values) ->
values.maxByOrNull { it.time }
}
@@ -223,11 +240,15 @@ class HistoryViewModel @Inject constructor(
return@collect
}
+ val books = getBooksById.execute(
+ history.map { it.bookId }.distinct()
+ )
val groupedHistory = mutableListOf()
history
.filter {
- it.book.title.lowercase().trim().contains(query.lowercase().trim())
+ val book = books.find { book -> book.id == it.bookId }!!
+ book.title.lowercase().trim().contains(query.lowercase().trim())
}.groupBy { item ->
val calendar = Calendar.getInstance().apply {
timeInMillis = item.time
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt
index 3f786611..fa843c82 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt
@@ -24,7 +24,6 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.CircularProgressIndicator
@@ -69,24 +68,23 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
-import ua.acclorite.book_story.presentation.Argument
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.Screen
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
-import ua.acclorite.book_story.presentation.components.IsEmpty
import ua.acclorite.book_story.presentation.components.MoreDropDown
+import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
+import ua.acclorite.book_story.presentation.data.Argument
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.components.LibraryBookItem
-import ua.acclorite.book_story.presentation.screens.library.components.LibraryDeleteDialog
-import ua.acclorite.book_story.presentation.screens.library.components.LibraryMoveDialog
import ua.acclorite.book_story.presentation.screens.library.components.LibraryTabRow
+import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryDeleteDialog
+import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryMoveDialog
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.Transitions
@@ -99,15 +97,17 @@ import java.util.UUID
)
@Composable
fun LibraryScreen(
- viewModel: LibraryViewModel = hiltViewModel(),
- browseViewModel: BrowseViewModel = hiltViewModel(),
- historyViewModel: HistoryViewModel = hiltViewModel(),
+ viewModel: LibraryViewModel,
+ browseViewModel: BrowseViewModel,
+ historyViewModel: HistoryViewModel,
navigator: Navigator,
addedBooks: List
) {
val state by viewModel.state.collectAsState()
- val pagerState =
- rememberPagerState(initialPage = state.currentPage, pageCount = { Category.entries.size })
+ val pagerState = rememberPagerState(
+ initialPage = state.currentPage,
+ pageCount = { Category.entries.size }
+ )
val refreshState = rememberPullRefreshState(
refreshing = state.isRefreshing,
onRefresh = {
@@ -165,16 +165,15 @@ fun LibraryScreen(
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
- Spacer(modifier = Modifier.width(4.dp))
+ Spacer(modifier = Modifier.width(6.dp))
Text(
text = state.books.size.toString(),
modifier = Modifier
- .clip(RoundedCornerShape(12.dp))
+ .clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation(elevation = 6.dp))
- .padding(horizontal = 10.dp, vertical = 4.dp),
+ .padding(horizontal = 8.dp, vertical = 3.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant,
- fontSize = 16.sp,
- style = MaterialTheme.typography.titleLarge
+ fontSize = 16.sp
)
}
},
@@ -313,9 +312,11 @@ fun LibraryScreen(
LaunchedEffect(state.books) {
categorizedBooks.clear()
categorizedBooks.addAll(state.books.filter { it.first.category == category }
- .sortedByDescending {
- it.first.lastOpened
- })
+ .sortedWith(
+ compareByDescending> { it.first.lastOpened }
+ .thenBy { it.first.title }
+ )
+ )
categoryIsLoading = false
}
@@ -333,15 +334,14 @@ fun LibraryScreen(
) {
LibraryBookItem(
book = it,
- modifier = Modifier.animateItemPlacement(
- animationSpec = tween(300)
- ),
+ modifier = Modifier.animateItemPlacement(),
onCoverImageClick = {
if (state.hasSelectedItems) {
viewModel.onEvent(LibraryEvent.OnSelectBook(it))
} else {
navigator.navigate(
Screen.BOOK_INFO,
+ false,
Argument("book", it.first)
)
}
@@ -354,6 +354,7 @@ fun LibraryScreen(
onButtonClick = {
navigator.navigate(
Screen.READER,
+ false,
Argument("book", it.first)
)
}
@@ -385,7 +386,7 @@ fun LibraryScreen(
modifier = Modifier.align(Alignment.Center),
actionTitle = stringResource(id = R.string.add_book)
) {
- navigator.navigate(Screen.BROWSE)
+ navigator.navigate(Screen.BROWSE, false)
}
}
@@ -393,9 +394,8 @@ fun LibraryScreen(
state.isRefreshing,
refreshState,
Modifier.align(Alignment.TopCenter),
- backgroundColor = MaterialTheme.elevation(),
- contentColor = MaterialTheme.colorScheme.primary,
- scale = true
+ backgroundColor = MaterialTheme.colorScheme.inverseSurface,
+ contentColor = MaterialTheme.colorScheme.inverseOnSurface
)
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt
index f0d168c5..a097aafc 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt
@@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Image
import androidx.compose.material.icons.filled.PlayArrow
-import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
@@ -30,7 +29,6 @@ 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.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -40,6 +38,9 @@ import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero
import ua.acclorite.book_story.ui.elevation
+/**
+ * Library list element item.
+ */
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LibraryBookItem(
@@ -102,7 +103,7 @@ fun LibraryBookItem(
if (book.first.coverImage != null) {
Image(
- bitmap = book.first.coverImage!!.asImageBitmap(),
+ bitmap = book.first.coverImage!!,
contentDescription = "Cover",
modifier = Modifier
.fillMaxSize()
@@ -111,40 +112,25 @@ fun LibraryBookItem(
)
}
- if (book.first.file != null) {
- Text(
- "${
- (book.first.progress * 100)
- .toDouble()
- .removeDigits(1)
- .removeTrailingZero()
- }%",
- textAlign = TextAlign.Center,
- color = MaterialTheme.colorScheme.onPrimary,
- modifier = Modifier
- .padding(6.dp)
- .clip(MaterialTheme.shapes.small)
- .background(MaterialTheme.colorScheme.primary)
- .padding(horizontal = 8.dp, vertical = 4.dp),
- style = MaterialTheme.typography.bodySmall
- )
- } else {
- Icon(
- imageVector = Icons.Default.Warning,
- contentDescription = "File is not found",
- modifier = Modifier
- .padding(6.dp)
- .clip(MaterialTheme.shapes.small)
- .background(MaterialTheme.colorScheme.error)
- .padding(4.dp)
- .size(20.dp),
- tint = MaterialTheme.colorScheme.onError
- )
- }
+ Text(
+ "${
+ (book.first.progress * 100)
+ .toDouble()
+ .removeDigits(1)
+ .removeTrailingZero()
+ }%",
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onPrimary,
+ modifier = Modifier
+ .padding(6.dp)
+ .clip(MaterialTheme.shapes.small)
+ .background(MaterialTheme.colorScheme.primary)
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ style = MaterialTheme.typography.bodySmall
+ )
FilledIconButton(
onClick = { onButtonClick() },
- enabled = book.first.file != null,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(6.dp)
@@ -154,11 +140,7 @@ fun LibraryBookItem(
containerColor = MaterialTheme.colorScheme.primaryContainer
.copy(0.9f),
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
- .copy(0.9f),
- disabledContainerColor = MaterialTheme.colorScheme.primaryContainer
- .copy(0.6f),
- disabledContentColor = MaterialTheme.colorScheme.onPrimaryContainer
- .copy(0.6f)
+ .copy(0.9f)
)
) {
Icon(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt
index 664ce0ee..32cda42e 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt
@@ -31,6 +31,9 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.ui.elevation
+/**
+ * Tab row, either scrollable or static, depends on screen width.
+ */
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LibraryTabRow(viewModel: LibraryViewModel, books: List, pagerState: PagerState) {
@@ -97,9 +100,9 @@ fun LibraryTabRow(viewModel: LibraryViewModel, books: List, pagerState: Pa
Text(
text = tabItem.second.toString(),
modifier = Modifier
- .clip(RoundedCornerShape(12.dp))
+ .clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation(6.dp))
- .padding(horizontal = 6.dp, vertical = 4.dp),
+ .padding(horizontal = 8.dp, vertical = 4.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium
)
@@ -153,9 +156,9 @@ fun LibraryTabRow(viewModel: LibraryViewModel, books: List, pagerState: Pa
Text(
text = tabItem.second.toString(),
modifier = Modifier
- .clip(RoundedCornerShape(12.dp))
+ .clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation(6.dp))
- .padding(horizontal = 6.dp, vertical = 4.dp),
+ .padding(horizontal = 8.dp, vertical = 4.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium
)
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryDeleteDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt
similarity index 92%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryDeleteDialog.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt
index 1ea9df85..5b1117df 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryDeleteDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.library.components
+package ua.acclorite.book_story.presentation.screens.library.components.dialog
import android.widget.Toast
import androidx.compose.material.icons.Icons
@@ -9,7 +9,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.components.CustomDialogWithContent
+import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
@@ -17,6 +17,9 @@ import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewMode
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
+/**
+ * Delete dialog. Deletes all selected books.
+ */
@Composable
fun LibraryDeleteDialog(
viewModel: LibraryViewModel,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt
similarity index 90%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryMoveDialog.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt
index 86e17724..a904a4cb 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryMoveDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.library.components
+package ua.acclorite.book_story.presentation.screens.library.components.dialog
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -13,11 +13,14 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Category
-import ua.acclorite.book_story.presentation.components.CustomDialogWithLazyColumn
-import ua.acclorite.book_story.presentation.components.SelectableDialogItem
+import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn
+import ua.acclorite.book_story.presentation.components.custom_dialog.SelectableDialogItem
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
+/**
+ * Move dialog. Moves all selected books to the selected category.
+ */
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt
index b25724b2..4f0c27a4 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt
@@ -29,10 +29,23 @@ class LibraryViewModel @Inject constructor(
private val _state = MutableStateFlow(LibraryState())
val state = _state.asStateFlow()
+ private val _isReady = MutableStateFlow(false)
+ val isReady = _isReady.asStateFlow()
+
private var job: Job? = null
init {
- onEvent(LibraryEvent.OnLoadList)
+ viewModelScope.launch(Dispatchers.IO) {
+ _state.update {
+ it.copy(
+ isLoading = true
+ )
+ }
+ getBooksFromDatabase()
+ _isReady.update {
+ true
+ }
+ }
}
fun onEvent(event: LibraryEvent) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt
index 2c8d9d18..5459a8f9 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt
@@ -1,6 +1,8 @@
package ua.acclorite.book_story.presentation.screens.reader
import android.annotation.SuppressLint
+import android.os.Build
+import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
@@ -20,7 +22,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.selection.DisableSelection
-import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -50,25 +51,25 @@ import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.hilt.navigation.compose.hiltViewModel
import dagger.hilt.android.lifecycle.withCreationCallback
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.components.IsError
+import ua.acclorite.book_story.presentation.components.CustomSelectionContainer
+import ua.acclorite.book_story.presentation.components.is_messages.IsError
import ua.acclorite.book_story.presentation.data.MainViewModel
+import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
-import ua.acclorite.book_story.presentation.screens.reader.components.ReaderBottomBar
import ua.acclorite.book_story.presentation.screens.reader.components.ReaderEndItem
-import ua.acclorite.book_story.presentation.screens.reader.components.ReaderSettingsBottomSheet
import ua.acclorite.book_story.presentation.screens.reader.components.ReaderStartItem
-import ua.acclorite.book_story.presentation.screens.reader.components.ReaderTopBar
+import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderBottomBar
+import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderTopBar
+import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
import ua.acclorite.book_story.ui.elevation
@@ -78,9 +79,9 @@ import ua.acclorite.book_story.util.Constants
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun ReaderScreen(
- mainViewModel: MainViewModel = hiltViewModel(),
- libraryViewModel: LibraryViewModel = hiltViewModel(),
- historyViewModel: HistoryViewModel = hiltViewModel(),
+ mainViewModel: MainViewModel,
+ libraryViewModel: LibraryViewModel,
+ historyViewModel: HistoryViewModel,
navigator: Navigator
) {
val context = LocalContext.current as ComponentActivity
@@ -189,7 +190,7 @@ fun ReaderScreen(
Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
- containerColor = Color(backgroundColor.toULong()),
+ containerColor = MaterialTheme.colorScheme.surface,
topBar = {
AnimatedVisibility(
visible = state.showMenu,
@@ -219,19 +220,66 @@ fun ReaderScreen(
}
}
) {
- SelectionContainer {
+ CustomSelectionContainer(
+ onCopyRequested = {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
+ Toast.makeText(
+ context,
+ context.getString(R.string.copied),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ },
+ onTranslateRequested = { textToTranslate ->
+ viewModel.onEvent(
+ ReaderEvent.OnTranslateText(
+ textToTranslate,
+ context,
+ noAppsFound = {
+ Toast.makeText(
+ context,
+ context.getString(R.string.error_no_translator),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ )
+ )
+ },
+ onDictionaryRequested = { textToDefine ->
+ viewModel.onEvent(
+ ReaderEvent.OnOpenDictionary(
+ textToDefine,
+ context,
+ noAppsFound = {
+ Toast.makeText(
+ context,
+ context.getString(R.string.error_no_browser),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ )
+ )
+ }
+ ) { toolbarShowed ->
LazyColumn(
state = listState,
modifier = Modifier
- .fillMaxSize()
- .clickable(
- interactionSource = null,
- indication = null,
- enabled = !loading,
- onClick = {
- viewModel.onEvent(
- ReaderEvent.OnShowHideMenu(context = context)
- )
+ .then(
+ if (!loading && toolbarShowed) {
+ Modifier
+ .fillMaxSize()
+ .clickable(
+ interactionSource = null,
+ indication = null,
+ onClick = {
+ viewModel.onEvent(
+ ReaderEvent.OnShowHideMenu(context = context)
+ )
+ }
+ )
+ } else {
+ Modifier
+ .fillMaxSize()
}
)
) {
@@ -246,28 +294,31 @@ fun ReaderScreen(
itemsIndexed(
state.book.text, key = { _, key -> key.id }
) { index, text ->
- if (index == 0)
- Spacer(modifier = Modifier.height(36.dp))
+ Column(
+ Modifier
+ .background(Color(backgroundColor.toULong()))
+ .fillMaxWidth()
+ .padding(
+ top = if (index == 0) 36.dp else 0.dp,
+ start = 18.dp,
+ end = 18.dp,
+ bottom = if (index == state.book.text.lastIndex) 36.dp
+ else (paragraphHeight * 3).dp
+ )
+ ) {
+ Text(
+ text = "${if (paragraphIndentation) " " else ""}${text.line}",
+ color = Color(fontColor.toULong()),
- Text(
- text = "${if (paragraphIndentation) " " else ""}${text.line}",
- color = Color(fontColor.toULong()),
-
- style = TextStyle(
- lineBreak = LineBreak.Paragraph
- ),
- fontFamily = fontFamily.font,
- fontStyle = fontStyle,
- fontSize = fontSize.sp,
- lineHeight = (fontSize + lineHeight).sp,
-
- modifier = Modifier.padding(horizontal = 18.dp)
- )
-
- if (index == state.book.text.lastIndex)
- Spacer(modifier = Modifier.height(36.dp))
- else
- Spacer(modifier = Modifier.height((paragraphHeight * 3).dp))
+ style = TextStyle(
+ lineBreak = LineBreak.Paragraph
+ ),
+ fontFamily = fontFamily.font,
+ fontStyle = fontStyle,
+ fontSize = fontSize.sp,
+ lineHeight = (fontSize + lineHeight).sp
+ )
+ }
}
if (state.book.text.isNotEmpty()) {
@@ -300,6 +351,9 @@ fun ReaderScreen(
actionTitle = stringResource(id = R.string.go_back),
action = {
navigator.navigateBack()
+ libraryViewModel.onEvent(LibraryEvent.OnLoadList)
+ historyViewModel.onEvent(HistoryEvent.OnLoadList)
+ viewModel.onEvent(ReaderEvent.OnShowSystemBars(context))
}
)
} else {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt
index 47d27f7d..cc1b5c15 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt
@@ -4,11 +4,15 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -26,13 +30,17 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Category
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.Screen
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
+/**
+ * Reader end item. Displays at the end of the book.
+ */
+@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ReaderEndItem(
libraryViewModel: LibraryViewModel,
@@ -46,7 +54,13 @@ fun ReaderEndItem(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
- .padding(24.dp),
+ .windowInsetsPadding(WindowInsets.navigationBarsIgnoringVisibility)
+ .padding(
+ top = 24.dp,
+ start = 24.dp,
+ end = 24.dp,
+ bottom = 8.dp
+ ),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
@@ -56,7 +70,14 @@ fun ReaderEndItem(
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis
)
- Spacer(modifier = Modifier.height(16.dp))
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ stringResource(id = R.string.letters_and_words, state.letters, state.words),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodyLarge,
+ textAlign = TextAlign.Center
+ )
+ Spacer(modifier = Modifier.height(12.dp))
TextButton(
onClick = {
if (state.book.category != Category.ALREADY_READ) {
@@ -86,7 +107,7 @@ fun ReaderEndItem(
context
)
)
- navigator.navigate(Screen.LIBRARY)
+ navigator.navigate(Screen.LIBRARY, true)
}
},
contentPadding = PaddingValues(horizontal = 12.dp),
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt
index b397dafa..d209e611 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt
@@ -5,14 +5,18 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
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.padding
+import androidx.compose.foundation.layout.statusBarsIgnoringVisibility
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Image
import androidx.compose.material3.Icon
@@ -24,7 +28,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@@ -33,6 +36,10 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
import ua.acclorite.book_story.ui.elevation
+/**
+ * Reader start item. Displays at the beginning of the book.
+ */
+@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ReaderStartItem(viewModel: ReaderViewModel) {
val state by viewModel.state.collectAsState()
@@ -41,6 +48,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
+ .windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility)
.padding(24.dp)
) {
Row(
@@ -66,7 +74,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
if (state.book.coverImage != null) {
Image(
- bitmap = state.book.coverImage!!.asImageBitmap(),
+ bitmap = state.book.coverImage!!,
contentDescription = "Cover",
modifier = Modifier
.fillMaxSize()
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderBottomBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt
similarity index 96%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderBottomBar.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt
index 6ae3075a..ea21c461 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderBottomBar.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.reader.components
+package ua.acclorite.book_story.presentation.screens.reader.components.app_bar
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -19,7 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
-import ua.acclorite.book_story.presentation.Navigator
+import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
@@ -27,6 +27,9 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
+/**
+ * Reader bottom bar. Has a slider to change progress.
+ */
@Composable
fun ReaderBottomBar(
viewModel: ReaderViewModel,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt
similarity index 82%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderTopBar.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt
index 37a88904..678b341a 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderTopBar.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.reader.components
+package ua.acclorite.book_story.presentation.screens.reader.components.app_bar
import androidx.activity.ComponentActivity
import androidx.compose.foundation.basicMarquee
@@ -6,10 +6,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@@ -28,17 +25,20 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.Argument
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.Screen
+import ua.acclorite.book_story.presentation.components.GoBackButton
+import ua.acclorite.book_story.presentation.data.Argument
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
+/**
+ * Reader top bar. Displays title of the book.
+ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColor: Color) {
@@ -48,15 +48,8 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
TopAppBar(
navigationIcon = {
- IconButton(onClick = {
- navigator.navigateBack()
+ GoBackButton(navigator = navigator) {
viewModel.onEvent(ReaderEvent.OnShowSystemBars(context))
- }) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Go back",
- tint = MaterialTheme.colorScheme.onSurface
- )
}
},
title = {
@@ -75,6 +68,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
onClick = {
navigator.navigateWithoutBackStack(
Screen.BOOK_INFO,
+ true,
Argument(
"book",
book
@@ -86,7 +80,6 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
delayMillis = 4000
)
)
- Spacer(modifier = Modifier.height(4.dp))
Text(
stringResource(
id = R.string.read_query,
@@ -107,8 +100,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
}) {
Icon(
imageVector = Icons.Default.Settings,
- contentDescription = "Open Settings",
- tint = MaterialTheme.colorScheme.onSurface
+ contentDescription = stringResource(id = R.string.open_reader_settings_content_desc),
)
}
},
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderSettingsBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt
similarity index 98%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderSettingsBottomSheet.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt
index 0e1c507b..fe09675c 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderSettingsBottomSheet.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.reader.components
+package ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet
import androidx.activity.ComponentActivity
import androidx.compose.animation.animateColorAsState
@@ -48,6 +48,9 @@ import ua.acclorite.book_story.ui.ElevationDefaults
import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.util.Constants
+/**
+ * Settings bottom sheet. Has General and Colors categories.
+ */
@OptIn(
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class
)
@@ -118,7 +121,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
}
item {
CategoryTitle(
- title = stringResource(id = R.string.font_settings),
+ title = stringResource(id = R.string.font_reader_settings),
color = MaterialTheme.colorScheme.onSurface
)
}
@@ -197,7 +200,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
}
item {
CategoryTitle(
- title = stringResource(id = R.string.text_settings),
+ title = stringResource(id = R.string.text_reader_settings),
color = MaterialTheme.colorScheme.onSurface
)
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderSettingsBottomSheetTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt
similarity index 96%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderSettingsBottomSheetTabRow.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt
index 28d787c4..71818fc6 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderSettingsBottomSheetTabRow.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.reader.components
+package ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.fillMaxWidth
@@ -21,6 +21,9 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
+/**
+ * Settings bottom sheet tab row. It is used to switch between categories.
+ */
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: PagerState) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt
index f9294234..92bcc2e9 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt
@@ -7,10 +7,10 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.pager.PagerState
import ua.acclorite.book_story.domain.model.Book
-import ua.acclorite.book_story.presentation.Navigator
+import ua.acclorite.book_story.presentation.data.Navigator
sealed class ReaderEvent {
- data class OnFileNotFound(val onLoaded: () -> Unit) : ReaderEvent()
+ data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent()
data class OnLoadText(
val scrollState: LazyListState,
val navigator: Navigator,
@@ -37,4 +37,16 @@ sealed class ReaderEvent {
val updatePage: (Int) -> Unit,
val navigator: Navigator
) : ReaderEvent()
+
+ data class OnTranslateText(
+ val textToTranslate: String,
+ val context: ComponentActivity,
+ val noAppsFound: () -> Unit
+ ) : ReaderEvent()
+
+ data class OnOpenDictionary(
+ val textToDefine: String,
+ val context: ComponentActivity,
+ val noAppsFound: () -> Unit
+ ) : ReaderEvent()
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt
index 056f6758..5bca3077 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt
@@ -7,6 +7,9 @@ import ua.acclorite.book_story.util.UIText
@Immutable
data class ReaderState(
val book: Book,
+ val letters: Int = 0,
+ val words: Int = 0,
+
val errorMessage: UIText? = null,
val showMenu: Boolean = false,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt
index 1184f683..c6d59ae0 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt
@@ -1,5 +1,7 @@
package ua.acclorite.book_story.presentation.screens.reader.data
+import android.app.SearchManager
+import android.content.Intent
import androidx.activity.ComponentActivity
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState
@@ -24,13 +26,11 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.History
-import ua.acclorite.book_story.domain.use_case.GetText
import ua.acclorite.book_story.domain.use_case.InsertHistory
import ua.acclorite.book_story.domain.use_case.UpdateBooks
-import ua.acclorite.book_story.presentation.Argument
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.Screen
-import ua.acclorite.book_story.util.Resource
+import ua.acclorite.book_story.presentation.data.Argument
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.util.UIText
import java.util.Date
import kotlin.math.roundToInt
@@ -39,7 +39,6 @@ import kotlin.math.roundToInt
@HiltViewModel(assistedFactory = ReaderViewModel.Factory::class)
class ReaderViewModel @AssistedInject constructor(
@Assisted book: Book,
- private val getText: GetText,
private val updateBooks: UpdateBooks,
private val insertHistory: InsertHistory
) : ViewModel() {
@@ -49,61 +48,29 @@ class ReaderViewModel @AssistedInject constructor(
fun onEvent(event: ReaderEvent) {
when (event) {
- is ReaderEvent.OnFileNotFound -> {
+ is ReaderEvent.OnTextIsEmpty -> {
_state.update {
event.onLoaded()
it.copy(
- errorMessage = UIText.StringResource(R.string.error_file_not_found),
+ errorMessage = UIText.StringResource(R.string.error_no_text),
)
}
}
is ReaderEvent.OnLoadText -> {
viewModelScope.launch(Dispatchers.IO) {
- if (_state.value.book.file == null) {
- return@launch
- }
-
- if (_state.value.book.text.isEmpty()) {
- getText.execute(_state.value.book.file!!).collect { result ->
- when (result) {
- is Resource.Loading -> Unit
- is Resource.Success -> {
- if (result.data?.isEmpty() != false) {
- _state.update {
- it.copy(
- errorMessage = UIText.StringResource(
- R.string.error_something_went_wrong
- ),
- )
- }
- event.onLoaded()
- return@collect
- }
-
- _state.update {
- it.copy(
- book = it.book.copy(
- text = result.data,
- )
- )
- }
-
-
- }
-
- is Resource.Error -> Unit
- }
- }
- }
-
val time = Date().time
+ val text = _state.value.book.text.joinToString(separator = " ") { it.line }
+ val letters = text.replace(" ", "").length
+ val words = text.split("\\s+".toRegex()).size
_state.update {
it.copy(
book = it.book.copy(
lastOpened = time
- )
+ ),
+ letters = letters,
+ words = words
)
}
@@ -114,7 +81,7 @@ class ReaderViewModel @AssistedInject constructor(
listOf(
History(
null,
- _state.value.book,
+ _state.value.book.id!!,
time
)
)
@@ -141,7 +108,7 @@ class ReaderViewModel @AssistedInject constructor(
}
}
- delay(500)
+ delay(100)
event.onLoaded()
return@collectLatest
}
@@ -234,7 +201,8 @@ class ReaderViewModel @AssistedInject constructor(
onEvent(ReaderEvent.OnShowSystemBars(event.context))
val book = _state.value.book.copy(
- category = Category.ALREADY_READ
+ category = Category.ALREADY_READ,
+ progress = 1f
)
updateBooks.execute(listOf(book))
@@ -245,7 +213,59 @@ class ReaderViewModel @AssistedInject constructor(
it != Category.ALREADY_READ
}.size - 1
)
- event.navigator.navigate(Screen.LIBRARY)
+ event.navigator.navigate(Screen.LIBRARY, true)
+ }
+ }
+
+ is ReaderEvent.OnTranslateText -> {
+ viewModelScope.launch {
+ val translatorIntent = Intent()
+ val browserIntent = Intent()
+
+ translatorIntent.type = "text/plain"
+ translatorIntent.action = Intent.ACTION_PROCESS_TEXT
+
+ browserIntent.action = Intent.ACTION_WEB_SEARCH
+
+ translatorIntent.putExtra(Intent.EXTRA_PROCESS_TEXT, event.textToTranslate)
+ translatorIntent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
+
+ browserIntent.putExtra(
+ SearchManager.QUERY,
+ "translate: ${event.textToTranslate.trim()}"
+ )
+
+ if (translatorIntent.resolveActivity(event.context.packageManager) != null) {
+ event.context.startActivity(translatorIntent)
+ return@launch
+ }
+
+ if (browserIntent.resolveActivity(event.context.packageManager) != null) {
+ event.context.startActivity(browserIntent)
+ return@launch
+ }
+
+ event.noAppsFound()
+ }
+ }
+
+ is ReaderEvent.OnOpenDictionary -> {
+ viewModelScope.launch {
+ val browserIntent = Intent()
+
+ browserIntent.action = Intent.ACTION_WEB_SEARCH
+ browserIntent.putExtra(
+ SearchManager.QUERY,
+ "dictionary" +
+ ": ${event.textToDefine.trim()}"
+ )
+
+ if (browserIntent.resolveActivity(event.context.packageManager) != null) {
+ event.context.startActivity(browserIntent)
+ return@launch
+ }
+
+ event.noAppsFound()
}
}
}
@@ -270,8 +290,8 @@ class ReaderViewModel @AssistedInject constructor(
ReaderState(book = book)
}
- if (book.file == null) {
- onEvent(ReaderEvent.OnFileNotFound(onLoaded = { onLoaded() }))
+ if (book.text.isEmpty()) {
+ onEvent(ReaderEvent.OnTextIsEmpty(onLoaded = { onLoaded() }))
} else {
onEvent(ReaderEvent.OnShowHideMenu(false, context))
onEvent(
@@ -304,3 +324,5 @@ class ReaderViewModel @AssistedInject constructor(
+
+
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt
index 77985539..c2f2e0bd 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt
@@ -6,13 +6,13 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material.icons.filled.DisplaySettings
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@@ -21,19 +21,24 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.Screen
-import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
+import ua.acclorite.book_story.presentation.components.GoBackButton
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem
+import ua.acclorite.book_story.ui.elevation
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
navigator: Navigator
) {
- val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+ val listState = rememberLazyListState()
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
+ canScroll = {
+ listState.canScrollForward
+ }
+ )
Scaffold(
Modifier
@@ -42,36 +47,26 @@ fun SettingsScreen(
.windowInsetsPadding(WindowInsets.navigationBars),
containerColor = MaterialTheme.colorScheme.surface,
topBar = {
- AnimatedTopAppBar(
+ LargeTopAppBar(
+ title = {
+ Text(stringResource(id = R.string.settings_screen))
+ },
+ navigationIcon = {
+ GoBackButton(navigator = navigator)
+ },
scrollBehavior = scrollBehavior,
- isTopBarScrolled = null,
-
- content1NavigationIcon = {
- IconButton(onClick = { navigator.navigateBack() }) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Go back",
- tint = MaterialTheme.colorScheme.onSurface
- )
- }
- },
- content1Title = {
- Text(
- stringResource(id = R.string.settings_screen),
- style = MaterialTheme.typography.titleLarge,
- color = MaterialTheme.colorScheme.onSurface,
- overflow = TextOverflow.Ellipsis,
- maxLines = 1
- )
- },
- content1Actions = {}
+ colors = TopAppBarDefaults.largeTopAppBarColors(
+ containerColor = MaterialTheme.colorScheme.surface,
+ scrolledContainerColor = MaterialTheme.elevation()
+ )
)
}
) { paddingValues ->
LazyColumn(
Modifier
.fillMaxSize()
- .padding(top = paddingValues.calculateTopPadding())
+ .padding(top = paddingValues.calculateTopPadding()),
+ state = listState
) {
item {
SettingsCategoryItem(
@@ -79,7 +74,7 @@ fun SettingsScreen(
text = stringResource(id = R.string.general_settings),
description = stringResource(id = R.string.general_settings_desc)
) {
- navigator.navigate(Screen.GENERAL_SETTINGS)
+ navigator.navigate(Screen.GENERAL_SETTINGS, false)
}
}
@@ -89,11 +84,19 @@ fun SettingsScreen(
text = stringResource(id = R.string.appearance_settings),
description = stringResource(id = R.string.appearance_settings_desc)
) {
- navigator.navigate(Screen.APPEARANCE_SETTINGS)
+ navigator.navigate(Screen.APPEARANCE_SETTINGS, false)
}
}
- //todo Reader settings
+ item {
+ SettingsCategoryItem(
+ icon = Icons.AutoMirrored.Filled.MenuBook,
+ text = stringResource(id = R.string.reader_settings),
+ description = stringResource(id = R.string.reader_settings_desc)
+ ) {
+ navigator.navigate(Screen.READER_SETTINGS, false)
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/CheckboxWIthTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/CheckboxWIthTitle.kt
index e7fbcf54..afa52624 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/CheckboxWIthTitle.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/CheckboxWIthTitle.kt
@@ -14,6 +14,9 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.presentation.components.CategoryTitle
+/**
+ * Checkbox with title.
+ */
@Composable
fun CheckboxWithTitle(
selected: Boolean,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ChipsWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ChipsWithTitle.kt
index 8d8fa4d0..76516e72 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ChipsWithTitle.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ChipsWithTitle.kt
@@ -22,6 +22,9 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.domain.model.ChipItem
import ua.acclorite.book_story.presentation.components.CategoryTitle
+/**
+ * Chips with title. Use list of [ChipItem]s to display chips.
+ */
@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
@Composable
fun ChipsWithTitle(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt
index 3396ee86..2906f581 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt
@@ -28,6 +28,9 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CategoryTitle
+/**
+ * Color picker with title.
+ */
@Composable
fun ColorPickerWithTitle(
value: Color,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt
index 3babc5c4..6742a3d1 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt
@@ -17,10 +17,14 @@ 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.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+/**
+ * Settings category item. It is used to represent the whole category.
+ */
@Composable
fun SettingsCategoryItem(
icon: ImageVector,
@@ -53,13 +57,17 @@ fun SettingsCategoryItem(
text,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
- fontSize = 18.sp
+ fontSize = 18.sp,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(2.dp))
Text(
description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
)
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SliderWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SliderWithTitle.kt
index c3c048ec..63515b35 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SliderWithTitle.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SliderWithTitle.kt
@@ -18,6 +18,9 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.presentation.components.CategoryTitle
import kotlin.math.roundToInt
+/**
+ * Slider with title.
+ */
@Composable
fun SliderWithTitle(
modifier: Modifier = Modifier,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt
index 5405f01a..cff4c1bb 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt
@@ -1,16 +1,16 @@
package ua.acclorite.book_story.presentation.screens.settings.nested.appearance
+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.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@@ -18,29 +18,39 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ChipItem
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
+import ua.acclorite.book_story.presentation.components.CategoryTitle
+import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel
+import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
-import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.AppearanceSettingsThemeSwitcher
+import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle
+import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher.AppearanceSettingsThemeSwitcher
import ua.acclorite.book_story.ui.DarkTheme
+import ua.acclorite.book_story.ui.elevation
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppearanceSettings(
- mainViewModel: MainViewModel = hiltViewModel(),
+ mainViewModel: MainViewModel,
navigator: Navigator
) {
- val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+ val listState = rememberLazyListState()
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
+ canScroll = {
+ listState.canScrollForward
+ }
+ )
val darkTheme = mainViewModel.darkTheme.collectAsState().value!!
+ val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!!
+ val fontColor = mainViewModel.fontColor.collectAsState().value!!
Scaffold(
Modifier
@@ -49,37 +59,40 @@ fun AppearanceSettings(
.windowInsetsPadding(WindowInsets.navigationBars),
containerColor = MaterialTheme.colorScheme.surface,
topBar = {
- AnimatedTopAppBar(
+ LargeTopAppBar(
+ title = {
+ Text(stringResource(id = R.string.appearance_settings))
+ },
+ navigationIcon = {
+ GoBackButton(navigator = navigator)
+ },
scrollBehavior = scrollBehavior,
- isTopBarScrolled = null,
-
- content1NavigationIcon = {
- IconButton(onClick = { navigator.navigateBack() }) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Go back",
- tint = MaterialTheme.colorScheme.onSurface
- )
- }
- },
- content1Title = {
- Text(
- stringResource(id = R.string.appearance_settings),
- style = MaterialTheme.typography.titleLarge,
- color = MaterialTheme.colorScheme.onSurface,
- overflow = TextOverflow.Ellipsis,
- maxLines = 1
- )
- },
- content1Actions = {}
+ colors = TopAppBarDefaults.largeTopAppBarColors(
+ containerColor = MaterialTheme.colorScheme.surface,
+ scrolledContainerColor = MaterialTheme.elevation()
+ )
)
}
) { paddingValues ->
LazyColumn(
Modifier
.fillMaxSize()
- .padding(top = paddingValues.calculateTopPadding())
+ .padding(top = paddingValues.calculateTopPadding()),
+ state = listState
) {
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ item {
+ CategoryTitle(
+ title = stringResource(id = R.string.theme_appearance_settings),
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+
item {
ChipsWithTitle(
title = stringResource(id = R.string.dark_theme_option),
@@ -107,6 +120,46 @@ fun AppearanceSettings(
item {
AppearanceSettingsThemeSwitcher(mainViewModel = mainViewModel)
}
+
+ item {
+ Spacer(modifier = Modifier.height(22.dp))
+ }
+ item {
+ CategoryTitle(
+ title = stringResource(id = R.string.colors_appearance_settings),
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+
+ item {
+ ColorPickerWithTitle(
+ value = Color(backgroundColor.toULong()),
+ title = stringResource(id = R.string.background_color_option),
+ onValueChange = {
+ mainViewModel.onEvent(
+ MainEvent.OnChangeBackgroundColor(
+ it.value.toLong()
+ )
+ )
+ }
+ )
+ }
+ item {
+ ColorPickerWithTitle(
+ value = Color(fontColor.toULong()),
+ title = stringResource(id = R.string.font_color_option),
+ onValueChange = {
+ mainViewModel.onEvent(
+ MainEvent.OnChangeFontColor(
+ it.value.toLong()
+ )
+ )
+ }
+ )
+ }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcher.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt
similarity index 97%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcher.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt
index d543800d..0a73e1b6 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcher.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components
+package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher
import android.os.Build
import androidx.compose.foundation.layout.Column
@@ -21,6 +21,9 @@ import ua.acclorite.book_story.ui.Theme
import ua.acclorite.book_story.ui.isDark
import ua.acclorite.book_story.util.Constants
+/**
+ * Theme switcher.
+ */
@Composable
fun AppearanceSettingsThemeSwitcher(
mainViewModel: MainViewModel
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcherItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt
similarity index 98%
rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcherItem.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt
index df5e3b9b..e4bdd814 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcherItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt
@@ -1,4 +1,4 @@
-package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components
+package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -33,6 +33,9 @@ import ua.acclorite.book_story.ui.Theme
import ua.acclorite.book_story.ui.colorScheme
import ua.acclorite.book_story.util.UIText
+/**
+ * Theme switcher item.
+ */
@Composable
fun AppearanceSettingsThemeSwitcherItem(
theme: Pair,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt
index 01cc5d76..2d2d8160 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt
@@ -7,11 +7,9 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@@ -22,24 +20,28 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ChipItem
-import ua.acclorite.book_story.presentation.Navigator
-import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
+import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel
+import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
+import ua.acclorite.book_story.ui.elevation
import ua.acclorite.book_story.util.Constants
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GeneralSettings(
- mainViewModel: MainViewModel = hiltViewModel(),
+ mainViewModel: MainViewModel,
navigator: Navigator
) {
- val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+ val listState = rememberLazyListState()
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
+ canScroll = {
+ listState.canScrollForward
+ }
+ )
val context = LocalContext.current as ComponentActivity
val language = mainViewModel.language.collectAsState().value!!
@@ -51,36 +53,26 @@ fun GeneralSettings(
.windowInsetsPadding(WindowInsets.navigationBars),
containerColor = MaterialTheme.colorScheme.surface,
topBar = {
- AnimatedTopAppBar(
+ LargeTopAppBar(
+ title = {
+ Text(stringResource(id = R.string.general_settings))
+ },
+ navigationIcon = {
+ GoBackButton(navigator = navigator)
+ },
scrollBehavior = scrollBehavior,
- isTopBarScrolled = null,
-
- content1NavigationIcon = {
- IconButton(onClick = { navigator.navigateBack() }) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Go back",
- tint = MaterialTheme.colorScheme.onSurface
- )
- }
- },
- content1Title = {
- Text(
- stringResource(id = R.string.general_settings),
- style = MaterialTheme.typography.titleLarge,
- color = MaterialTheme.colorScheme.onSurface,
- overflow = TextOverflow.Ellipsis,
- maxLines = 1
- )
- },
- content1Actions = {}
+ colors = TopAppBarDefaults.largeTopAppBarColors(
+ containerColor = MaterialTheme.colorScheme.surface,
+ scrolledContainerColor = MaterialTheme.elevation()
+ )
)
}
) { paddingValues ->
LazyColumn(
Modifier
.fillMaxSize()
- .padding(top = paddingValues.calculateTopPadding())
+ .padding(top = paddingValues.calculateTopPadding()),
+ state = listState
) {
item {
ChipsWithTitle(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt
new file mode 100644
index 00000000..a1b10ba3
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt
@@ -0,0 +1,217 @@
+package ua.acclorite.book_story.presentation.screens.settings.nested.reader
+
+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.windowInsetsPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LargeTopAppBar
+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.collectAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.unit.dp
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.domain.model.ChipItem
+import ua.acclorite.book_story.presentation.components.CategoryTitle
+import ua.acclorite.book_story.presentation.components.GoBackButton
+import ua.acclorite.book_story.presentation.data.MainEvent
+import ua.acclorite.book_story.presentation.data.MainViewModel
+import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.screens.settings.components.CheckboxWithTitle
+import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
+import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle
+import ua.acclorite.book_story.ui.elevation
+import ua.acclorite.book_story.util.Constants
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ReaderSettings(
+ mainViewModel: MainViewModel,
+ navigator: Navigator
+) {
+ val listState = rememberLazyListState()
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
+ canScroll = {
+ listState.canScrollForward
+ }
+ )
+
+ val fontFamily = Constants.FONTS.find {
+ it.id == mainViewModel.fontFamily.collectAsState().value!!
+ } ?: Constants.FONTS[0]
+ val fontStyle = mainViewModel.isItalic.collectAsState().value!!
+ val fontSize = mainViewModel.fontSize.collectAsState().value!!
+ val lineHeight = mainViewModel.lineHeight.collectAsState().value!!
+ val paragraphHeight = mainViewModel.paragraphHeight.collectAsState().value!!
+ val paragraphIndentation = mainViewModel.paragraphIndentation.collectAsState().value!!
+
+ Scaffold(
+ Modifier
+ .fillMaxSize()
+ .nestedScroll(scrollBehavior.nestedScrollConnection)
+ .windowInsetsPadding(WindowInsets.navigationBars),
+ containerColor = MaterialTheme.colorScheme.surface,
+ topBar = {
+ LargeTopAppBar(
+ title = {
+ Text(stringResource(id = R.string.reader_settings))
+ },
+ navigationIcon = {
+ GoBackButton(navigator = navigator)
+ },
+ scrollBehavior = scrollBehavior,
+ colors = TopAppBarDefaults.largeTopAppBarColors(
+ containerColor = MaterialTheme.colorScheme.surface,
+ scrolledContainerColor = MaterialTheme.elevation()
+ )
+ )
+ }
+ ) { paddingValues ->
+ LazyColumn(
+ Modifier
+ .fillMaxSize()
+ .padding(top = paddingValues.calculateTopPadding()),
+ state = listState
+ ) {
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ item {
+ CategoryTitle(
+ title = stringResource(id = R.string.font_reader_settings),
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ item {
+ ChipsWithTitle(
+ title = stringResource(id = R.string.font_family_option),
+ chips = Constants.FONTS
+ .map {
+ ChipItem(
+ it.id,
+ it.fontName.asString(),
+ MaterialTheme.typography.labelLarge.copy(
+ fontFamily = it.font
+ ),
+ it.id == fontFamily.id
+ )
+ },
+ onClick = {
+ mainViewModel.onEvent(MainEvent.OnChangeFontFamily(it.id))
+ }
+ )
+ }
+ item {
+ ChipsWithTitle(
+ title = stringResource(id = R.string.font_style_option),
+ chips = listOf(
+ ChipItem(
+ "normal",
+ stringResource(id = R.string.font_style_normal),
+ MaterialTheme.typography.labelLarge.copy(
+ fontFamily = fontFamily.font,
+ fontStyle = FontStyle.Normal
+ ),
+ !fontStyle
+ ),
+ ChipItem(
+ "italic",
+ stringResource(id = R.string.font_style_italic),
+ MaterialTheme.typography.labelLarge.copy(
+ fontFamily = fontFamily.font,
+ fontStyle = FontStyle.Italic
+ ),
+ fontStyle
+ ),
+ ),
+ onClick = {
+ mainViewModel.onEvent(
+ MainEvent.OnChangeFontStyle(
+ when (it.id) {
+ "italic" -> true
+ else -> false
+ }
+ )
+ )
+ }
+ )
+ }
+ item {
+ SliderWithTitle(
+ value = fontSize to "pt",
+ fromValue = 10,
+ toValue = 35,
+ title = stringResource(id = R.string.font_size_option),
+ onValueChange = {
+ mainViewModel.onEvent(
+ MainEvent.OnChangeFontSize(it)
+ )
+ }
+ )
+ }
+ item {
+ Spacer(modifier = Modifier.height(22.dp))
+ }
+ item {
+ CategoryTitle(
+ title = stringResource(id = R.string.text_reader_settings),
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ item {
+ SliderWithTitle(
+ value = lineHeight to "pt",
+ fromValue = 1,
+ toValue = 16,
+ title = stringResource(id = R.string.line_height_option),
+ onValueChange = {
+ mainViewModel.onEvent(
+ MainEvent.OnChangeLineHeight(it)
+ )
+ }
+ )
+ }
+ item {
+ SliderWithTitle(
+ value = paragraphHeight to "pt",
+ fromValue = 0,
+ toValue = 24,
+ title = stringResource(id = R.string.paragraph_height_option),
+ onValueChange = {
+ mainViewModel.onEvent(
+ MainEvent.OnChangeParagraphHeight(it)
+ )
+ }
+ )
+ }
+ item {
+ CheckboxWithTitle(
+ selected = paragraphIndentation,
+ title = stringResource(id = R.string.paragraph_indentation_option)
+ ) {
+ mainViewModel.onEvent(
+ MainEvent.OnChangeParagraphIndentation(!paragraphIndentation)
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/ui/Transitions.kt b/app/src/main/java/ua/acclorite/book_story/ui/Transitions.kt
index 71e701e7..71b8279f 100644
--- a/app/src/main/java/ua/acclorite/book_story/ui/Transitions.kt
+++ b/app/src/main/java/ua/acclorite/book_story/ui/Transitions.kt
@@ -17,10 +17,16 @@ object Transitions {
val FadeTransitionOut = fadeOut(tween(50))
val SlidingTransitionIn = fadeIn(tween(300)) +
- slideInHorizontally(tween(300)) { it / 15 }
+ slideInHorizontally(tween(300)) { it / 16 }
+
+ val BackSlidingTransitionIn = fadeIn(tween(300)) +
+ slideInHorizontally(tween(300)) { -it / 16 }
val SlidingTransitionOut = fadeOut(tween(200)) +
- slideOutHorizontally(tween(200)) { it / 25 }
+ slideOutHorizontally(tween(300)) { -it / 16 }
+
+ val BackSlidingTransitionOut = fadeOut(tween(200)) +
+ slideOutHorizontally(tween(300)) { it / 16 }
}
@Composable
diff --git a/app/src/main/java/ua/acclorite/book_story/util/UIText.kt b/app/src/main/java/ua/acclorite/book_story/util/UIText.kt
index c859b54d..223ccf24 100644
--- a/app/src/main/java/ua/acclorite/book_story/util/UIText.kt
+++ b/app/src/main/java/ua/acclorite/book_story/util/UIText.kt
@@ -6,9 +6,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
-sealed class UIText {
- data class StringValue(val value: String) : UIText()
- class StringResource(@StringRes val resId: Int, vararg val args: Any) : UIText()
+sealed class UIText(val string: String?) {
+ data class StringValue(val value: String) : UIText(value)
+ class StringResource(@StringRes val resId: Int, vararg val args: Any) : UIText(null)
@Composable
fun asString(): String {
diff --git a/app/src/main/res/drawable/app_icon_colored.png b/app/src/main/res/drawable/app_icon_colored.png
index e76100d1..3615361a 100644
Binary files a/app/src/main/res/drawable/app_icon_colored.png and b/app/src/main/res/drawable/app_icon_colored.png differ
diff --git a/app/src/main/res/drawable/app_icon_monochrome.png b/app/src/main/res/drawable/app_icon_monochrome.png
new file mode 100644
index 00000000..0bc46a28
Binary files /dev/null and b/app/src/main/res/drawable/app_icon_monochrome.png differ
diff --git a/app/src/main/res/drawable/splash_icon.xml b/app/src/main/res/drawable/splash_icon.xml
index a088bfa2..fd0864f8 100644
--- a/app/src/main/res/drawable/splash_icon.xml
+++ b/app/src/main/res/drawable/splash_icon.xml
@@ -2,7 +2,7 @@
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/app_icon.xml b/app/src/main/res/mipmap-anydpi-v26/app_icon.xml
similarity index 60%
rename from app/src/main/res/mipmap-anydpi/app_icon.xml
rename to app/src/main/res/mipmap-anydpi-v26/app_icon.xml
index be2d22d2..e32ce33f 100644
--- a/app/src/main/res/mipmap-anydpi/app_icon.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/app_icon.xml
@@ -1,6 +1,6 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/app_icon_round.xml b/app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml
similarity index 60%
rename from app/src/main/res/mipmap-anydpi/app_icon_round.xml
rename to app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml
index be2d22d2..e32ce33f 100644
--- a/app/src/main/res/mipmap-anydpi/app_icon_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml
@@ -1,6 +1,6 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/app_icon.webp b/app/src/main/res/mipmap-hdpi/app_icon.webp
index 62ba9e1c..98437d18 100644
Binary files a/app/src/main/res/mipmap-hdpi/app_icon.webp and b/app/src/main/res/mipmap-hdpi/app_icon.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/app_icon_foreground.webp b/app/src/main/res/mipmap-hdpi/app_icon_foreground.webp
index 06f5c8f4..e9f6b85f 100644
Binary files a/app/src/main/res/mipmap-hdpi/app_icon_foreground.webp and b/app/src/main/res/mipmap-hdpi/app_icon_foreground.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/app_icon_monochrome.webp b/app/src/main/res/mipmap-hdpi/app_icon_monochrome.webp
index 2078e7fb..96b04fce 100644
Binary files a/app/src/main/res/mipmap-hdpi/app_icon_monochrome.webp and b/app/src/main/res/mipmap-hdpi/app_icon_monochrome.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/app_icon_round.webp b/app/src/main/res/mipmap-hdpi/app_icon_round.webp
index 5a201221..bf61c7b9 100644
Binary files a/app/src/main/res/mipmap-hdpi/app_icon_round.webp and b/app/src/main/res/mipmap-hdpi/app_icon_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/app_icon.webp b/app/src/main/res/mipmap-mdpi/app_icon.webp
index 17f3912c..70ae38f3 100644
Binary files a/app/src/main/res/mipmap-mdpi/app_icon.webp and b/app/src/main/res/mipmap-mdpi/app_icon.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/app_icon_foreground.webp b/app/src/main/res/mipmap-mdpi/app_icon_foreground.webp
index eeb288e4..8196e481 100644
Binary files a/app/src/main/res/mipmap-mdpi/app_icon_foreground.webp and b/app/src/main/res/mipmap-mdpi/app_icon_foreground.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/app_icon_monochrome.webp b/app/src/main/res/mipmap-mdpi/app_icon_monochrome.webp
index 3e46530f..132ec0d3 100644
Binary files a/app/src/main/res/mipmap-mdpi/app_icon_monochrome.webp and b/app/src/main/res/mipmap-mdpi/app_icon_monochrome.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/app_icon_round.webp b/app/src/main/res/mipmap-mdpi/app_icon_round.webp
index b0889cab..a837a3b4 100644
Binary files a/app/src/main/res/mipmap-mdpi/app_icon_round.webp and b/app/src/main/res/mipmap-mdpi/app_icon_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/app_icon.webp b/app/src/main/res/mipmap-xhdpi/app_icon.webp
index 73172c04..f7ce11aa 100644
Binary files a/app/src/main/res/mipmap-xhdpi/app_icon.webp and b/app/src/main/res/mipmap-xhdpi/app_icon.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/app_icon_foreground.webp b/app/src/main/res/mipmap-xhdpi/app_icon_foreground.webp
index 67bc1941..dc7a25de 100644
Binary files a/app/src/main/res/mipmap-xhdpi/app_icon_foreground.webp and b/app/src/main/res/mipmap-xhdpi/app_icon_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/app_icon_monochrome.webp b/app/src/main/res/mipmap-xhdpi/app_icon_monochrome.webp
index ee458a14..0587af88 100644
Binary files a/app/src/main/res/mipmap-xhdpi/app_icon_monochrome.webp and b/app/src/main/res/mipmap-xhdpi/app_icon_monochrome.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/app_icon_round.webp b/app/src/main/res/mipmap-xhdpi/app_icon_round.webp
index 24508b61..689c93a3 100644
Binary files a/app/src/main/res/mipmap-xhdpi/app_icon_round.webp and b/app/src/main/res/mipmap-xhdpi/app_icon_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/app_icon.webp b/app/src/main/res/mipmap-xxhdpi/app_icon.webp
index c04de250..e5e7fdb3 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/app_icon.webp and b/app/src/main/res/mipmap-xxhdpi/app_icon.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/app_icon_foreground.webp b/app/src/main/res/mipmap-xxhdpi/app_icon_foreground.webp
index b14b0285..9cafdb0b 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/app_icon_foreground.webp and b/app/src/main/res/mipmap-xxhdpi/app_icon_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/app_icon_monochrome.webp b/app/src/main/res/mipmap-xxhdpi/app_icon_monochrome.webp
index 2e0651e1..5e79d3ca 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/app_icon_monochrome.webp and b/app/src/main/res/mipmap-xxhdpi/app_icon_monochrome.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/app_icon_round.webp b/app/src/main/res/mipmap-xxhdpi/app_icon_round.webp
index 9f81e3a6..af8653bb 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/app_icon_round.webp and b/app/src/main/res/mipmap-xxhdpi/app_icon_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/app_icon.webp b/app/src/main/res/mipmap-xxxhdpi/app_icon.webp
index a56c37e3..96f71076 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/app_icon.webp and b/app/src/main/res/mipmap-xxxhdpi/app_icon.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/app_icon_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/app_icon_foreground.webp
index 511230fb..70b4d01d 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/app_icon_foreground.webp and b/app/src/main/res/mipmap-xxxhdpi/app_icon_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/app_icon_monochrome.webp b/app/src/main/res/mipmap-xxxhdpi/app_icon_monochrome.webp
index 2614838c..f01fe04a 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/app_icon_monochrome.webp and b/app/src/main/res/mipmap-xxxhdpi/app_icon_monochrome.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/app_icon_round.webp b/app/src/main/res/mipmap-xxxhdpi/app_icon_round.webp
index 7654f8b6..f962189c 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/app_icon_round.webp and b/app/src/main/res/mipmap-xxxhdpi/app_icon_round.webp differ
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 6e172b27..b1f63a7f 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -5,7 +5,7 @@
Бібліотека
Історія
- Оглянути
+ Огляд
Налаштування
Про застосунок
Допомога
@@ -61,6 +61,15 @@
Щось пішло не так. Будь ласка, перевірте чи файл цієї книги не пошкоджений.
+ Хибний формат файлу.
+ Помилка: %1$s
+ Цей файл зашифрований.
+ Цей файл пустий або пошкоджений.
+ Не знайдено жодного перекладача.
+ Не знайдено жодного браузера.
+
+ Вміст книги пустий. Будь ласка, перевірте вміст книги та оновіть її.
+
@@ -96,6 +105,10 @@
Перемістити цю книгу в \"Прочитано\"
Назад в Бібліотеку
Скасувати
+ Копіювати
+ Перекласти
+ Словник
+ Ще раз
файли
@@ -106,6 +119,7 @@
Невідомий
Вам потрібно читати більше!
Так тримати!
+ Браво, ви повністю прочитали цю книгу!
Змінити обкладинку
Виберіть нову обкладинку з ваших фото
Видалити обкладинку
@@ -121,24 +135,30 @@
Головні
- Мова застосунку
+ Мова застосунка
Зовнішній вигляд
- Тема застосунку, темна тема
- Шрифт
- Текст
+ Налаштування теми, кольори
+ Читач
+ Налаштування шрифта, налаштування текста
+
+
+ Шрифт
+ Текст
+ Налаштування теми
+ Кольори
- Мова застосунку
+ Мова застосунка
Темна тема
- Тема застосунку
+ Тема застосунка
Сімейство шрифтів
- Стиль шрифту
- Розмір шрифту
+ Стиль шрифта
+ Розмір шрифта
Висота рядка
- Висота абзацу
+ Висота абзаца
Абзацний відступ
- Колір фону
- Колір шрифту
+ Колір фона
+ Колір шрифта
Червоний
@@ -172,7 +192,6 @@
Обкладинка була успішно змінена.
Обкладинка була успішно видалена.
Назва була успішно змінена.
- Файл \"%1$s\" не знайдено.
Ця книга була успішно видалена.
Ця книга була успішно переміщена.
Скопійовано
@@ -180,6 +199,8 @@
Елемент історії був успішно видалений.
+ Файл \"%1$s\" не знайдено.
+ Текст, автор та опис книги були успішно оновлені.
Перемістити
@@ -200,6 +221,7 @@
Дякуємо за те, що прочитали \"%1$s\" з нами.
Ми сподіваємось, що вам сподобалася ця книга!
+ %1$s букв, %2$s слів.
За замовчуванням
@@ -208,4 +230,11 @@
Сьогодні
Вчора
+
+ Оновити текст, автора та опис книги
+ Продовжити читання
+ Назад
+ Обкладинка
+ Відкрити налаштування читача
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d4206141..5d8568b1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,6 +1,6 @@
- 0.8.0
+ 0.9.0
Book\'s Story
@@ -57,11 +57,20 @@
No description.
- File was not found. Please check whether this book\'s file still exist.
+ File was not found. Please check whether this book\'s file still exists.
Something went wrong. Please check whether this book\'s file is not corrupted.
+ Wrong file format.
+ Error: %1$s
+ This file is encrypted.
+ This file is empty or corrupted.
+ No translator app found.
+ No browser app found.
+
+ The book content is empty. Please, make sure that the book content is not empty and update it.
+
@@ -96,6 +105,10 @@
Move this book to \"Already Read\"
Back to Library
Undo
+ Copy
+ Translate
+ Dictionary
+ Retry
files
@@ -106,6 +119,7 @@
Unknown
You need to read more!
Keep it up!
+ Well done, you have read the whole book!
Change cover image
Choose new cover from your photos
Delete cover image
@@ -123,9 +137,15 @@
General
App language
Appearance
- App theme, dark theme
- Font
- Text
+ Theme preferences, colors
+ Reader
+ Font settings, text settings
+
+
+ Font
+ Text
+ Theme preferences
+ Colors
App language
@@ -172,7 +192,6 @@
Cover Image was successfully changed.
Cover Image was successfully deleted.
Title was successfully changed.
- File \"%1$s\" was not found.
This book was successfully deleted.
This book was successfully moved.
Copied
@@ -180,6 +199,8 @@
History element was successfully deleted.
+ File \"%1$s\" was not found.
+ Book text, author and description were successfully updated.
Move
@@ -200,6 +221,7 @@
Thank you for reading \"%1$s\" with us.
We hope you enjoyed this book!
+ %1$s characters, %2$s words.
Default
@@ -208,4 +230,11 @@
Today
Yesterday
+
+ Refresh text, author and description of the book
+ Continue reading
+ Go back
+ Cover image
+ Open reader settings
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 972f848f..ad932758 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -3,6 +3,7 @@