0.9.0 - New loading system(much faster) + New between screen transitions + New snackbar(vertical animation + visuals) + New selection toolbar(translate + dictionary options) in ReaderScreen + New top app bar in SettingsScreen and nested categories + New updating system in BookInfo(updating text, author and description of the book) + Tablet UI support(on wide screens NavigationBar -> NavigationRail) + New app icon + New error handling system(it won't let you add/update book if it's not valid) + New theme(+new names) + Small bugs fixed.. + General performance optimisation(still in progress).
This commit is contained in:
parent
5a966e9d9d
commit
6806ac904c
136 changed files with 2580 additions and 911 deletions
|
|
@ -2,10 +2,7 @@ plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("com.google.devtools.ksp")
|
id("com.google.devtools.ksp")
|
||||||
|
|
||||||
id("com.google.dagger.hilt.android")
|
id("com.google.dagger.hilt.android")
|
||||||
kotlin("kapt")
|
|
||||||
|
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,7 +15,7 @@ android {
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "0.8.0"
|
versionName = "0.9.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|
@ -62,35 +59,35 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
val composeVersion = "1.5.7"
|
|
||||||
|
|
||||||
// Default
|
// Default
|
||||||
implementation("androidx.core:core-ktx:1.12.0")
|
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")
|
implementation("androidx.activity:activity-compose:1.8.2")
|
||||||
|
|
||||||
|
// Compose BOM libraries
|
||||||
implementation(platform("androidx.compose:compose-bom:2024.01.00"))
|
implementation(platform("androidx.compose:compose-bom:2024.01.00"))
|
||||||
implementation("androidx.compose.ui:ui:$composeVersion")
|
implementation("androidx.compose.ui:ui")
|
||||||
implementation("androidx.compose.ui:ui-graphics:$composeVersion")
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion")
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
implementation("androidx.compose.ui:ui-android:1.6.0")
|
implementation("androidx.compose.ui:ui-android")
|
||||||
implementation("androidx.compose.material3:material3:1.1.2")
|
implementation("androidx.compose.material3:material3")
|
||||||
implementation("androidx.compose.material:material:1.6.0")
|
implementation("androidx.compose.material3:material3-window-size-class")
|
||||||
|
implementation("androidx.compose.material:material")
|
||||||
|
|
||||||
// Unit tests
|
// Unit tests
|
||||||
testImplementation("androidx.test:core:latest.release")
|
testImplementation("androidx.test:core:latest.release")
|
||||||
testImplementation("junit:junit:latest.release")
|
testImplementation("junit:junit:latest.release")
|
||||||
testImplementation("androidx.arch.core:core-testing:latest.release")
|
testImplementation("androidx.arch.core:core-testing:latest.release")
|
||||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test: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("androidx.test.ext:truth:1.5.0")
|
||||||
testImplementation("com.squareup.okhttp3:mockwebserver:latest.release")
|
testImplementation("com.squareup.okhttp3:mockwebserver:latest.release")
|
||||||
testImplementation("io.mockk:mockk: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
|
// Instrumentation tests
|
||||||
androidTestImplementation("com.google.dagger:hilt-android-testing:latest.release")
|
androidTestImplementation("com.google.dagger:hilt-android-testing:latest.release")
|
||||||
kaptAndroidTest("com.google.dagger:hilt-android-compiler:latest.release")
|
|
||||||
androidTestImplementation("junit:junit:latest.release")
|
androidTestImplementation("junit:junit:latest.release")
|
||||||
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:latest.release")
|
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:latest.release")
|
||||||
androidTestImplementation("androidx.arch.core:core-testing:latest.release")
|
androidTestImplementation("androidx.arch.core:core-testing:latest.release")
|
||||||
|
|
@ -102,17 +99,16 @@ dependencies {
|
||||||
androidTestImplementation("androidx.test:runner:latest.release")
|
androidTestImplementation("androidx.test:runner:latest.release")
|
||||||
|
|
||||||
// All dependencies
|
// All dependencies
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
|
||||||
implementation("androidx.compose.material:material-icons-extended:1.5.4")
|
implementation("androidx.compose.material:material-icons-extended")
|
||||||
implementation("androidx.activity:activity-compose:1.8.2")
|
|
||||||
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
|
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
|
||||||
|
|
||||||
// Dagger - Hilt
|
// Dagger - Hilt
|
||||||
implementation("com.google.dagger:hilt-android:2.50")
|
implementation("com.google.dagger:hilt-android:2.51")
|
||||||
kapt("com.google.dagger:hilt-android-compiler:2.50")
|
ksp("com.google.dagger:hilt-android-compiler:2.51")
|
||||||
implementation("com.google.dagger:hilt-compiler:latest.release")
|
implementation("com.google.dagger:hilt-compiler:latest.release")
|
||||||
kapt("androidx.hilt:hilt-compiler:latest.release")
|
ksp("androidx.hilt:hilt-compiler:latest.release")
|
||||||
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
|
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
|
||||||
|
|
||||||
// Room
|
// Room
|
||||||
implementation("androidx.room:room-runtime:2.6.1")
|
implementation("androidx.room:room-runtime:2.6.1")
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,21 @@
|
||||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
|
||||||
|
<!-- Translator -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent>
|
||||||
|
|
||||||
|
<!-- Browser -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.WEB_SEARCH" />
|
||||||
|
</intent>
|
||||||
|
|
||||||
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".util.BookApplication"
|
android:name=".util.BookApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|
@ -19,7 +34,7 @@
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".Activity"
|
||||||
android:configChanges="screenSize|screenLayout|smallestScreenSize|locale|density|uiMode"
|
android:configChanges="screenSize|screenLayout|smallestScreenSize|locale|density|uiMode"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
@ -30,15 +45,6 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
|
||||||
android:enabled="false"
|
|
||||||
android:exported="false">
|
|
||||||
<meta-data
|
|
||||||
android:name="autoStoreLocales"
|
|
||||||
android:value="true" />
|
|
||||||
</service>
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
BIN
app/src/main/app_icon-playstore.png
Normal file
BIN
app/src/main/app_icon-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
app/src/main/app_icon_monochrome-playstore.png
Normal file
BIN
app/src/main/app_icon_monochrome-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
|
|
@ -1,67 +1,104 @@
|
||||||
package ua.acclorite.book_story
|
package ua.acclorite.book_story
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.database.CursorWindow
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
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.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
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.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.presentation.NavigationHost
|
import ua.acclorite.book_story.presentation.components.bottom_navigation_bar.BottomNavigationBar
|
||||||
import ua.acclorite.book_story.presentation.Screen
|
import ua.acclorite.book_story.presentation.components.custom_navigation_rail.CustomNavigationRail
|
||||||
import ua.acclorite.book_story.presentation.components.BottomNavigationBar
|
|
||||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||||
import ua.acclorite.book_story.presentation.data.MainViewModel
|
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.book_info.BookInfoScreen
|
||||||
import ua.acclorite.book_story.presentation.screens.browse.BrowseScreen
|
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.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.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.reader.ReaderScreen
|
||||||
import ua.acclorite.book_story.presentation.screens.settings.SettingsScreen
|
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.appearance.AppearanceSettings
|
||||||
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings
|
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.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.Transitions
|
||||||
import ua.acclorite.book_story.ui.isDark
|
import ua.acclorite.book_story.ui.isDark
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("DiscouragedPrivateApi")
|
||||||
|
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class Activity : ComponentActivity() {
|
||||||
private val mainViewModel: MainViewModel by viewModels()
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
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 {
|
installSplashScreen().apply {
|
||||||
setKeepOnScreenCondition {
|
setKeepOnScreenCondition {
|
||||||
!mainViewModel.isReady.value
|
!mainViewModel.isReady.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edge to edge
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
|
val windowClass = calculateWindowSizeClass(activity = this)
|
||||||
val updating = mainViewModel.updating.collectAsState().value
|
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(
|
BooksHistoryResurrectionTheme(
|
||||||
theme = theme,
|
theme = theme,
|
||||||
|
|
@ -70,28 +107,36 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (!updating) {
|
if (!updating) {
|
||||||
NavigationHost(startScreen = Screen.LIBRARY) {
|
NavigationHost(startScreen = Screen.LIBRARY) {
|
||||||
val currentScreen by this.getCurrentScreen().collectAsState()
|
val currentScreen by this.getCurrentScreen().collectAsState()
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = currentScreen == Screen.LIBRARY ||
|
visible = currentScreen == Screen.LIBRARY ||
|
||||||
currentScreen == Screen.HISTORY ||
|
currentScreen == Screen.HISTORY ||
|
||||||
currentScreen == Screen.BROWSE,
|
currentScreen == Screen.BROWSE,
|
||||||
enter = fadeIn(tween(200)) +
|
enter = Transitions.BackSlidingTransitionIn,
|
||||||
slideInHorizontally(tween(200)) { it / 15 },
|
|
||||||
exit = Transitions.SlidingTransitionOut
|
exit = Transitions.SlidingTransitionOut
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
if (!tabletUI) {
|
||||||
BottomNavigationBar(navigator = this@NavigationHost)
|
BottomNavigationBar(navigator = this@NavigationHost)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(bottom = it.calculateBottomPadding())
|
.padding(
|
||||||
|
start = if (tabletUI) 80.dp else 0.dp,
|
||||||
|
bottom = it.calculateBottomPadding()
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
composable(screen = Screen.LIBRARY) {
|
composable(screen = Screen.LIBRARY) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
LibraryScreen(
|
LibraryScreen(
|
||||||
|
viewModel = libraryViewModel,
|
||||||
|
historyViewModel = historyViewModel,
|
||||||
|
browseViewModel = browseViewModel,
|
||||||
navigator = this@NavigationHost,
|
navigator = this@NavigationHost,
|
||||||
addedBooks = retrieveArgument("added_books") as? List<Book>
|
addedBooks = retrieveArgument("added_books") as? List<Book>
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
|
|
@ -99,13 +144,25 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(screen = Screen.HISTORY) {
|
composable(screen = Screen.HISTORY) {
|
||||||
HistoryScreen(navigator = this@NavigationHost)
|
HistoryScreen(
|
||||||
|
viewModel = historyViewModel,
|
||||||
|
libraryViewModel = libraryViewModel,
|
||||||
|
navigator = this@NavigationHost
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(screen = Screen.BROWSE) {
|
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
|
exitAnim = Transitions.SlidingTransitionOut
|
||||||
) {
|
) {
|
||||||
BookInfoScreen(
|
BookInfoScreen(
|
||||||
|
libraryViewModel = libraryViewModel,
|
||||||
|
browseViewModel = browseViewModel,
|
||||||
|
historyViewModel = historyViewModel,
|
||||||
navigator = this@NavigationHost
|
navigator = this@NavigationHost
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -125,6 +185,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
exitAnim = Transitions.SlidingTransitionOut
|
exitAnim = Transitions.SlidingTransitionOut
|
||||||
) {
|
) {
|
||||||
ReaderScreen(
|
ReaderScreen(
|
||||||
|
mainViewModel = mainViewModel,
|
||||||
|
libraryViewModel = libraryViewModel,
|
||||||
|
historyViewModel = historyViewModel,
|
||||||
navigator = this@NavigationHost
|
navigator = this@NavigationHost
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -147,6 +210,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
exitAnim = Transitions.SlidingTransitionOut
|
exitAnim = Transitions.SlidingTransitionOut
|
||||||
) {
|
) {
|
||||||
GeneralSettings(
|
GeneralSettings(
|
||||||
|
mainViewModel = mainViewModel,
|
||||||
navigator = this@NavigationHost
|
navigator = this@NavigationHost
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -156,9 +220,25 @@ class MainActivity : AppCompatActivity() {
|
||||||
exitAnim = Transitions.SlidingTransitionOut
|
exitAnim = Transitions.SlidingTransitionOut
|
||||||
) {
|
) {
|
||||||
AppearanceSettings(
|
AppearanceSettings(
|
||||||
|
mainViewModel = mainViewModel,
|
||||||
navigator = this@NavigationHost
|
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 {
|
} else {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -6,10 +6,10 @@ import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import ua.acclorite.book_story.data.local.data_store.DataStore
|
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.local.data_store.DataStoreImpl
|
||||||
import ua.acclorite.book_story.data.mapper.BookMapper
|
import ua.acclorite.book_story.data.mapper.book.BookMapper
|
||||||
import ua.acclorite.book_story.data.mapper.BookMapperImpl
|
import ua.acclorite.book_story.data.mapper.book.BookMapperImpl
|
||||||
import ua.acclorite.book_story.data.mapper.HistoryMapper
|
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
||||||
import ua.acclorite.book_story.data.mapper.HistoryMapperImpl
|
import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl
|
||||||
import ua.acclorite.book_story.data.repository.BookRepositoryImpl
|
import ua.acclorite.book_story.data.repository.BookRepositoryImpl
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@ import ua.acclorite.book_story.domain.model.Category
|
||||||
data class BookEntity(
|
data class BookEntity(
|
||||||
@PrimaryKey(true) val id: Int? = null,
|
@PrimaryKey(true) val id: Int? = null,
|
||||||
val title: String,
|
val title: String,
|
||||||
|
val author: String?,
|
||||||
|
val description: String?,
|
||||||
|
val text: String,
|
||||||
val filePath: String,
|
val filePath: String,
|
||||||
val progress: Float,
|
val progress: Float,
|
||||||
var lastOpened: Long? = null,
|
|
||||||
val image: ByteArray = byteArrayOf(),
|
val image: ByteArray = byteArrayOf(),
|
||||||
val category: Category
|
val category: Category
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ interface BookDao {
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT *
|
SELECT * FROM bookentity
|
||||||
FROM bookentity
|
|
||||||
WHERE LOWER(title) LIKE '%' || LOWER(:query) || '%'
|
WHERE LOWER(title) LIKE '%' || LOWER(:query) || '%'
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
@ -31,6 +30,9 @@ interface BookDao {
|
||||||
)
|
)
|
||||||
suspend fun findBookById(id: Int): BookEntity
|
suspend fun findBookById(id: Int): BookEntity
|
||||||
|
|
||||||
|
@Query("SELECT * FROM bookentity WHERE id IN (:ids)")
|
||||||
|
suspend fun findBooksById(ids: List<Int>): List<BookEntity>
|
||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
suspend fun deleteBooks(books: List<BookEntity>)
|
suspend fun deleteBooks(books: List<BookEntity>)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.data.local.dto.BookEntity
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.data.local.dto.HistoryEntity
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
|
||||||
import ua.acclorite.book_story.domain.model.History
|
import ua.acclorite.book_story.domain.model.History
|
||||||
|
|
||||||
interface HistoryMapper {
|
interface HistoryMapper {
|
||||||
suspend fun toHistoryEntity(history: History): HistoryEntity
|
suspend fun toHistoryEntity(history: History): HistoryEntity
|
||||||
|
|
||||||
suspend fun toHistory(historyEntity: HistoryEntity, book: Book): History
|
suspend fun toHistory(historyEntity: HistoryEntity): History
|
||||||
}
|
}
|
||||||
|
|
@ -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.data.local.dto.HistoryEntity
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
|
||||||
import ua.acclorite.book_story.domain.model.History
|
import ua.acclorite.book_story.domain.model.History
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -9,15 +8,15 @@ class HistoryMapperImpl @Inject constructor() : HistoryMapper {
|
||||||
override suspend fun toHistoryEntity(history: History): HistoryEntity {
|
override suspend fun toHistoryEntity(history: History): HistoryEntity {
|
||||||
return HistoryEntity(
|
return HistoryEntity(
|
||||||
id = history.id,
|
id = history.id,
|
||||||
bookId = history.book.id ?: 0,
|
bookId = history.bookId,
|
||||||
time = history.time
|
time = history.time
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun toHistory(historyEntity: HistoryEntity, book: Book): History {
|
override suspend fun toHistory(historyEntity: HistoryEntity): History {
|
||||||
return History(
|
return History(
|
||||||
historyEntity.id,
|
historyEntity.id,
|
||||||
book = book,
|
bookId = historyEntity.bookId,
|
||||||
time = historyEntity.time
|
time = historyEntity.time
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package ua.acclorite.book_story.data.parser
|
package ua.acclorite.book_story.data.parser
|
||||||
|
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
import ua.acclorite.book_story.domain.model.StringWithId
|
||||||
|
import ua.acclorite.book_story.util.Resource
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
interface TextParser {
|
interface TextParser {
|
||||||
suspend fun parse(file: File): List<StringWithId>
|
suspend fun parse(file: File): Resource<List<StringWithId>>
|
||||||
}
|
}
|
||||||
|
|
@ -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<StringWithId> {
|
|
||||||
if (!file.name.endsWith(".txt")) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
|
||||||
val formattedLines = mutableListOf<StringWithId>()
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
BufferedReader(FileReader(file)).forEachLine { line ->
|
|
||||||
formattedLines.add(
|
|
||||||
StringWithId(line)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedLines
|
|
||||||
} catch (e: Exception) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package ua.acclorite.book_story.data.parser
|
package ua.acclorite.book_story.data.parser.epub
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nl.siegmann.epublib.epub.EpubReader
|
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.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
|
|
@ -51,7 +53,7 @@ class EpubFileParser @Inject constructor() : FileParser {
|
||||||
filePath = file.path,
|
filePath = file.path,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
category = Category.entries[0],
|
||||||
coverImage = coverImage
|
coverImage = coverImage?.asImageBitmap()
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
|
|
@ -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.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nl.siegmann.epublib.epub.EpubReader
|
import nl.siegmann.epublib.epub.EpubReader
|
||||||
import org.jsoup.Jsoup
|
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.domain.model.StringWithId
|
||||||
|
import ua.acclorite.book_story.util.Resource
|
||||||
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
|
@ -14,9 +18,9 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class EpubTextParser @Inject constructor() : TextParser {
|
class EpubTextParser @Inject constructor() : TextParser {
|
||||||
|
|
||||||
override suspend fun parse(file: File): List<StringWithId> {
|
override suspend fun parse(file: File): Resource<List<StringWithId>> {
|
||||||
if (!file.name.endsWith(".epub")) {
|
if (!file.name.endsWith(".epub")) {
|
||||||
return emptyList()
|
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (e: Exception) {
|
||||||
return emptyList()
|
return Resource.Error(
|
||||||
|
UIText.StringResource(
|
||||||
|
R.string.error_query,
|
||||||
|
e.message?.take(40)?.trim() ?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package ua.acclorite.book_story.data.parser
|
package ua.acclorite.book_story.data.parser.pdf
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
|
import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
|
||||||
import com.tom_roush.pdfbox.pdmodel.PDDocument
|
import com.tom_roush.pdfbox.pdmodel.PDDocument
|
||||||
import ua.acclorite.book_story.R
|
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.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
import ua.acclorite.book_story.domain.model.StringWithId
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
package ua.acclorite.book_story.data.parser
|
package ua.acclorite.book_story.data.parser.pdf
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
|
import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
|
||||||
import com.tom_roush.pdfbox.pdmodel.PDDocument
|
import com.tom_roush.pdfbox.pdmodel.PDDocument
|
||||||
import com.tom_roush.pdfbox.text.PDFTextStripper
|
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.domain.model.StringWithId
|
||||||
|
import ua.acclorite.book_story.util.Resource
|
||||||
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PdfTextParser @Inject constructor(private val application: Application) : TextParser {
|
class PdfTextParser @Inject constructor(private val application: Application) : TextParser {
|
||||||
|
|
||||||
override suspend fun parse(file: File): List<StringWithId> {
|
override suspend fun parse(file: File): Resource<List<StringWithId>> {
|
||||||
if (!file.name.endsWith(".pdf")) {
|
if (!file.name.endsWith(".pdf")) {
|
||||||
return emptyList()
|
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -24,6 +28,11 @@ class PdfTextParser @Inject constructor(private val application: Application) :
|
||||||
val pdfStripper = PDFTextStripper()
|
val pdfStripper = PDFTextStripper()
|
||||||
pdfStripper.paragraphStart = "_new_line_"
|
pdfStripper.paragraphStart = "_new_line_"
|
||||||
|
|
||||||
|
if (document.isEncrypted) {
|
||||||
|
document.close()
|
||||||
|
return Resource.Error(UIText.StringResource(R.string.error_file_encrypted))
|
||||||
|
}
|
||||||
|
|
||||||
val oldText = pdfStripper.getText(document)
|
val oldText = pdfStripper.getText(document)
|
||||||
.replace("\r", "")
|
.replace("\r", "")
|
||||||
|
|
||||||
|
|
@ -38,7 +47,7 @@ class PdfTextParser @Inject constructor(private val application: Application) :
|
||||||
}
|
}
|
||||||
|
|
||||||
val unformattedLines = text.split("${pdfStripper.paragraphStart}|\\n".toRegex())
|
val unformattedLines = text.split("${pdfStripper.paragraphStart}|\\n".toRegex())
|
||||||
.filter { it.isNotEmpty() }
|
.filter { it.isNotBlank() }
|
||||||
|
|
||||||
val lines = mutableListOf<String>()
|
val lines = mutableListOf<String>()
|
||||||
unformattedLines.forEachIndexed { index, string ->
|
unformattedLines.forEachIndexed { index, string ->
|
||||||
|
|
@ -52,10 +61,18 @@ class PdfTextParser @Inject constructor(private val application: Application) :
|
||||||
stringWithIds.add(StringWithId(line.trim()))
|
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) {
|
} catch (e: Exception) {
|
||||||
return emptyList()
|
return Resource.Error(
|
||||||
|
UIText.StringResource(
|
||||||
|
R.string.error_query,
|
||||||
|
e.message?.take(40)?.trim() ?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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.R
|
||||||
|
import ua.acclorite.book_story.data.parser.FileParser
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
import ua.acclorite.book_story.domain.model.StringWithId
|
||||||
|
|
@ -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<List<StringWithId>> {
|
||||||
|
if (!file.name.endsWith(".txt")) {
|
||||||
|
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val formattedLines = mutableListOf<StringWithId>()
|
||||||
|
|
||||||
|
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() ?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,24 +4,27 @@ import android.os.Environment
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
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.data_store.DataStore
|
||||||
import ua.acclorite.book_story.data.local.dto.BookEntity
|
import ua.acclorite.book_story.data.local.dto.BookEntity
|
||||||
import ua.acclorite.book_story.data.local.room.BookDao
|
import ua.acclorite.book_story.data.local.room.BookDao
|
||||||
import ua.acclorite.book_story.data.mapper.BookMapper
|
import ua.acclorite.book_story.data.mapper.book.BookMapper
|
||||||
import ua.acclorite.book_story.data.mapper.HistoryMapper
|
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
||||||
import ua.acclorite.book_story.data.parser.EpubFileParser
|
import ua.acclorite.book_story.data.parser.epub.EpubFileParser
|
||||||
import ua.acclorite.book_story.data.parser.EpubTextParser
|
import ua.acclorite.book_story.data.parser.epub.EpubTextParser
|
||||||
import ua.acclorite.book_story.data.parser.PdfFileParser
|
import ua.acclorite.book_story.data.parser.pdf.PdfFileParser
|
||||||
import ua.acclorite.book_story.data.parser.PdfTextParser
|
import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
|
||||||
import ua.acclorite.book_story.data.parser.TxtFileParser
|
import ua.acclorite.book_story.data.parser.txt.TxtFileParser
|
||||||
import ua.acclorite.book_story.data.parser.TxtTextParser
|
import ua.acclorite.book_story.data.parser.txt.TxtTextParser
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.History
|
import ua.acclorite.book_story.domain.model.History
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
import ua.acclorite.book_story.domain.model.StringWithId
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
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.Constants
|
||||||
import ua.acclorite.book_story.util.Resource
|
import ua.acclorite.book_story.util.Resource
|
||||||
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
@ -49,9 +52,20 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
emit(Resource.Loading(true))
|
emit(Resource.Loading(true))
|
||||||
|
|
||||||
val books = database.searchBooks(query)
|
val books = database.searchBooks(query)
|
||||||
|
val history = database.getHistory()
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
Resource.Success(
|
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<Book> {
|
override suspend fun fastGetBooks(query: String): List<Book> {
|
||||||
val books = database.searchBooks(query)
|
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<Int>): List<Book> {
|
||||||
|
return database.findBooksById(ids).map { bookMapper.toBook(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findBook(
|
override suspend fun findBook(
|
||||||
|
|
@ -154,7 +182,10 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
filteredFiles = if (query.trim().isNotEmpty()) {
|
filteredFiles = if (query.trim().isNotEmpty()) {
|
||||||
filteredFiles.sortedByDescending {
|
filteredFiles.sortedByDescending {
|
||||||
it.name.lowercase().startsWith(query.trim().lowercase())
|
calculateFamiliarity(
|
||||||
|
query,
|
||||||
|
it.name.lowercase().trim()
|
||||||
|
)
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
} else {
|
} else {
|
||||||
filteredFiles.sortedByDescending {
|
filteredFiles.sortedByDescending {
|
||||||
|
|
@ -182,20 +213,38 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
} else if (file.name.endsWith(".epub")) {
|
} else if (file.name.endsWith(".epub")) {
|
||||||
epubTextParser.parse(file)
|
epubTextParser.parse(file)
|
||||||
} else {
|
} else {
|
||||||
|
emit(Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)))
|
||||||
return@flow
|
return@flow
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(Resource.Success(text))
|
if (text is Resource.Success && text.data == null) {
|
||||||
|
emit(
|
||||||
|
Resource.Error(
|
||||||
|
text.message
|
||||||
|
?: UIText.StringResource(R.string.error_file_empty)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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<File>): Flow<Resource<List<NullableBook>>> {
|
override suspend fun getBooksFromFiles(files: List<File>): List<NullableBook> {
|
||||||
return flow {
|
|
||||||
emit(Resource.Loading(true))
|
|
||||||
|
|
||||||
val books = mutableListOf<NullableBook>()
|
val books = mutableListOf<NullableBook>()
|
||||||
for (file in files) {
|
|
||||||
|
|
||||||
|
for (file in files) {
|
||||||
val parsedBook = if (file.name.endsWith(".txt")) {
|
val parsedBook = if (file.name.endsWith(".txt")) {
|
||||||
txtFileParser.parse(file)
|
txtFileParser.parse(file)
|
||||||
} else if (file.name.endsWith(".pdf")) {
|
} else if (file.name.endsWith(".pdf")) {
|
||||||
|
|
@ -203,7 +252,12 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
} else if (file.name.endsWith(".epub")) {
|
} else if (file.name.endsWith(".epub")) {
|
||||||
epubFileParser.parse(file)
|
epubFileParser.parse(file)
|
||||||
} else {
|
} else {
|
||||||
books.add(NullableBook.Null(file.name))
|
books.add(
|
||||||
|
NullableBook.Null(
|
||||||
|
file.name,
|
||||||
|
UIText.StringResource(R.string.error_wrong_file_format)
|
||||||
|
)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,27 +268,56 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
} else if (file.name.endsWith(".epub")) {
|
} else if (file.name.endsWith(".epub")) {
|
||||||
epubTextParser.parse(file)
|
epubTextParser.parse(file)
|
||||||
} else {
|
} else {
|
||||||
books.add(NullableBook.Null(file.name))
|
books.add(
|
||||||
|
NullableBook.Null(
|
||||||
|
file.name,
|
||||||
|
UIText.StringResource(R.string.error_wrong_file_format)
|
||||||
|
)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedBook == null || parsedText.isEmpty()) {
|
if (parsedBook == null) {
|
||||||
books.add(NullableBook.Null(file.name))
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
books.add(
|
books.add(
|
||||||
NullableBook.NotNull(
|
NullableBook.NotNull(
|
||||||
Pair(
|
Pair(
|
||||||
parsedBook.copy(text = parsedText),
|
parsedBook.copy(text = parsedText.data),
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(Resource.Success(books))
|
return books
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun insertHistory(history: List<History>) {
|
override suspend fun insertHistory(history: List<History>) {
|
||||||
|
|
@ -246,11 +329,13 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
emit(Resource.Loading(true))
|
emit(Resource.Loading(true))
|
||||||
|
|
||||||
val history = database.getHistory()
|
val history = database.getHistory()
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
Resource.Success(
|
Resource.Success(
|
||||||
data = history.map {
|
data = history.map {
|
||||||
val book = database.findBookById(it.bookId)
|
historyMapper.toHistory(
|
||||||
historyMapper.toHistory(it, bookMapper.toBook(book))
|
it
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package ua.acclorite.book_story.domain.model
|
package ua.acclorite.book_story.domain.model
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
@ -17,5 +17,5 @@ data class Book(
|
||||||
val filePath: String,
|
val filePath: String,
|
||||||
val lastOpened: Long?,
|
val lastOpened: Long?,
|
||||||
val category: Category,
|
val category: Category,
|
||||||
val coverImage: Bitmap?
|
val coverImage: ImageBitmap?
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package ua.acclorite.book_story.domain.model
|
package ua.acclorite.book_story.domain.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
enum class Category {
|
enum class Category {
|
||||||
READING, ALREADY_READ, PLANNING, DROPPED
|
READING, ALREADY_READ, PLANNING, DROPPED
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,6 @@ import androidx.compose.runtime.Immutable
|
||||||
@Immutable
|
@Immutable
|
||||||
data class History(
|
data class History(
|
||||||
val id: Int?,
|
val id: Int?,
|
||||||
val book: Book,
|
val bookId: Int,
|
||||||
val time: Long
|
val time: Long
|
||||||
)
|
)
|
||||||
|
|
@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class NavigationBarItem(
|
data class NavigationItem(
|
||||||
val title: String,
|
val title: String,
|
||||||
val selectedIcon: Painter,
|
val selectedIcon: Painter,
|
||||||
val unselectedIcon: Painter
|
val unselectedIcon: Painter
|
||||||
|
|
@ -1,6 +1,17 @@
|
||||||
package ua.acclorite.book_story.domain.model
|
package ua.acclorite.book_story.domain.model
|
||||||
|
|
||||||
sealed class NullableBook(val book: Pair<Book, Boolean>?, val fileName: String?) {
|
import androidx.compose.runtime.Immutable
|
||||||
class NotNull(book: Pair<Book, Boolean>) : NullableBook(book, null)
|
import ua.acclorite.book_story.util.UIText
|
||||||
class Null(fileName: String) : NullableBook(null, fileName)
|
|
||||||
|
@Immutable
|
||||||
|
sealed class NullableBook(
|
||||||
|
val book: Pair<Book, Boolean>?,
|
||||||
|
val fileName: String?,
|
||||||
|
val message: UIText?
|
||||||
|
) {
|
||||||
|
class NotNull(book: Pair<Book, Boolean>) : NullableBook(book, null, null)
|
||||||
|
class Null(
|
||||||
|
fileName: String,
|
||||||
|
message: UIText?
|
||||||
|
) : NullableBook(null, fileName, message)
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +20,10 @@ interface BookRepository {
|
||||||
query: String
|
query: String
|
||||||
): List<Book>
|
): List<Book>
|
||||||
|
|
||||||
|
suspend fun getBooksById(
|
||||||
|
ids: List<Int>
|
||||||
|
): List<Book>
|
||||||
|
|
||||||
suspend fun findBook(
|
suspend fun findBook(
|
||||||
id: Int
|
id: Int
|
||||||
): BookEntity
|
): BookEntity
|
||||||
|
|
@ -50,7 +54,7 @@ interface BookRepository {
|
||||||
|
|
||||||
suspend fun getBookTextFromFile(file: File): Flow<Resource<List<StringWithId>>>
|
suspend fun getBookTextFromFile(file: File): Flow<Resource<List<StringWithId>>>
|
||||||
|
|
||||||
suspend fun getBooksFromFiles(files: List<File>): Flow<Resource<List<NullableBook>>>
|
suspend fun getBooksFromFiles(files: List<File>): List<NullableBook>
|
||||||
|
|
||||||
|
|
||||||
suspend fun insertHistory(history: List<History>)
|
suspend fun insertHistory(history: List<History>)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Int>): List<Book> {
|
||||||
|
return repository.getBooksById(ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
package ua.acclorite.book_story.domain.use_case
|
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.model.NullableBook
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||||
import ua.acclorite.book_story.util.Resource
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -11,7 +9,7 @@ class GetBooksFromFiles @Inject constructor(
|
||||||
private val repository: BookRepository
|
private val repository: BookRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun execute(files: List<File>): Flow<Resource<List<NullableBook>>> {
|
suspend fun execute(files: List<File>): List<NullableBook> {
|
||||||
return repository.getBooksFromFiles(files)
|
return repository.getBooksFromFiles(files)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -26,6 +26,14 @@ import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.ui.DefaultTransition
|
import ua.acclorite.book_story.ui.DefaultTransition
|
||||||
import ua.acclorite.book_story.ui.elevation
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimatedTopAppBar(
|
fun AnimatedTopAppBar(
|
||||||
|
|
@ -50,6 +58,7 @@ fun AnimatedTopAppBar(
|
||||||
content3Title: @Composable () -> Unit = {},
|
content3Title: @Composable () -> Unit = {},
|
||||||
content3Actions: @Composable RowScope.() -> Unit = {}
|
content3Actions: @Composable RowScope.() -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
//todo fix lags.
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
if ((scrollBehavior != null || isTopBarScrolled != null) && scrolledContainerColor != null) {
|
if ((scrollBehavior != null || isTopBarScrolled != null) && scrolledContainerColor != null) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category Title. Used to name a category.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryTitle(
|
fun CategoryTitle(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Checkbox. Has a Circle shape.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomCheckbox(selected: Boolean) {
|
fun CustomCheckbox(selected: Boolean) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Dropdown Menu Item. Used in Dropdown.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomDropDownMenuItem(
|
fun CustomDropDownMenuItem(
|
||||||
leadingIcon: ImageVector,
|
leadingIcon: ImageVector,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,9 +21,12 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
import ua.acclorite.book_story.presentation.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
|
|
||||||
|
/**
|
||||||
|
* More drop down. Navigates to Settings, About and Help screens.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun MoreDropDown(navigator: Navigator) {
|
fun MoreDropDown(navigator: Navigator) {
|
||||||
var showDropDown by remember { mutableStateOf(false) }
|
var showDropDown by remember { mutableStateOf(false) }
|
||||||
|
|
@ -58,7 +61,7 @@ fun MoreDropDown(navigator: Navigator) {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.navigate(Screen.SETTINGS)
|
navigator.navigate(Screen.SETTINGS, false)
|
||||||
showDropDown = false
|
showDropDown = false
|
||||||
},
|
},
|
||||||
contentPadding = PaddingValues(start = startPadding, end = endPadding)
|
contentPadding = PaddingValues(start = startPadding, end = endPadding)
|
||||||
|
|
@ -72,7 +75,7 @@ fun MoreDropDown(navigator: Navigator) {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.navigate(Screen.HELP)
|
navigator.navigate(Screen.HELP, false)
|
||||||
showDropDown = false
|
showDropDown = false
|
||||||
},
|
},
|
||||||
contentPadding = PaddingValues(start = startPadding, end = endPadding)
|
contentPadding = PaddingValues(start = startPadding, end = endPadding)
|
||||||
|
|
@ -86,7 +89,7 @@ fun MoreDropDown(navigator: Navigator) {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.navigate(Screen.ABOUT)
|
navigator.navigate(Screen.ABOUT, false)
|
||||||
showDropDown = false
|
showDropDown = false
|
||||||
},
|
},
|
||||||
contentPadding = PaddingValues(start = startPadding, end = endPadding)
|
contentPadding = PaddingValues(start = startPadding, end = endPadding)
|
||||||
|
|
|
||||||
|
|
@ -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.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationBar
|
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.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.NavigationBarItem
|
import ua.acclorite.book_story.domain.model.NavigationItem
|
||||||
import ua.acclorite.book_story.presentation.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
import ua.acclorite.book_story.presentation.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -37,34 +37,34 @@ fun BottomNavigationBar(
|
||||||
containerColor = MaterialTheme.elevation()
|
containerColor = MaterialTheme.elevation()
|
||||||
) {
|
) {
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
item = NavigationBarItem(
|
item = NavigationItem(
|
||||||
stringResource(id = R.string.library_screen),
|
stringResource(id = R.string.library_screen),
|
||||||
selectedIcon = painterResource(id = R.drawable.library_screen_filled),
|
selectedIcon = painterResource(id = R.drawable.library_screen_filled),
|
||||||
unselectedIcon = painterResource(id = R.drawable.library_screen_outlined)
|
unselectedIcon = painterResource(id = R.drawable.library_screen_outlined)
|
||||||
),
|
),
|
||||||
isSelected = currentScreen == Screen.LIBRARY
|
isSelected = currentScreen == Screen.LIBRARY
|
||||||
) {
|
) {
|
||||||
navigator.navigate(Screen.LIBRARY)
|
navigator.navigate(Screen.LIBRARY, false)
|
||||||
}
|
}
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
item = NavigationBarItem(
|
item = NavigationItem(
|
||||||
stringResource(id = R.string.history_screen),
|
stringResource(id = R.string.history_screen),
|
||||||
selectedIcon = painterResource(id = R.drawable.history_screen_filled),
|
selectedIcon = painterResource(id = R.drawable.history_screen_filled),
|
||||||
unselectedIcon = painterResource(id = R.drawable.history_screen_outlined)
|
unselectedIcon = painterResource(id = R.drawable.history_screen_outlined)
|
||||||
),
|
),
|
||||||
isSelected = currentScreen == Screen.HISTORY
|
isSelected = currentScreen == Screen.HISTORY
|
||||||
) {
|
) {
|
||||||
navigator.navigate(Screen.HISTORY)
|
navigator.navigate(Screen.HISTORY, false)
|
||||||
}
|
}
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
item = NavigationBarItem(
|
item = NavigationItem(
|
||||||
stringResource(id = R.string.browse_screen),
|
stringResource(id = R.string.browse_screen),
|
||||||
selectedIcon = painterResource(id = R.drawable.browse_screen_filled),
|
selectedIcon = painterResource(id = R.drawable.browse_screen_filled),
|
||||||
unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined)
|
unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined)
|
||||||
),
|
),
|
||||||
isSelected = currentScreen == Screen.BROWSE
|
isSelected = currentScreen == Screen.BROWSE
|
||||||
) {
|
) {
|
||||||
navigator.navigate(Screen.BROWSE)
|
navigator.navigate(Screen.BROWSE, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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.RowScope
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
|
@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.domain.model.NavigationBarItem
|
import ua.acclorite.book_story.domain.model.NavigationItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bottom Navigation Bar Item, uses default [NavigationBarItem].
|
* Bottom Navigation Bar Item, uses default [NavigationBarItem].
|
||||||
|
|
@ -19,7 +19,7 @@ import ua.acclorite.book_story.domain.model.NavigationBarItem
|
||||||
@Composable
|
@Composable
|
||||||
fun RowScope.BottomNavigationBarItem(
|
fun RowScope.BottomNavigationBarItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
item: NavigationBarItem,
|
item: NavigationItem,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
@ -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.background
|
||||||
import androidx.compose.foundation.layout.Column
|
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.ElevationDefaults
|
||||||
import ua.acclorite.book_story.ui.elevation
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomDialogWithContent(
|
fun CustomDialogWithContent(
|
||||||
|
|
@ -54,7 +71,7 @@ fun CustomDialogWithContent(
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { onDismiss() },
|
onDismissRequest = { onDismiss() },
|
||||||
properties = properties,
|
properties = properties
|
||||||
) {
|
) {
|
||||||
(LocalView.current.parent as DialogWindowProvider).window.setDimAmount(
|
(LocalView.current.parent as DialogWindowProvider).window.setDimAmount(
|
||||||
backgroundTransparency
|
backgroundTransparency
|
||||||
|
|
@ -118,8 +135,8 @@ fun CustomDialogWithContent(
|
||||||
if (withDivider) {
|
if (withDivider) {
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
Divider(
|
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))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -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.background
|
||||||
import androidx.compose.foundation.layout.Column
|
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.ElevationDefaults
|
||||||
import ua.acclorite.book_story.ui.elevation
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomDialogWithLazyColumn(
|
fun CustomDialogWithLazyColumn(
|
||||||
|
|
@ -55,7 +73,7 @@ fun CustomDialogWithLazyColumn(
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { onDismiss() },
|
onDismissRequest = { onDismiss() },
|
||||||
properties = properties,
|
properties = properties
|
||||||
) {
|
) {
|
||||||
(LocalView.current.parent as DialogWindowProvider).window.setDimAmount(
|
(LocalView.current.parent as DialogWindowProvider).window.setDimAmount(
|
||||||
backgroundTransparency
|
backgroundTransparency
|
||||||
|
|
@ -119,8 +137,8 @@ fun CustomDialogWithLazyColumn(
|
||||||
if (withDivider) {
|
if (withDivider) {
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
Divider(
|
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))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -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.clickable
|
||||||
import androidx.compose.foundation.layout.Row
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selectable dialog item. Perfectly combines with [CustomDialogWithLazyColumn].
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectableDialogItem(selected: Boolean, title: String, onClick: () -> Unit) {
|
fun SelectableDialogItem(selected: Boolean, title: String, onClick: () -> Unit) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
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
|
@Composable
|
||||||
fun IsEmpty(
|
fun IsEmpty(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
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
|
@Composable
|
||||||
fun IsError(
|
fun IsError(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
package ua.acclorite.book_story.presentation.data
|
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 {
|
fun String.removeTrailingZero(): String {
|
||||||
if (!this.contains('.'))
|
if (!this.contains('.'))
|
||||||
return this
|
return this
|
||||||
|
|
@ -9,3 +13,18 @@ fun String.removeTrailingZero(): String {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Double.removeDigits(digits: Int) = "%.${digits}f".format(this).replace(",", ".")
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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.ChangeLanguage
|
||||||
import ua.acclorite.book_story.domain.use_case.GetDatastore
|
import ua.acclorite.book_story.domain.use_case.GetDatastore
|
||||||
import ua.acclorite.book_story.domain.use_case.SetDatastore
|
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.DarkTheme
|
||||||
import ua.acclorite.book_story.ui.Theme
|
import ua.acclorite.book_story.ui.Theme
|
||||||
import ua.acclorite.book_story.ui.toDarkTheme
|
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(
|
val isDataReady = combine(
|
||||||
language, theme, darkTheme, showStartScreen, backgroundColor,
|
language,
|
||||||
fontColor, fontFamily, isItalic, fontSize, lineHeight,
|
theme,
|
||||||
paragraphHeight, paragraphIndentation
|
darkTheme,
|
||||||
|
showStartScreen,
|
||||||
|
backgroundColor,
|
||||||
|
fontColor,
|
||||||
|
fontFamily,
|
||||||
|
isItalic,
|
||||||
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
|
paragraphHeight,
|
||||||
|
paragraphIndentation
|
||||||
) { values ->
|
) { values ->
|
||||||
values.all { it != null }
|
values.all { it != null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isReady = combine(
|
||||||
|
isViewModelsReady,
|
||||||
|
isDataReady
|
||||||
|
) { values ->
|
||||||
|
values.all { it }
|
||||||
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
isDataReady.collect { bool ->
|
isReady.collect { bool ->
|
||||||
if (bool) {
|
if (bool) {
|
||||||
_isReady.value = true
|
_isReady.update {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
return@collect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ua.acclorite.book_story.presentation
|
package ua.acclorite.book_story.presentation.data
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.activity.ComponentActivity
|
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 CURRENT_SCREEN = "current_screen"
|
||||||
private const val BACKSTACK = "back_stack"
|
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.
|
* 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,
|
SETTINGS,
|
||||||
GENERAL_SETTINGS,
|
GENERAL_SETTINGS,
|
||||||
APPEARANCE_SETTINGS,
|
APPEARANCE_SETTINGS,
|
||||||
|
READER_SETTINGS,
|
||||||
|
|
||||||
ABOUT,
|
ABOUT,
|
||||||
HELP
|
HELP,
|
||||||
|
START
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Argument(
|
data class Argument(
|
||||||
|
|
@ -72,6 +75,7 @@ class Navigator @AssistedInject constructor(
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val currentScreen = savedStateHandle.getStateFlow(CURRENT_SCREEN, startScreen)
|
private val currentScreen = savedStateHandle.getStateFlow(CURRENT_SCREEN, startScreen)
|
||||||
|
private val useBackAnimation = savedStateHandle.getStateFlow(USE_BACK_ANIM, false)
|
||||||
private val backStack = savedStateHandle.getStateFlow(BACKSTACK, mutableListOf<Screen>())
|
private val backStack = savedStateHandle.getStateFlow(BACKSTACK, mutableListOf<Screen>())
|
||||||
private val arguments = mutableStateListOf<Argument>()
|
private val arguments = mutableStateListOf<Argument>()
|
||||||
|
|
||||||
|
|
@ -104,7 +108,7 @@ class Navigator @AssistedInject constructor(
|
||||||
arguments.removeIf { it.key == key }
|
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) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
backStack.value.add(currentScreen.value)
|
backStack.value.add(currentScreen.value)
|
||||||
|
|
||||||
|
|
@ -112,10 +116,11 @@ class Navigator @AssistedInject constructor(
|
||||||
putArgument(it)
|
putArgument(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
savedStateHandle[USE_BACK_ANIM] = useBackAnimation
|
||||||
savedStateHandle[CURRENT_SCREEN] = screen
|
savedStateHandle[CURRENT_SCREEN] = screen
|
||||||
}
|
}
|
||||||
|
|
||||||
fun navigateWithoutBackStack(screen: Screen, vararg args: Argument) =
|
fun navigateWithoutBackStack(screen: Screen, useBackAnimation: Boolean, vararg args: Argument) =
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
if (backStack.value.last() == screen) {
|
if (backStack.value.last() == screen) {
|
||||||
backStack.value.removeLast()
|
backStack.value.removeLast()
|
||||||
|
|
@ -125,11 +130,14 @@ class Navigator @AssistedInject constructor(
|
||||||
putArgument(it)
|
putArgument(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
savedStateHandle[USE_BACK_ANIM] = useBackAnimation
|
||||||
savedStateHandle[CURRENT_SCREEN] = screen
|
savedStateHandle[CURRENT_SCREEN] = screen
|
||||||
}
|
}
|
||||||
|
|
||||||
fun navigateBack() = viewModelScope.launch(Dispatchers.Default) {
|
fun navigateBack(useBackAnimation: Boolean = true) =
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
if (canGoBack()) {
|
if (canGoBack()) {
|
||||||
|
savedStateHandle[USE_BACK_ANIM] = useBackAnimation
|
||||||
savedStateHandle[CURRENT_SCREEN] = backStack.value.last()
|
savedStateHandle[CURRENT_SCREEN] = backStack.value.last()
|
||||||
backStack.value.removeLast()
|
backStack.value.removeLast()
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +156,9 @@ class Navigator @AssistedInject constructor(
|
||||||
*
|
*
|
||||||
* @param screen The [Screen] that represents [content].
|
* @param screen The [Screen] that represents [content].
|
||||||
* @param enterAnim Enter Animation.
|
* @param enterAnim Enter Animation.
|
||||||
|
* @param backEnterAnim Enter Animation for navigating back.
|
||||||
* @param exitAnim Exit Animation.
|
* @param exitAnim Exit Animation.
|
||||||
|
* @param backExitAnim Exit Animation for navigating back.
|
||||||
* @param content The Screen content to show when [Navigator.currentScreen] equals [screen].
|
* @param content The Screen content to show when [Navigator.currentScreen] equals [screen].
|
||||||
*/
|
*/
|
||||||
@SuppressLint("ComposableNaming")
|
@SuppressLint("ComposableNaming")
|
||||||
|
|
@ -156,14 +166,16 @@ class Navigator @AssistedInject constructor(
|
||||||
fun composable(
|
fun composable(
|
||||||
screen: Screen,
|
screen: Screen,
|
||||||
enterAnim: EnterTransition = Transitions.FadeTransitionIn,
|
enterAnim: EnterTransition = Transitions.FadeTransitionIn,
|
||||||
|
backEnterAnim: EnterTransition = Transitions.BackSlidingTransitionIn,
|
||||||
exitAnim: ExitTransition = Transitions.FadeTransitionOut,
|
exitAnim: ExitTransition = Transitions.FadeTransitionOut,
|
||||||
|
backExitAnim: ExitTransition = Transitions.BackSlidingTransitionOut,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = getCurrentScreen().collectAsState().value == screen,
|
visible = getCurrentScreen().collectAsState().value == screen,
|
||||||
enter = enterAnim,
|
enter = if (!useBackAnimation.collectAsState().value) enterAnim else backEnterAnim,
|
||||||
exit = exitAnim,
|
exit = if (!useBackAnimation.collectAsState().value) exitAnim else backExitAnim,
|
||||||
label = screen.toString()
|
label = ""
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|
@ -21,10 +21,15 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
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.Done
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
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.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||||
|
|
@ -32,6 +37,7 @@ import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -43,32 +49,33 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import dagger.hilt.android.lifecycle.withCreationCallback
|
import dagger.hilt.android.lifecycle.withCreationCallback
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
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.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.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.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.BookInfoInfoSection
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoMoreDropDown
|
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.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.BookInfoEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
|
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.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.history.data.HistoryViewModel
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
|
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.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.ui.elevation
|
||||||
import ua.acclorite.book_story.util.Constants
|
import ua.acclorite.book_story.util.Constants
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoScreen(
|
fun BookInfoScreen(
|
||||||
libraryViewModel: LibraryViewModel = hiltViewModel(),
|
libraryViewModel: LibraryViewModel,
|
||||||
browseViewModel: BrowseViewModel = hiltViewModel(),
|
browseViewModel: BrowseViewModel,
|
||||||
historyViewModel: HistoryViewModel = hiltViewModel(),
|
historyViewModel: HistoryViewModel,
|
||||||
navigator: Navigator
|
navigator: Navigator
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
@ -104,22 +111,29 @@ fun BookInfoScreen(
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
val listState = rememberLazyListState()
|
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 } }
|
val firstVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.init(
|
viewModel.init(
|
||||||
navigator,
|
navigator
|
||||||
failure = {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(
|
|
||||||
R.string.file_not_found,
|
|
||||||
it.takeLast(40)
|
|
||||||
),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,8 +167,9 @@ fun BookInfoScreen(
|
||||||
Scaffold(
|
Scaffold(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||||
.windowInsetsPadding(WindowInsets.navigationBars),
|
.pullRefresh(refreshState)
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
topBar = {
|
topBar = {
|
||||||
AnimatedTopAppBar(
|
AnimatedTopAppBar(
|
||||||
|
|
@ -164,15 +179,7 @@ fun BookInfoScreen(
|
||||||
isTopBarScrolled = null,
|
isTopBarScrolled = null,
|
||||||
|
|
||||||
content1NavigationIcon = {
|
content1NavigationIcon = {
|
||||||
IconButton(onClick = {
|
GoBackButton(navigator = navigator)
|
||||||
navigator.navigateBack()
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
|
|
||||||
contentDescription = "Go back",
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
content1Title = {
|
content1Title = {
|
||||||
DefaultTransition(visible = firstVisibleItemIndex > 0) {
|
DefaultTransition(visible = firstVisibleItemIndex > 0) {
|
||||||
|
|
@ -186,11 +193,37 @@ fun BookInfoScreen(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content1Actions = {
|
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 {
|
Box {
|
||||||
DefaultTransition(
|
DefaultTransition(
|
||||||
visible = !state.editTitle
|
visible = !state.editTitle
|
||||||
) {
|
) {
|
||||||
BookInfoMoreDropDown(viewModel = viewModel)
|
BookInfoMoreDropDown(
|
||||||
|
viewModel = viewModel,
|
||||||
|
snackbarState = snackbarState
|
||||||
|
)
|
||||||
}
|
}
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
androidx.compose.animation.AnimatedVisibility(
|
||||||
visible = state.editTitle,
|
visible = state.editTitle,
|
||||||
|
|
@ -230,6 +263,12 @@ fun BookInfoScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
CustomSnackbar(
|
||||||
|
modifier = Modifier.padding(bottom = 70.dp),
|
||||||
|
snackbarState = snackbarState
|
||||||
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -246,7 +285,7 @@ fun BookInfoScreen(
|
||||||
if (state.book.coverImage != null) {
|
if (state.book.coverImage != null) {
|
||||||
BookInfoBackground(
|
BookInfoBackground(
|
||||||
height = paddingValues.calculateTopPadding() + 12.dp + 195.dp,
|
height = paddingValues.calculateTopPadding() + 12.dp + 195.dp,
|
||||||
image = state.book.coverImage!!.asImageBitmap()
|
image = state.book.coverImage!!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,19 +310,18 @@ fun BookInfoScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.book.file != null) {
|
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.navigate(
|
navigator.navigate(
|
||||||
Screen.READER, Argument(
|
Screen.READER, false, Argument(
|
||||||
"book", state.book
|
"book", state.book
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
shape = MaterialTheme.shapes.large,
|
|
||||||
elevation = FloatingActionButtonDefaults.elevation(),
|
elevation = FloatingActionButtonDefaults.elevation(),
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
|
@ -292,7 +330,7 @@ fun BookInfoScreen(
|
||||||
Spacer(modifier = Modifier.width(13.dp))
|
Spacer(modifier = Modifier.width(13.dp))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.PlayArrow,
|
imageVector = Icons.Filled.PlayArrow,
|
||||||
contentDescription = "Continue reading",
|
contentDescription = stringResource(id = R.string.continue_reading_content_desc),
|
||||||
Modifier.size(24.dp)
|
Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
|
|
@ -312,7 +350,14 @@ fun BookInfoScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
PullRefreshIndicator(
|
||||||
|
state.isRefreshing,
|
||||||
|
refreshState,
|
||||||
|
Modifier.align(Alignment.TopCenter),
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.inverseSurface,
|
||||||
|
contentColor = MaterialTheme.colorScheme.inverseOnSurface
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -15,6 +15,9 @@ import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Background of the BookInfoScreen.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoBackground(height: Dp, image: ImageBitmap) {
|
fun BookInfoBackground(height: Dp, image: ImageBitmap) {
|
||||||
val background = MaterialTheme.colorScheme.background
|
val background = MaterialTheme.colorScheme.background
|
||||||
|
|
@ -34,7 +37,7 @@ fun BookInfoBackground(height: Dp, image: ImageBitmap) {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.blur(2.dp),
|
.blur(3.dp),
|
||||||
alpha = 0.3f,
|
alpha = 0.3f,
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,15 @@ import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Description section.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoDescriptionSection(book: Book) {
|
fun BookInfoDescriptionSection(book: Book) {
|
||||||
SelectionContainer {
|
SelectionContainer {
|
||||||
Text(
|
Text(
|
||||||
book.description
|
if (book.description?.isNotBlank() == true) book.description
|
||||||
?: stringResource(id = R.string.error_no_description),
|
else stringResource(id = R.string.error_no_description),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
|
|
|
||||||
|
|
@ -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.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.text.TextStyle
|
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.presentation.screens.book_info.data.BookInfoViewModel
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Info section.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
|
fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
|
||||||
|
|
@ -84,7 +86,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
|
||||||
|
|
||||||
if (book.coverImage != null) {
|
if (book.coverImage != null) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = book.coverImage.asImageBitmap(),
|
bitmap = book.coverImage,
|
||||||
contentDescription = "Cover",
|
contentDescription = "Cover",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -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.BookInfoEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Book info more dropdown. Has three options: Delete, Move and Details.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoMoreDropDown(viewModel: BookInfoViewModel) {
|
fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHostState) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
|
|
@ -64,6 +68,7 @@ fun BookInfoMoreDropDown(viewModel: BookInfoViewModel) {
|
||||||
leadingIcon = Icons.Outlined.Info,
|
leadingIcon = Icons.Outlined.Info,
|
||||||
text = stringResource(id = R.string.details_book)
|
text = stringResource(id = R.string.details_book)
|
||||||
) {
|
) {
|
||||||
|
snackbarState.currentSnackbarData?.dismiss()
|
||||||
viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
|
viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
|
||||||
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown)
|
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.removeDigits
|
||||||
import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistic section.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoStatisticSection(book: Book) {
|
fun BookInfoStatisticSection(book: Book) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -41,16 +44,16 @@ fun BookInfoStatisticSection(book: Book) {
|
||||||
|
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
book.progress.coerceAtLeast(0.01f),
|
book.progress.coerceAtLeast(0.01f),
|
||||||
strokeCap = StrokeCap.Round,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
color = MaterialTheme.colorScheme.primary,
|
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))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(
|
text = if (book.progress < 1f) stringResource(
|
||||||
id = R.string.you_read_query,
|
id = R.string.you_read_query,
|
||||||
(book.progress * 100)
|
(book.progress * 100)
|
||||||
.toDouble()
|
.toDouble()
|
||||||
|
|
@ -59,7 +62,7 @@ fun BookInfoStatisticSection(book: Book) {
|
||||||
) + " " + stringResource(
|
) + " " + stringResource(
|
||||||
if (book.progress > 0.2f) R.string.read_keep
|
if (book.progress > 0.2f) R.string.read_keep
|
||||||
else R.string.read_more
|
else R.string.read_more
|
||||||
),
|
) else stringResource(id = R.string.read_done),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|
|
||||||
|
|
@ -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 android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
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.ElevationDefaults
|
||||||
import ua.acclorite.book_story.ui.elevation
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoChangeCoverBottomSheet(
|
fun BookInfoChangeCoverBottomSheet(
|
||||||
|
|
@ -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.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change cover bottom sheet item.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoChangeCoverBottomSheetItem(
|
fun BookInfoChangeCoverBottomSheetItem(
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
|
|
@ -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 android.widget.Toast
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
|
@ -25,6 +25,9 @@ import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Details bottom sheet. Displays name, path, last opened and size of the file.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoDetailsBottomSheet(
|
fun BookInfoDetailsBottomSheet(
|
||||||
|
|
@ -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.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Details bottom sheet item.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoDetailsBottomSheetItem(
|
fun BookInfoDetailsBottomSheetItem(
|
||||||
|
|
@ -51,7 +54,6 @@ fun BookInfoDetailsBottomSheetItem(
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
description,
|
description,
|
||||||
maxLines = 3,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
|
@ -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 android.widget.Toast
|
||||||
import androidx.compose.material.icons.Icons
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.Navigator
|
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
|
||||||
import ua.acclorite.book_story.presentation.components.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.BookInfoEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
|
||||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
|
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.LibraryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete dialog. Deletes current book.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoDeleteDialog(
|
fun BookInfoDeleteDialog(
|
||||||
libraryViewModel: LibraryViewModel,
|
libraryViewModel: LibraryViewModel,
|
||||||
|
|
@ -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 android.widget.Toast
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
|
@ -13,14 +13,17 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.presentation.Navigator
|
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn
|
||||||
import ua.acclorite.book_story.presentation.components.CustomDialogWithLazyColumn
|
import ua.acclorite.book_story.presentation.components.custom_dialog.SelectableDialogItem
|
||||||
import ua.acclorite.book_story.presentation.components.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.BookInfoEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
|
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.LibraryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move dialog. Moves current book to the selected category.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoMoveDialog(
|
fun BookInfoMoveDialog(
|
||||||
libraryViewModel: LibraryViewModel,
|
libraryViewModel: LibraryViewModel,
|
||||||
|
|
@ -2,9 +2,10 @@ package ua.acclorite.book_story.presentation.screens.book_info.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
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 {
|
sealed class BookInfoEvent {
|
||||||
data object OnShowHideChangeCoverBottomSheet : BookInfoEvent()
|
data object OnShowHideChangeCoverBottomSheet : BookInfoEvent()
|
||||||
|
|
@ -33,4 +34,18 @@ sealed class BookInfoEvent {
|
||||||
val text: String,
|
val text: String,
|
||||||
val success: () -> Unit
|
val success: () -> Unit
|
||||||
) : BookInfoEvent()
|
) : 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()
|
||||||
}
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import ua.acclorite.book_story.domain.model.Category
|
||||||
@Immutable
|
@Immutable
|
||||||
data class BookInfoState(
|
data class BookInfoState(
|
||||||
val book: Book,
|
val book: Book,
|
||||||
|
val isRefreshing: Boolean = false,
|
||||||
|
|
||||||
val editTitle: Boolean = false,
|
val editTitle: Boolean = false,
|
||||||
val hasFocused: Boolean = false,
|
val hasFocused: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import android.content.ClipboardManager
|
||||||
import android.content.Context.CLIPBOARD_SERVICE
|
import android.content.Context.CLIPBOARD_SERVICE
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.compose.material3.SnackbarResult
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
|
|
@ -12,28 +14,36 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
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.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
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.DeleteBooks
|
||||||
|
import ua.acclorite.book_story.domain.use_case.GetBookFromFile
|
||||||
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
||||||
import ua.acclorite.book_story.presentation.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
import ua.acclorite.book_story.presentation.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel(assistedFactory = BookInfoViewModel.Factory::class)
|
@HiltViewModel(assistedFactory = BookInfoViewModel.Factory::class)
|
||||||
class BookInfoViewModel @AssistedInject constructor(
|
class BookInfoViewModel @AssistedInject constructor(
|
||||||
@Assisted book: Book,
|
@Assisted book: Book,
|
||||||
private val updateBooks: UpdateBooks,
|
private val updateBooks: UpdateBooks,
|
||||||
private val deleteBooks: DeleteBooks
|
private val deleteBooks: DeleteBooks,
|
||||||
|
private val getBookFromFile: GetBookFromFile
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _state = MutableStateFlow(BookInfoState(book))
|
private val _state = MutableStateFlow(BookInfoState(book))
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
var job: Job? = null
|
||||||
|
|
||||||
fun onEvent(event: BookInfoEvent) {
|
fun onEvent(event: BookInfoEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is BookInfoEvent.OnShowHideChangeCoverBottomSheet -> {
|
is BookInfoEvent.OnShowHideChangeCoverBottomSheet -> {
|
||||||
|
|
@ -47,9 +57,11 @@ class BookInfoViewModel @AssistedInject constructor(
|
||||||
is BookInfoEvent.OnChangeCover -> {
|
is BookInfoEvent.OnChangeCover -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val image = event.context.contentResolver?.openInputStream(event.uri)?.use {
|
val image = event.context.contentResolver?.openInputStream(event.uri)?.use {
|
||||||
BitmapFactory.decodeStream(it)
|
BitmapFactory.decodeStream(it).asImageBitmap()
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
|
|
||||||
|
image.prepareToDraw()
|
||||||
|
|
||||||
updateBooks.execute(
|
updateBooks.execute(
|
||||||
listOf(
|
listOf(
|
||||||
_state.value.book.copy(coverImage = image)
|
_state.value.book.copy(coverImage = image)
|
||||||
|
|
@ -221,7 +233,7 @@ class BookInfoViewModel @AssistedInject constructor(
|
||||||
it != _state.value.selectedCategory
|
it != _state.value.selectedCategory
|
||||||
}.size - 1
|
}.size - 1
|
||||||
)
|
)
|
||||||
event.navigator.navigate(Screen.LIBRARY)
|
event.navigator.navigate(Screen.LIBRARY, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,10 +255,139 @@ class BookInfoViewModel @AssistedInject constructor(
|
||||||
event.success()
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(navigator: Navigator, failure: (String) -> Unit) {
|
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) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val book = navigator.retrieveArgument("book") as? Book
|
val book = navigator.retrieveArgument("book") as? Book
|
||||||
|
|
||||||
|
|
@ -255,10 +396,6 @@ class BookInfoViewModel @AssistedInject constructor(
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
if (book.file == null) {
|
|
||||||
failure(book.filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
BookInfoState(book = book)
|
BookInfoState(book = book)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,24 +53,24 @@ import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import ua.acclorite.book_story.R
|
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.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.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.BrowseFileItem
|
||||||
import ua.acclorite.book_story.presentation.screens.browse.components.BrowseStoragePermissionDialog
|
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.BrowseEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
|
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.DefaultTransition
|
||||||
import ua.acclorite.book_story.ui.Transitions
|
import ua.acclorite.book_story.ui.Transitions
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
@ -83,7 +83,8 @@ import ua.acclorite.book_story.ui.elevation
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseScreen(
|
fun BrowseScreen(
|
||||||
viewModel: BrowseViewModel = hiltViewModel(),
|
viewModel: BrowseViewModel,
|
||||||
|
libraryViewModel: LibraryViewModel,
|
||||||
navigator: Navigator
|
navigator: Navigator
|
||||||
) {
|
) {
|
||||||
val permissionState = rememberPermissionState(
|
val permissionState = rememberPermissionState(
|
||||||
|
|
@ -127,7 +128,11 @@ fun BrowseScreen(
|
||||||
BrowseStoragePermissionDialog(viewModel, permissionState)
|
BrowseStoragePermissionDialog(viewModel, permissionState)
|
||||||
}
|
}
|
||||||
if (state.showAddingDialog) {
|
if (state.showAddingDialog) {
|
||||||
BrowseAddingDialog(viewModel = viewModel, navigator = navigator)
|
BrowseAddingDialog(
|
||||||
|
viewModel = viewModel,
|
||||||
|
navigator = navigator,
|
||||||
|
libraryViewModel = libraryViewModel
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -270,13 +275,14 @@ fun BrowseScreen(
|
||||||
columns = GridCells.Adaptive(170.dp),
|
columns = GridCells.Adaptive(170.dp),
|
||||||
contentPadding = PaddingValues(12.dp)
|
contentPadding = PaddingValues(12.dp)
|
||||||
) {
|
) {
|
||||||
items(state.selectableFiles, key = { it.first.path }) { selectableFile ->
|
items(
|
||||||
|
state.selectableFiles,
|
||||||
|
key = { it.first.path }
|
||||||
|
) { selectableFile ->
|
||||||
BrowseFileItem(
|
BrowseFileItem(
|
||||||
file = selectableFile,
|
file = selectableFile,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.animateItemPlacement(
|
.animateItemPlacement(),
|
||||||
animationSpec = tween(300)
|
|
||||||
),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile))
|
viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile))
|
||||||
}
|
}
|
||||||
|
|
@ -317,7 +323,7 @@ fun BrowseScreen(
|
||||||
message = stringResource(id = R.string.browse_empty),
|
message = stringResource(id = R.string.browse_empty),
|
||||||
icon = painterResource(id = R.drawable.empty_browse),
|
icon = painterResource(id = R.drawable.empty_browse),
|
||||||
actionTitle = stringResource(id = R.string.get_help),
|
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,
|
state.isRefreshing,
|
||||||
refreshState,
|
refreshState,
|
||||||
Modifier.align(Alignment.TopCenter),
|
Modifier.align(Alignment.TopCenter),
|
||||||
backgroundColor = MaterialTheme.elevation(),
|
backgroundColor = MaterialTheme.colorScheme.inverseSurface,
|
||||||
contentColor = MaterialTheme.colorScheme.primary,
|
contentColor = MaterialTheme.colorScheme.inverseOnSurface
|
||||||
scale = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -343,7 +348,7 @@ fun BrowseScreen(
|
||||||
return@BackHandler
|
return@BackHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.navigate(Screen.LIBRARY)
|
navigator.navigate(Screen.LIBRARY, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browse list element item. Can be selected.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseFileItem(file: Pair<File, Boolean>, modifier: Modifier, onClick: () -> Unit) {
|
fun BrowseFileItem(file: Pair<File, Boolean>, modifier: Modifier, onClick: () -> Unit) {
|
||||||
val fileExtension: String = file.first.name.substringAfterLast(".", "")
|
val fileExtension: String = file.first.name.substringAfterLast(".", "")
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,13 @@ import androidx.compose.ui.res.stringResource
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.PermissionState
|
import com.google.accompanist.permissions.PermissionState
|
||||||
import ua.acclorite.book_story.R
|
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.BrowseEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
|
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage permission dialog.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseStoragePermissionDialog(viewModel: BrowseViewModel, permissionState: PermissionState) {
|
fun BrowseStoragePermissionDialog(viewModel: BrowseViewModel, permissionState: PermissionState) {
|
||||||
|
|
|
||||||
|
|
@ -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 android.widget.Toast
|
||||||
import androidx.compose.foundation.layout.Box
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.Navigator
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
import ua.acclorite.book_story.presentation.components.CustomDialogWithLazyColumn
|
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.BrowseEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
|
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.LibraryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adding dialog. Adds all selected books to the Library.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseAddingDialog(
|
fun BrowseAddingDialog(
|
||||||
libraryViewModel: LibraryViewModel = hiltViewModel(),
|
libraryViewModel: LibraryViewModel,
|
||||||
viewModel: BrowseViewModel,
|
viewModel: BrowseViewModel,
|
||||||
navigator: Navigator
|
navigator: Navigator
|
||||||
) {
|
) {
|
||||||
|
|
@ -42,7 +45,10 @@ fun BrowseAddingDialog(
|
||||||
imageVectorIcon = Icons.Default.AddChart,
|
imageVectorIcon = Icons.Default.AddChart,
|
||||||
description = stringResource(id = R.string.add_books_description),
|
description = stringResource(id = R.string.add_books_description),
|
||||||
actionText = stringResource(id = R.string.add),
|
actionText = stringResource(id = R.string.add),
|
||||||
isActionEnabled = !state.isBooksLoading,
|
isActionEnabled = !state.isBooksLoading &&
|
||||||
|
state.selectedBooks
|
||||||
|
.filterIsInstance<NullableBook.NotNull>()
|
||||||
|
.isNotEmpty(),
|
||||||
onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) },
|
onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) },
|
||||||
onAction = {
|
onAction = {
|
||||||
viewModel.onEvent(
|
viewModel.onEvent(
|
||||||
|
|
@ -80,7 +86,15 @@ fun BrowseAddingDialog(
|
||||||
} else {
|
} else {
|
||||||
items(state.selectedBooks) { book ->
|
items(state.selectedBooks) { book ->
|
||||||
BrowseAddingDialogItem(result = book) {
|
BrowseAddingDialogItem(result = book) {
|
||||||
|
if (it) {
|
||||||
viewModel.onEvent(BrowseEvent.OnSelectBook(book))
|
viewModel.onEvent(BrowseEvent.OnSelectBook(book))
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
book.message?.asString(context),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
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.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.icons.Icons
|
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.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adding dialog item.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseAddingDialogItem(result: NullableBook, onClick: () -> Unit) {
|
fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
|
||||||
|
|
||||||
if (result is NullableBook.NotNull) {
|
if (result is NullableBook.NotNull) {
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
onClick()
|
onClick(true)
|
||||||
}
|
}
|
||||||
.padding(vertical = 12.dp, horizontal = 24.dp),
|
.padding(vertical = 12.dp, horizontal = 24.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
|
@ -58,14 +63,24 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: () -> Unit) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
val icon =
|
||||||
|
if (result.message?.asString() == stringResource(R.string.error_file_encrypted)) {
|
||||||
|
Icons.Default.Lock
|
||||||
|
} else {
|
||||||
|
Icons.Default.Error
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
onClick(false)
|
||||||
|
}
|
||||||
.padding(vertical = 12.dp, horizontal = 24.dp),
|
.padding(vertical = 12.dp, horizontal = 24.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Warning,
|
imageVector = icon,
|
||||||
contentDescription = "Error",
|
contentDescription = "Error",
|
||||||
modifier = Modifier.size(26.dp),
|
modifier = Modifier.size(26.dp),
|
||||||
tint = MaterialTheme.colorScheme.error
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
|
@ -7,7 +7,7 @@ import androidx.compose.ui.focus.FocusRequester
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.PermissionState
|
import com.google.accompanist.permissions.PermissionState
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
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
|
import java.io.File
|
||||||
|
|
||||||
sealed class BrowseEvent {
|
sealed class BrowseEvent {
|
||||||
|
|
|
||||||
|
|
@ -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.GetBooksFromFiles
|
||||||
import ua.acclorite.book_story.domain.use_case.GetFilesFromDownloads
|
import ua.acclorite.book_story.domain.use_case.GetFilesFromDownloads
|
||||||
import ua.acclorite.book_story.domain.use_case.InsertBooks
|
import ua.acclorite.book_story.domain.use_case.InsertBooks
|
||||||
import ua.acclorite.book_story.presentation.Argument
|
import ua.acclorite.book_story.presentation.data.Argument
|
||||||
import ua.acclorite.book_story.presentation.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
import ua.acclorite.book_story.util.Resource
|
import ua.acclorite.book_story.util.Resource
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -42,7 +42,14 @@ class BrowseViewModel @Inject constructor(
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
onEvent(BrowseEvent.OnLoadList)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getFilesFromDownloads()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEvent(event: BrowseEvent) {
|
fun onEvent(event: BrowseEvent) {
|
||||||
|
|
@ -268,36 +275,25 @@ class BrowseViewModel @Inject constructor(
|
||||||
|
|
||||||
is BrowseEvent.OnGetBooksFromFiles -> {
|
is BrowseEvent.OnGetBooksFromFiles -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
getBooksFromFiles.execute(
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isBooksLoading = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val books = getBooksFromFiles.execute(
|
||||||
_state.value.selectableFiles
|
_state.value.selectableFiles
|
||||||
.filter { it.second }
|
.filter { it.second }
|
||||||
.map { it.first }
|
.map { it.first }
|
||||||
).collect { result ->
|
)
|
||||||
when (result) {
|
|
||||||
is Resource.Success -> {
|
|
||||||
if (result.data?.isEmpty() ?: return@collect) {
|
|
||||||
return@collect
|
|
||||||
}
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
selectedBooks = result.data,
|
selectedBooks = books,
|
||||||
isBooksLoading = false
|
isBooksLoading = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Resource.Loading -> {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isBooksLoading = result.isLoading
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is Resource.Error -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnAddBooks -> {
|
is BrowseEvent.OnAddBooks -> {
|
||||||
|
|
@ -323,7 +319,11 @@ class BrowseViewModel @Inject constructor(
|
||||||
onEvent(BrowseEvent.OnClearSelectedFiles)
|
onEvent(BrowseEvent.OnClearSelectedFiles)
|
||||||
onEvent(BrowseEvent.OnLoadList)
|
onEvent(BrowseEvent.OnLoadList)
|
||||||
|
|
||||||
event.navigator.navigate(Screen.LIBRARY, Argument("added_books", books))
|
event.navigator.navigate(
|
||||||
|
Screen.LIBRARY,
|
||||||
|
false,
|
||||||
|
Argument("added_books", books)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,23 +54,24 @@ import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import ua.acclorite.book_story.R
|
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.AnimatedTopAppBar
|
||||||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
||||||
import ua.acclorite.book_story.presentation.components.CustomSnackbar
|
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.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.HistoryDeleteWholeHistoryDialog
|
||||||
import ua.acclorite.book_story.presentation.screens.history.components.HistoryItem
|
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.HistoryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
|
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.Transitions
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
@ -81,11 +82,14 @@ import java.util.UUID
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun HistoryScreen(
|
fun HistoryScreen(
|
||||||
viewModel: HistoryViewModel = hiltViewModel(),
|
viewModel: HistoryViewModel,
|
||||||
|
libraryViewModel: LibraryViewModel,
|
||||||
navigator: Navigator
|
navigator: Navigator
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
val books = libraryViewModel.state.collectAsState().value.books.map { it.first }
|
||||||
|
|
||||||
val refreshState = rememberPullRefreshState(
|
val refreshState = rememberPullRefreshState(
|
||||||
refreshing = state.isRefreshing,
|
refreshing = state.isRefreshing,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
|
|
@ -118,7 +122,7 @@ fun HistoryScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.showDeleteWholeHistoryDialog) {
|
if (state.showDeleteWholeHistoryDialog) {
|
||||||
HistoryDeleteWholeHistoryDialog(viewModel = viewModel)
|
HistoryDeleteWholeHistoryDialog(viewModel = viewModel, libraryViewModel = libraryViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -144,7 +148,10 @@ fun HistoryScreen(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content1Actions = {
|
content1Actions = {
|
||||||
IconButton(onClick = { viewModel.onEvent(HistoryEvent.OnSearchShowHide) }) {
|
IconButton(
|
||||||
|
enabled = !state.isRefreshing,
|
||||||
|
onClick = { viewModel.onEvent(HistoryEvent.OnSearchShowHide) }
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Search,
|
imageVector = Icons.Default.Search,
|
||||||
contentDescription = "Search history",
|
contentDescription = "Search history",
|
||||||
|
|
@ -153,7 +160,9 @@ fun HistoryScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
enabled = !state.isLoading && !state.isRefreshing && state.history.isNotEmpty(),
|
enabled = !state.isLoading
|
||||||
|
&& !state.isRefreshing
|
||||||
|
&& state.history.isNotEmpty(),
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog)
|
viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog)
|
||||||
}
|
}
|
||||||
|
|
@ -248,7 +257,7 @@ fun HistoryScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
CategoryTitle(
|
CategoryTitle(
|
||||||
modifier = Modifier.animateItemPlacement(tween(300)),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
title = when (groupedHistory.title) {
|
title = when (groupedHistory.title) {
|
||||||
"today" -> stringResource(id = R.string.today)
|
"today" -> stringResource(id = R.string.today)
|
||||||
"yesterday" -> stringResource(id = R.string.yesterday)
|
"yesterday" -> stringResource(id = R.string.yesterday)
|
||||||
|
|
@ -262,27 +271,36 @@ fun HistoryScreen(
|
||||||
items(
|
items(
|
||||||
groupedHistory.history, key = { it.id ?: UUID.randomUUID() }
|
groupedHistory.history, key = { it.id ?: UUID.randomUUID() }
|
||||||
) {
|
) {
|
||||||
|
val book = books.find { book -> book.id == it.bookId } ?: return@items
|
||||||
|
|
||||||
HistoryItem(
|
HistoryItem(
|
||||||
modifier = Modifier.animateItemPlacement(tween(300)),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
history = it,
|
history = it,
|
||||||
|
book = book,
|
||||||
onBodyClick = {
|
onBodyClick = {
|
||||||
navigator.navigate(
|
navigator.navigate(
|
||||||
Screen.BOOK_INFO,
|
Screen.BOOK_INFO,
|
||||||
Argument("book", it.book)
|
false,
|
||||||
|
Argument("book", book)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onTitleClick = {
|
onTitleClick = {
|
||||||
navigator.navigate(
|
navigator.navigate(
|
||||||
Screen.READER,
|
Screen.READER,
|
||||||
Argument("book", it.book)
|
false,
|
||||||
|
Argument("book", book)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
isDeleteEnabled = !state.isRefreshing,
|
||||||
onDeleteClick = {
|
onDeleteClick = {
|
||||||
viewModel.onEvent(
|
viewModel.onEvent(
|
||||||
HistoryEvent.OnDeleteHistoryElement(
|
HistoryEvent.OnDeleteHistoryElement(
|
||||||
it,
|
historyToDelete = it,
|
||||||
snackbarState,
|
snackbarState = snackbarState,
|
||||||
context
|
context = context,
|
||||||
|
refreshList = {
|
||||||
|
libraryViewModel.onEvent(LibraryEvent.OnLoadList)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -318,9 +336,8 @@ fun HistoryScreen(
|
||||||
state.isRefreshing,
|
state.isRefreshing,
|
||||||
refreshState,
|
refreshState,
|
||||||
Modifier.align(Alignment.TopCenter),
|
Modifier.align(Alignment.TopCenter),
|
||||||
backgroundColor = MaterialTheme.elevation(),
|
backgroundColor = MaterialTheme.colorScheme.inverseSurface,
|
||||||
contentColor = MaterialTheme.colorScheme.primary,
|
contentColor = MaterialTheme.colorScheme.inverseOnSurface
|
||||||
scale = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -331,7 +348,7 @@ fun HistoryScreen(
|
||||||
return@BackHandler
|
return@BackHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.navigate(Screen.LIBRARY)
|
navigator.navigate(Screen.LIBRARY, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,19 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import ua.acclorite.book_story.R
|
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.HistoryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
|
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
|
@Composable
|
||||||
fun HistoryDeleteWholeHistoryDialog(
|
fun HistoryDeleteWholeHistoryDialog(
|
||||||
viewModel: HistoryViewModel
|
viewModel: HistoryViewModel,
|
||||||
|
libraryViewModel: LibraryViewModel
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
|
@ -29,7 +35,9 @@ fun HistoryDeleteWholeHistoryDialog(
|
||||||
isActionEnabled = true,
|
isActionEnabled = true,
|
||||||
onAction = {
|
onAction = {
|
||||||
viewModel.onEvent(
|
viewModel.onEvent(
|
||||||
HistoryEvent.OnDeleteWholeHistory
|
HistoryEvent.OnDeleteWholeHistory {
|
||||||
|
libraryViewModel.onEvent(LibraryEvent.OnLoadList)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -26,22 +26,29 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import 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.domain.model.History
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History list element item.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun HistoryItem(
|
fun HistoryItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
history: History,
|
history: History,
|
||||||
|
book: Book,
|
||||||
onBodyClick: () -> Unit,
|
onBodyClick: () -> Unit,
|
||||||
onTitleClick: () -> Unit,
|
onTitleClick: () -> Unit,
|
||||||
|
isDeleteEnabled: Boolean,
|
||||||
onDeleteClick: () -> Unit
|
onDeleteClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val date = Date(history.time)
|
val date = Date(history.time)
|
||||||
|
|
@ -77,10 +84,10 @@ fun HistoryItem(
|
||||||
tint = MaterialTheme.elevation(12.dp)
|
tint = MaterialTheme.elevation(12.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (history.book.coverImage != null) {
|
if (book.coverImage != null) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = history.book.coverImage.asImageBitmap(),
|
bitmap = book.coverImage,
|
||||||
contentDescription = "Cover",
|
contentDescription = stringResource(id = R.string.cover_image_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(MaterialTheme.shapes.medium),
|
.clip(MaterialTheme.shapes.medium),
|
||||||
|
|
@ -96,7 +103,7 @@ fun HistoryItem(
|
||||||
modifier = Modifier.fillMaxHeight()
|
modifier = Modifier.fillMaxHeight()
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
history.book.title,
|
book.title,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -109,7 +116,7 @@ fun HistoryItem(
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
pattern.format(date),
|
pattern.format(date),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
|
@ -123,6 +130,7 @@ fun HistoryItem(
|
||||||
}
|
}
|
||||||
Box(modifier = Modifier.weight(0.1f), contentAlignment = Alignment.CenterEnd) {
|
Box(modifier = Modifier.weight(0.1f), contentAlignment = Alignment.CenterEnd) {
|
||||||
IconButton(
|
IconButton(
|
||||||
|
enabled = isDeleteEnabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
onDeleteClick()
|
onDeleteClick()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,11 @@ sealed class HistoryEvent {
|
||||||
data object OnRefreshList : HistoryEvent()
|
data object OnRefreshList : HistoryEvent()
|
||||||
data object OnLoadList : HistoryEvent()
|
data object OnLoadList : HistoryEvent()
|
||||||
data object OnShowHideDeleteWholeHistoryDialog : HistoryEvent()
|
data object OnShowHideDeleteWholeHistoryDialog : HistoryEvent()
|
||||||
data object OnDeleteWholeHistory : HistoryEvent()
|
data class OnDeleteWholeHistory(val refreshList: () -> Unit) : HistoryEvent()
|
||||||
data class OnDeleteHistoryElement(
|
data class OnDeleteHistoryElement(
|
||||||
val historyToDelete: History,
|
val historyToDelete: History,
|
||||||
val snackbarState: SnackbarHostState,
|
val snackbarState: SnackbarHostState,
|
||||||
|
val refreshList: () -> Unit,
|
||||||
val context: Context
|
val context: Context
|
||||||
) : HistoryEvent()
|
) : HistoryEvent()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.history.data
|
package ua.acclorite.book_story.presentation.screens.history.data
|
||||||
|
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.model.History
|
||||||
import ua.acclorite.book_story.domain.use_case.DeleteHistory
|
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.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.GetHistory
|
||||||
import ua.acclorite.book_story.domain.use_case.InsertHistory
|
import ua.acclorite.book_story.domain.use_case.InsertHistory
|
||||||
import ua.acclorite.book_story.util.Resource
|
import ua.acclorite.book_story.util.Resource
|
||||||
|
|
@ -30,16 +30,25 @@ class HistoryViewModel @Inject constructor(
|
||||||
private val insertHistory: InsertHistory,
|
private val insertHistory: InsertHistory,
|
||||||
private val getHistory: GetHistory,
|
private val getHistory: GetHistory,
|
||||||
private val deleteWholeHistory: DeleteWholeHistory,
|
private val deleteWholeHistory: DeleteWholeHistory,
|
||||||
private val deleteHistory: DeleteHistory
|
private val deleteHistory: DeleteHistory,
|
||||||
|
private val getBooksById: GetBooksById
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _state = MutableStateFlow(HistoryState())
|
private val _state = MutableStateFlow(HistoryState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
|
private var job2: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
onEvent(HistoryEvent.OnLoadList)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getHistoryFromDatabase()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEvent(event: HistoryEvent) {
|
fun onEvent(event: HistoryEvent) {
|
||||||
|
|
@ -85,6 +94,7 @@ class HistoryViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteWholeHistory.execute()
|
deleteWholeHistory.execute()
|
||||||
|
event.refreshList()
|
||||||
getHistoryFromDatabase("")
|
getHistoryFromDatabase("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,20 +114,27 @@ class HistoryViewModel @Inject constructor(
|
||||||
listOf(event.historyToDelete)
|
listOf(event.historyToDelete)
|
||||||
)
|
)
|
||||||
onEvent(HistoryEvent.OnRefreshList)
|
onEvent(HistoryEvent.OnRefreshList)
|
||||||
|
event.refreshList()
|
||||||
|
|
||||||
|
job2?.cancel()
|
||||||
event.snackbarState.currentSnackbarData?.dismiss()
|
event.snackbarState.currentSnackbarData?.dismiss()
|
||||||
|
|
||||||
|
job2 = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
delay(10000)
|
||||||
|
event.snackbarState.currentSnackbarData?.dismiss()
|
||||||
|
}
|
||||||
val snackbar = event.snackbarState.showSnackbar(
|
val snackbar = event.snackbarState.showSnackbar(
|
||||||
event.context.getString(R.string.history_element_deleted),
|
event.context.getString(R.string.history_element_deleted),
|
||||||
event.context.getString(R.string.undo),
|
event.context.getString(R.string.undo)
|
||||||
duration = SnackbarDuration.Long
|
|
||||||
)
|
)
|
||||||
|
|
||||||
when (snackbar) {
|
when (snackbar) {
|
||||||
SnackbarResult.Dismissed -> return@launch
|
SnackbarResult.Dismissed -> Unit
|
||||||
SnackbarResult.ActionPerformed -> {
|
SnackbarResult.ActionPerformed -> {
|
||||||
insertHistory.execute(
|
insertHistory.execute(
|
||||||
listOf(event.historyToDelete)
|
listOf(event.historyToDelete)
|
||||||
)
|
)
|
||||||
|
event.refreshList()
|
||||||
onEvent(HistoryEvent.OnRefreshList)
|
onEvent(HistoryEvent.OnRefreshList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -197,7 +214,7 @@ class HistoryViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun filterMaxElementsById(elements: List<History>): List<History> {
|
fun filterMaxElementsById(elements: List<History>): List<History> {
|
||||||
val groupedById = elements.groupBy { it.book.id }
|
val groupedById = elements.groupBy { it.bookId }
|
||||||
val maxElementsById = groupedById.map { (_, values) ->
|
val maxElementsById = groupedById.map { (_, values) ->
|
||||||
values.maxByOrNull { it.time }
|
values.maxByOrNull { it.time }
|
||||||
}
|
}
|
||||||
|
|
@ -223,11 +240,15 @@ class HistoryViewModel @Inject constructor(
|
||||||
return@collect
|
return@collect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val books = getBooksById.execute(
|
||||||
|
history.map { it.bookId }.distinct()
|
||||||
|
)
|
||||||
val groupedHistory = mutableListOf<GroupedHistory>()
|
val groupedHistory = mutableListOf<GroupedHistory>()
|
||||||
|
|
||||||
history
|
history
|
||||||
.filter {
|
.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 ->
|
}.groupBy { item ->
|
||||||
val calendar = Calendar.getInstance().apply {
|
val calendar = Calendar.getInstance().apply {
|
||||||
timeInMillis = item.time
|
timeInMillis = item.time
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
|
@ -69,24 +68,23 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
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.AnimatedTopAppBar
|
||||||
import ua.acclorite.book_story.presentation.components.IsEmpty
|
|
||||||
import ua.acclorite.book_story.presentation.components.MoreDropDown
|
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.browse.data.BrowseViewModel
|
||||||
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
|
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.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.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.LibraryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
||||||
import ua.acclorite.book_story.ui.Transitions
|
import ua.acclorite.book_story.ui.Transitions
|
||||||
|
|
@ -99,15 +97,17 @@ import java.util.UUID
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryScreen(
|
fun LibraryScreen(
|
||||||
viewModel: LibraryViewModel = hiltViewModel(),
|
viewModel: LibraryViewModel,
|
||||||
browseViewModel: BrowseViewModel = hiltViewModel(),
|
browseViewModel: BrowseViewModel,
|
||||||
historyViewModel: HistoryViewModel = hiltViewModel(),
|
historyViewModel: HistoryViewModel,
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
addedBooks: List<Book>
|
addedBooks: List<Book>
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val pagerState =
|
val pagerState = rememberPagerState(
|
||||||
rememberPagerState(initialPage = state.currentPage, pageCount = { Category.entries.size })
|
initialPage = state.currentPage,
|
||||||
|
pageCount = { Category.entries.size }
|
||||||
|
)
|
||||||
val refreshState = rememberPullRefreshState(
|
val refreshState = rememberPullRefreshState(
|
||||||
refreshing = state.isRefreshing,
|
refreshing = state.isRefreshing,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
|
|
@ -165,16 +165,15 @@ fun LibraryScreen(
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text(
|
Text(
|
||||||
text = state.books.size.toString(),
|
text = state.books.size.toString(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(MaterialTheme.elevation(elevation = 6.dp))
|
.background(MaterialTheme.elevation(elevation = 6.dp))
|
||||||
.padding(horizontal = 10.dp, vertical = 4.dp),
|
.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -313,9 +312,11 @@ fun LibraryScreen(
|
||||||
LaunchedEffect(state.books) {
|
LaunchedEffect(state.books) {
|
||||||
categorizedBooks.clear()
|
categorizedBooks.clear()
|
||||||
categorizedBooks.addAll(state.books.filter { it.first.category == category }
|
categorizedBooks.addAll(state.books.filter { it.first.category == category }
|
||||||
.sortedByDescending {
|
.sortedWith(
|
||||||
it.first.lastOpened
|
compareByDescending<Pair<Book, Boolean>> { it.first.lastOpened }
|
||||||
})
|
.thenBy { it.first.title }
|
||||||
|
)
|
||||||
|
)
|
||||||
categoryIsLoading = false
|
categoryIsLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,15 +334,14 @@ fun LibraryScreen(
|
||||||
) {
|
) {
|
||||||
LibraryBookItem(
|
LibraryBookItem(
|
||||||
book = it,
|
book = it,
|
||||||
modifier = Modifier.animateItemPlacement(
|
modifier = Modifier.animateItemPlacement(),
|
||||||
animationSpec = tween(300)
|
|
||||||
),
|
|
||||||
onCoverImageClick = {
|
onCoverImageClick = {
|
||||||
if (state.hasSelectedItems) {
|
if (state.hasSelectedItems) {
|
||||||
viewModel.onEvent(LibraryEvent.OnSelectBook(it))
|
viewModel.onEvent(LibraryEvent.OnSelectBook(it))
|
||||||
} else {
|
} else {
|
||||||
navigator.navigate(
|
navigator.navigate(
|
||||||
Screen.BOOK_INFO,
|
Screen.BOOK_INFO,
|
||||||
|
false,
|
||||||
Argument("book", it.first)
|
Argument("book", it.first)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -354,6 +354,7 @@ fun LibraryScreen(
|
||||||
onButtonClick = {
|
onButtonClick = {
|
||||||
navigator.navigate(
|
navigator.navigate(
|
||||||
Screen.READER,
|
Screen.READER,
|
||||||
|
false,
|
||||||
Argument("book", it.first)
|
Argument("book", it.first)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -385,7 +386,7 @@ fun LibraryScreen(
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
actionTitle = stringResource(id = R.string.add_book)
|
actionTitle = stringResource(id = R.string.add_book)
|
||||||
) {
|
) {
|
||||||
navigator.navigate(Screen.BROWSE)
|
navigator.navigate(Screen.BROWSE, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -393,9 +394,8 @@ fun LibraryScreen(
|
||||||
state.isRefreshing,
|
state.isRefreshing,
|
||||||
refreshState,
|
refreshState,
|
||||||
Modifier.align(Alignment.TopCenter),
|
Modifier.align(Alignment.TopCenter),
|
||||||
backgroundColor = MaterialTheme.elevation(),
|
backgroundColor = MaterialTheme.colorScheme.inverseSurface,
|
||||||
contentColor = MaterialTheme.colorScheme.primary,
|
contentColor = MaterialTheme.colorScheme.inverseOnSurface
|
||||||
scale = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Image
|
import androidx.compose.material.icons.filled.Image
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Warning
|
|
||||||
import androidx.compose.material3.FilledIconButton
|
import androidx.compose.material3.FilledIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
|
@ -30,7 +29,6 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.presentation.data.removeTrailingZero
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Library list element item.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryBookItem(
|
fun LibraryBookItem(
|
||||||
|
|
@ -102,7 +103,7 @@ fun LibraryBookItem(
|
||||||
|
|
||||||
if (book.first.coverImage != null) {
|
if (book.first.coverImage != null) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = book.first.coverImage!!.asImageBitmap(),
|
bitmap = book.first.coverImage!!,
|
||||||
contentDescription = "Cover",
|
contentDescription = "Cover",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -111,7 +112,6 @@ fun LibraryBookItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (book.first.file != null) {
|
|
||||||
Text(
|
Text(
|
||||||
"${
|
"${
|
||||||
(book.first.progress * 100)
|
(book.first.progress * 100)
|
||||||
|
|
@ -128,23 +128,9 @@ fun LibraryBookItem(
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
style = MaterialTheme.typography.bodySmall
|
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
FilledIconButton(
|
FilledIconButton(
|
||||||
onClick = { onButtonClick() },
|
onClick = { onButtonClick() },
|
||||||
enabled = book.first.file != null,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(6.dp)
|
.padding(6.dp)
|
||||||
|
|
@ -154,11 +140,7 @@ fun LibraryBookItem(
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
.copy(0.9f),
|
.copy(0.9f),
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
.copy(0.9f),
|
.copy(0.9f)
|
||||||
disabledContainerColor = MaterialTheme.colorScheme.primaryContainer
|
|
||||||
.copy(0.6f),
|
|
||||||
disabledContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
.copy(0.6f)
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
|
||||||
|
|
@ -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.presentation.screens.library.data.LibraryViewModel
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab row, either scrollable or static, depends on screen width.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryTabRow(viewModel: LibraryViewModel, books: List<Book>, pagerState: PagerState) {
|
fun LibraryTabRow(viewModel: LibraryViewModel, books: List<Book>, pagerState: PagerState) {
|
||||||
|
|
@ -97,9 +100,9 @@ fun LibraryTabRow(viewModel: LibraryViewModel, books: List<Book>, pagerState: Pa
|
||||||
Text(
|
Text(
|
||||||
text = tabItem.second.toString(),
|
text = tabItem.second.toString(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(MaterialTheme.elevation(6.dp))
|
.background(MaterialTheme.elevation(6.dp))
|
||||||
.padding(horizontal = 6.dp, vertical = 4.dp),
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
|
|
@ -153,9 +156,9 @@ fun LibraryTabRow(viewModel: LibraryViewModel, books: List<Book>, pagerState: Pa
|
||||||
Text(
|
Text(
|
||||||
text = tabItem.second.toString(),
|
text = tabItem.second.toString(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(MaterialTheme.elevation(6.dp))
|
.background(MaterialTheme.elevation(6.dp))
|
||||||
.padding(horizontal = 6.dp, vertical = 4.dp),
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 android.widget.Toast
|
||||||
import androidx.compose.material.icons.Icons
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import ua.acclorite.book_story.R
|
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.BrowseEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
|
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.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.LibraryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete dialog. Deletes all selected books.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryDeleteDialog(
|
fun LibraryDeleteDialog(
|
||||||
viewModel: LibraryViewModel,
|
viewModel: LibraryViewModel,
|
||||||
|
|
@ -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 android.widget.Toast
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
|
@ -13,11 +13,14 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.presentation.components.CustomDialogWithLazyColumn
|
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn
|
||||||
import ua.acclorite.book_story.presentation.components.SelectableDialogItem
|
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.LibraryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move dialog. Moves all selected books to the selected category.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) {
|
fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) {
|
||||||
|
|
@ -29,10 +29,23 @@ class LibraryViewModel @Inject constructor(
|
||||||
private val _state = MutableStateFlow(LibraryState())
|
private val _state = MutableStateFlow(LibraryState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val _isReady = MutableStateFlow(false)
|
||||||
|
val isReady = _isReady.asStateFlow()
|
||||||
|
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
onEvent(LibraryEvent.OnLoadList)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getBooksFromDatabase()
|
||||||
|
_isReady.update {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEvent(event: LibraryEvent) {
|
fun onEvent(event: LibraryEvent) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.reader
|
package ua.acclorite.book_story.presentation.screens.reader
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.viewModels
|
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.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.text.selection.DisableSelection
|
import androidx.compose.foundation.text.selection.DisableSelection
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
|
||||||
import androidx.compose.material.LinearProgressIndicator
|
import androidx.compose.material.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import dagger.hilt.android.lifecycle.withCreationCallback
|
import dagger.hilt.android.lifecycle.withCreationCallback
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.presentation.Navigator
|
import ua.acclorite.book_story.presentation.components.CustomSelectionContainer
|
||||||
import ua.acclorite.book_story.presentation.components.IsError
|
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.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.HistoryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
|
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.LibraryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
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.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.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.ReaderEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
@ -78,9 +79,9 @@ import ua.acclorite.book_story.util.Constants
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderScreen(
|
fun ReaderScreen(
|
||||||
mainViewModel: MainViewModel = hiltViewModel(),
|
mainViewModel: MainViewModel,
|
||||||
libraryViewModel: LibraryViewModel = hiltViewModel(),
|
libraryViewModel: LibraryViewModel,
|
||||||
historyViewModel: HistoryViewModel = hiltViewModel(),
|
historyViewModel: HistoryViewModel,
|
||||||
navigator: Navigator
|
navigator: Navigator
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current as ComponentActivity
|
val context = LocalContext.current as ComponentActivity
|
||||||
|
|
@ -189,7 +190,7 @@ fun ReaderScreen(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(nestedScrollConnection),
|
.nestedScroll(nestedScrollConnection),
|
||||||
containerColor = Color(backgroundColor.toULong()),
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
topBar = {
|
topBar = {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = state.showMenu,
|
visible = state.showMenu,
|
||||||
|
|
@ -219,21 +220,68 @@ 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(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.then(
|
||||||
|
if (!loading && toolbarShowed) {
|
||||||
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = null,
|
interactionSource = null,
|
||||||
indication = null,
|
indication = null,
|
||||||
enabled = !loading,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onEvent(
|
viewModel.onEvent(
|
||||||
ReaderEvent.OnShowHideMenu(context = context)
|
ReaderEvent.OnShowHideMenu(context = context)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
}
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
if (state.book.text.isNotEmpty()) {
|
if (state.book.text.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
|
|
@ -246,9 +294,18 @@ fun ReaderScreen(
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
state.book.text, key = { _, key -> key.id }
|
state.book.text, key = { _, key -> key.id }
|
||||||
) { index, text ->
|
) { index, text ->
|
||||||
if (index == 0)
|
Column(
|
||||||
Spacer(modifier = Modifier.height(36.dp))
|
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(
|
||||||
text = "${if (paragraphIndentation) " " else ""}${text.line}",
|
text = "${if (paragraphIndentation) " " else ""}${text.line}",
|
||||||
color = Color(fontColor.toULong()),
|
color = Color(fontColor.toULong()),
|
||||||
|
|
@ -259,15 +316,9 @@ fun ReaderScreen(
|
||||||
fontFamily = fontFamily.font,
|
fontFamily = fontFamily.font,
|
||||||
fontStyle = fontStyle,
|
fontStyle = fontStyle,
|
||||||
fontSize = fontSize.sp,
|
fontSize = fontSize.sp,
|
||||||
lineHeight = (fontSize + lineHeight).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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.book.text.isNotEmpty()) {
|
if (state.book.text.isNotEmpty()) {
|
||||||
|
|
@ -300,6 +351,9 @@ fun ReaderScreen(
|
||||||
actionTitle = stringResource(id = R.string.go_back),
|
actionTitle = stringResource(id = R.string.go_back),
|
||||||
action = {
|
action = {
|
||||||
navigator.navigateBack()
|
navigator.navigateBack()
|
||||||
|
libraryViewModel.onEvent(LibraryEvent.OnLoadList)
|
||||||
|
historyViewModel.onEvent(HistoryEvent.OnLoadList)
|
||||||
|
viewModel.onEvent(ReaderEvent.OnShowSystemBars(context))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,15 @@ import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -26,13 +30,17 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.presentation.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
import ua.acclorite.book_story.presentation.Screen
|
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.LibraryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
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.ReaderEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reader end item. Displays at the end of the book.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderEndItem(
|
fun ReaderEndItem(
|
||||||
libraryViewModel: LibraryViewModel,
|
libraryViewModel: LibraryViewModel,
|
||||||
|
|
@ -46,7 +54,13 @@ fun ReaderEndItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.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
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -56,7 +70,14 @@ fun ReaderEndItem(
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
overflow = TextOverflow.Ellipsis
|
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(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (state.book.category != Category.ALREADY_READ) {
|
if (state.book.category != Category.ALREADY_READ) {
|
||||||
|
|
@ -86,7 +107,7 @@ fun ReaderEndItem(
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
navigator.navigate(Screen.LIBRARY)
|
navigator.navigate(Screen.LIBRARY, true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp),
|
contentPadding = PaddingValues(horizontal = 12.dp),
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,18 @@ import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsIgnoringVisibility
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Image
|
import androidx.compose.material.icons.filled.Image
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
|
@ -24,7 +28,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.presentation.screens.reader.data.ReaderViewModel
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reader start item. Displays at the beginning of the book.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderStartItem(viewModel: ReaderViewModel) {
|
fun ReaderStartItem(viewModel: ReaderViewModel) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
@ -41,6 +48,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility)
|
||||||
.padding(24.dp)
|
.padding(24.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -66,7 +74,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
|
||||||
|
|
||||||
if (state.book.coverImage != null) {
|
if (state.book.coverImage != null) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = state.book.coverImage!!.asImageBitmap(),
|
bitmap = state.book.coverImage!!,
|
||||||
contentDescription = "Cover",
|
contentDescription = "Cover",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
|
||||||
|
|
@ -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.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
|
@ -19,7 +19,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
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.removeDigits
|
||||||
import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
|
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.ReaderEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reader bottom bar. Has a slider to change progress.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderBottomBar(
|
fun ReaderBottomBar(
|
||||||
viewModel: ReaderViewModel,
|
viewModel: ReaderViewModel,
|
||||||
|
|
@ -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.activity.ComponentActivity
|
||||||
import androidx.compose.foundation.basicMarquee
|
import androidx.compose.foundation.basicMarquee
|
||||||
|
|
@ -6,10 +6,7 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
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.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.Argument
|
import ua.acclorite.book_story.presentation.components.GoBackButton
|
||||||
import ua.acclorite.book_story.presentation.Navigator
|
import ua.acclorite.book_story.presentation.data.Argument
|
||||||
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.data.removeDigits
|
import ua.acclorite.book_story.presentation.data.removeDigits
|
||||||
import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
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.ReaderEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reader top bar. Displays title of the book.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColor: Color) {
|
fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColor: Color) {
|
||||||
|
|
@ -48,15 +48,8 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = {
|
GoBackButton(navigator = navigator) {
|
||||||
navigator.navigateBack()
|
|
||||||
viewModel.onEvent(ReaderEvent.OnShowSystemBars(context))
|
viewModel.onEvent(ReaderEvent.OnShowSystemBars(context))
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
|
||||||
contentDescription = "Go back",
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
|
|
@ -75,6 +68,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.navigateWithoutBackStack(
|
navigator.navigateWithoutBackStack(
|
||||||
Screen.BOOK_INFO,
|
Screen.BOOK_INFO,
|
||||||
|
true,
|
||||||
Argument(
|
Argument(
|
||||||
"book",
|
"book",
|
||||||
book
|
book
|
||||||
|
|
@ -86,7 +80,6 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
|
||||||
delayMillis = 4000
|
delayMillis = 4000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
id = R.string.read_query,
|
id = R.string.read_query,
|
||||||
|
|
@ -107,8 +100,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Settings,
|
imageVector = Icons.Default.Settings,
|
||||||
contentDescription = "Open Settings",
|
contentDescription = stringResource(id = R.string.open_reader_settings_content_desc),
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -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.activity.ComponentActivity
|
||||||
import androidx.compose.animation.animateColorAsState
|
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.ui.elevation
|
||||||
import ua.acclorite.book_story.util.Constants
|
import ua.acclorite.book_story.util.Constants
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings bottom sheet. Has General and Colors categories.
|
||||||
|
*/
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class
|
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class
|
||||||
)
|
)
|
||||||
|
|
@ -118,7 +121,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
CategoryTitle(
|
CategoryTitle(
|
||||||
title = stringResource(id = R.string.font_settings),
|
title = stringResource(id = R.string.font_reader_settings),
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -197,7 +200,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
CategoryTitle(
|
CategoryTitle(
|
||||||
title = stringResource(id = R.string.text_settings),
|
title = stringResource(id = R.string.text_reader_settings),
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.ReaderEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
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)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: PagerState) {
|
fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: PagerState) {
|
||||||
|
|
@ -7,10 +7,10 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
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 {
|
sealed class ReaderEvent {
|
||||||
data class OnFileNotFound(val onLoaded: () -> Unit) : ReaderEvent()
|
data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent()
|
||||||
data class OnLoadText(
|
data class OnLoadText(
|
||||||
val scrollState: LazyListState,
|
val scrollState: LazyListState,
|
||||||
val navigator: Navigator,
|
val navigator: Navigator,
|
||||||
|
|
@ -37,4 +37,16 @@ sealed class ReaderEvent {
|
||||||
val updatePage: (Int) -> Unit,
|
val updatePage: (Int) -> Unit,
|
||||||
val navigator: Navigator
|
val navigator: Navigator
|
||||||
) : ReaderEvent()
|
) : 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()
|
||||||
}
|
}
|
||||||
|
|
@ -7,6 +7,9 @@ import ua.acclorite.book_story.util.UIText
|
||||||
@Immutable
|
@Immutable
|
||||||
data class ReaderState(
|
data class ReaderState(
|
||||||
val book: Book,
|
val book: Book,
|
||||||
|
val letters: Int = 0,
|
||||||
|
val words: Int = 0,
|
||||||
|
|
||||||
val errorMessage: UIText? = null,
|
val errorMessage: UIText? = null,
|
||||||
|
|
||||||
val showMenu: Boolean = false,
|
val showMenu: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.reader.data
|
package ua.acclorite.book_story.presentation.screens.reader.data
|
||||||
|
|
||||||
|
import android.app.SearchManager
|
||||||
|
import android.content.Intent
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
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.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.domain.model.History
|
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.InsertHistory
|
||||||
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
||||||
import ua.acclorite.book_story.presentation.Argument
|
import ua.acclorite.book_story.presentation.data.Argument
|
||||||
import ua.acclorite.book_story.presentation.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
import ua.acclorite.book_story.presentation.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
import ua.acclorite.book_story.util.Resource
|
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
@ -39,7 +39,6 @@ import kotlin.math.roundToInt
|
||||||
@HiltViewModel(assistedFactory = ReaderViewModel.Factory::class)
|
@HiltViewModel(assistedFactory = ReaderViewModel.Factory::class)
|
||||||
class ReaderViewModel @AssistedInject constructor(
|
class ReaderViewModel @AssistedInject constructor(
|
||||||
@Assisted book: Book,
|
@Assisted book: Book,
|
||||||
private val getText: GetText,
|
|
||||||
private val updateBooks: UpdateBooks,
|
private val updateBooks: UpdateBooks,
|
||||||
private val insertHistory: InsertHistory
|
private val insertHistory: InsertHistory
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
@ -49,61 +48,29 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
fun onEvent(event: ReaderEvent) {
|
fun onEvent(event: ReaderEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is ReaderEvent.OnFileNotFound -> {
|
is ReaderEvent.OnTextIsEmpty -> {
|
||||||
_state.update {
|
_state.update {
|
||||||
event.onLoaded()
|
event.onLoaded()
|
||||||
it.copy(
|
it.copy(
|
||||||
errorMessage = UIText.StringResource(R.string.error_file_not_found),
|
errorMessage = UIText.StringResource(R.string.error_no_text),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ReaderEvent.OnLoadText -> {
|
is ReaderEvent.OnLoadText -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
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 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 {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
book = it.book.copy(
|
book = it.book.copy(
|
||||||
lastOpened = time
|
lastOpened = time
|
||||||
)
|
),
|
||||||
|
letters = letters,
|
||||||
|
words = words
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,7 +81,7 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
listOf(
|
listOf(
|
||||||
History(
|
History(
|
||||||
null,
|
null,
|
||||||
_state.value.book,
|
_state.value.book.id!!,
|
||||||
time
|
time
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -141,7 +108,7 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delay(500)
|
delay(100)
|
||||||
event.onLoaded()
|
event.onLoaded()
|
||||||
return@collectLatest
|
return@collectLatest
|
||||||
}
|
}
|
||||||
|
|
@ -234,7 +201,8 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
onEvent(ReaderEvent.OnShowSystemBars(event.context))
|
onEvent(ReaderEvent.OnShowSystemBars(event.context))
|
||||||
|
|
||||||
val book = _state.value.book.copy(
|
val book = _state.value.book.copy(
|
||||||
category = Category.ALREADY_READ
|
category = Category.ALREADY_READ,
|
||||||
|
progress = 1f
|
||||||
)
|
)
|
||||||
updateBooks.execute(listOf(book))
|
updateBooks.execute(listOf(book))
|
||||||
|
|
||||||
|
|
@ -245,7 +213,59 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
it != Category.ALREADY_READ
|
it != Category.ALREADY_READ
|
||||||
}.size - 1
|
}.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)
|
ReaderState(book = book)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (book.file == null) {
|
if (book.text.isEmpty()) {
|
||||||
onEvent(ReaderEvent.OnFileNotFound(onLoaded = { onLoaded() }))
|
onEvent(ReaderEvent.OnTextIsEmpty(onLoaded = { onLoaded() }))
|
||||||
} else {
|
} else {
|
||||||
onEvent(ReaderEvent.OnShowHideMenu(false, context))
|
onEvent(ReaderEvent.OnShowHideMenu(false, context))
|
||||||
onEvent(
|
onEvent(
|
||||||
|
|
@ -304,3 +324,5 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
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.DisplaySettings
|
||||||
import androidx.compose.material.icons.filled.Palette
|
import androidx.compose.material.icons.filled.Palette
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.LargeTopAppBar
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -21,19 +21,24 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.Navigator
|
import ua.acclorite.book_story.presentation.components.GoBackButton
|
||||||
import ua.acclorite.book_story.presentation.Screen
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem
|
import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem
|
||||||
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
navigator: Navigator
|
navigator: Navigator
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val listState = rememberLazyListState()
|
||||||
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
||||||
|
canScroll = {
|
||||||
|
listState.canScrollForward
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -42,36 +47,26 @@ fun SettingsScreen(
|
||||||
.windowInsetsPadding(WindowInsets.navigationBars),
|
.windowInsetsPadding(WindowInsets.navigationBars),
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
topBar = {
|
topBar = {
|
||||||
AnimatedTopAppBar(
|
LargeTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(stringResource(id = R.string.settings_screen))
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
GoBackButton(navigator = navigator)
|
||||||
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
isTopBarScrolled = null,
|
colors = TopAppBarDefaults.largeTopAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
content1NavigationIcon = {
|
scrolledContainerColor = MaterialTheme.elevation()
|
||||||
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 = {}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(top = paddingValues.calculateTopPadding())
|
.padding(top = paddingValues.calculateTopPadding()),
|
||||||
|
state = listState
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
SettingsCategoryItem(
|
SettingsCategoryItem(
|
||||||
|
|
@ -79,7 +74,7 @@ fun SettingsScreen(
|
||||||
text = stringResource(id = R.string.general_settings),
|
text = stringResource(id = R.string.general_settings),
|
||||||
description = stringResource(id = R.string.general_settings_desc)
|
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),
|
text = stringResource(id = R.string.appearance_settings),
|
||||||
description = stringResource(id = R.string.appearance_settings_desc)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +14,9 @@ import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkbox with title.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CheckboxWithTitle(
|
fun CheckboxWithTitle(
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.domain.model.ChipItem
|
import ua.acclorite.book_story.domain.model.ChipItem
|
||||||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chips with title. Use list of [ChipItem]s to display chips.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ChipsWithTitle(
|
fun ChipsWithTitle(
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color picker with title.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ColorPickerWithTitle(
|
fun ColorPickerWithTitle(
|
||||||
value: Color,
|
value: Color,
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,14 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
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.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings category item. It is used to represent the whole category.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsCategoryItem(
|
fun SettingsCategoryItem(
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
|
|
@ -53,13 +57,17 @@ fun SettingsCategoryItem(
|
||||||
text,
|
text,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
fontSize = 18.sp
|
fontSize = 18.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
description,
|
description,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue