diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 103e00cb..44ca2d9b 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -3,30 +3,39 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 37cea978..07c5bf31 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -4,6 +4,7 @@ plugins {
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
id("kotlin-parcelize")
+ id("com.mikepenz.aboutlibraries.plugin")
}
android {
@@ -15,7 +16,7 @@ android {
minSdk = 26
targetSdk = 34
versionCode = 1
- versionName = "0.9.7"
+ versionName = "0.9.8"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -61,16 +62,15 @@ android {
dependencies {
// Default
- implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.core:core-ktx:1.13.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
- implementation("androidx.activity:activity-compose:1.8.2")
+ implementation("androidx.activity:activity-compose:1.9.0")
// Compose BOM libraries
- implementation(platform("androidx.compose:compose-bom:2024.04.00"))
+ implementation(platform("androidx.compose:compose-bom:2024.04.01"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.animation:animation")
- implementation("androidx.compose.animation:animation-core")
- implementation("androidx.compose.animation:animation-graphics")
+ implementation("androidx.compose.animation:animation-android")
implementation("androidx.compose.foundation:foundation-layout")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
@@ -81,35 +81,13 @@ dependencies {
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material:material")
- // Unit tests
- testImplementation("androidx.test:core:latest.release")
- testImplementation("junit:junit:latest.release")
- testImplementation("androidx.arch.core:core-testing:latest.release")
- testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:latest.release")
- testImplementation("com.google.truth:truth:1.4.2")
- testImplementation("androidx.test.ext:truth:1.5.0")
- testImplementation("com.squareup.okhttp3:mockwebserver:latest.release")
- testImplementation("io.mockk:mockk:latest.release")
- debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.5")
-
- // Instrumentation tests
- androidTestImplementation("com.google.dagger:hilt-android-testing:latest.release")
- androidTestImplementation("junit:junit:latest.release")
- androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:latest.release")
- androidTestImplementation("androidx.arch.core:core-testing:latest.release")
- androidTestImplementation("com.google.truth:truth:latest.release")
- androidTestImplementation("androidx.test.ext:junit:latest.release")
- androidTestImplementation("androidx.test:core-ktx:latest.release")
- androidTestImplementation("com.squareup.okhttp3:mockwebserver:latest.release")
- androidTestImplementation("io.mockk:mockk-android:latest.release")
- androidTestImplementation("androidx.test:runner:latest.release")
-
// All dependencies
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
// Dagger - Hilt
implementation("com.google.dagger:hilt-android:2.51.1")
+ debugImplementation("androidx.compose.ui:ui-tooling:1.6.6")
ksp("com.google.dagger:hilt-android-compiler:2.51")
implementation("com.google.dagger:hilt-compiler:latest.release")
ksp("androidx.hilt:hilt-compiler:latest.release")
@@ -148,4 +126,16 @@ dependencies {
// Coil for loading bitmaps
implementation("io.coil-kt:coil-compose:2.6.0")
+
+ // Retrofit
+ implementation("com.squareup.retrofit2:retrofit:2.9.0")
+ implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
+ implementation("com.squareup.moshi:moshi:1.15.1")
+ implementation("com.squareup.moshi:moshi-kotlin:1.15.1")
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
+
+ // About open source libraries
+ implementation("com.mikepenz:aboutlibraries-core:latest.release")
+ implementation("com.mikepenz:aboutlibraries-compose:latest.release")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ecadbad2..832045a8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,11 +2,16 @@
+
+
+
+
+
diff --git a/app/src/main/java/ua/acclorite/book_story/Activity.kt b/app/src/main/java/ua/acclorite/book_story/Activity.kt
index a72639ec..3b54b4d0 100644
--- a/app/src/main/java/ua/acclorite/book_story/Activity.kt
+++ b/app/src/main/java/ua/acclorite/book_story/Activity.kt
@@ -7,30 +7,15 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.calculateEndPadding
-import androidx.compose.foundation.layout.calculateStartPadding
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
-import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
-import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import dagger.hilt.android.AndroidEntryPoint
@@ -39,22 +24,27 @@ import ua.acclorite.book_story.presentation.components.custom_navigation_rail.Cu
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.about.AboutScreen
-import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreen
+import ua.acclorite.book_story.presentation.screens.about.AboutScreenRoot
+import ua.acclorite.book_story.presentation.screens.about.data.AboutViewModel
+import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreenRoot
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
-import ua.acclorite.book_story.presentation.screens.browse.BrowseScreen
+import ua.acclorite.book_story.presentation.screens.browse.BrowseScreenRoot
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
-import ua.acclorite.book_story.presentation.screens.help.HelpScreen
-import ua.acclorite.book_story.presentation.screens.history.HistoryScreen
+import ua.acclorite.book_story.presentation.screens.help.HelpScreenRoot
+import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
+import ua.acclorite.book_story.presentation.screens.history.HistoryScreenRoot
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.LibraryScreenRoot
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.ReaderScreenRoot
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
-import ua.acclorite.book_story.presentation.screens.settings.SettingsScreen
-import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettings
-import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings
-import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettings
+import ua.acclorite.book_story.presentation.screens.settings.SettingsScreenRoot
+import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
+import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettingsRoot
+import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettingsRoot
+import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettingsRoot
+import ua.acclorite.book_story.presentation.screens.start.StartScreenRoot
+import ua.acclorite.book_story.presentation.screens.start.data.StartViewModel
import ua.acclorite.book_story.presentation.ui.BookStoryTheme
import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.presentation.ui.isDark
@@ -62,16 +52,21 @@ import ua.acclorite.book_story.presentation.ui.isPureDark
import java.lang.reflect.Field
+@Suppress("unused")
@SuppressLint("DiscouragedPrivateApi")
-@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@AndroidEntryPoint
class Activity : AppCompatActivity() {
+ // Initializing all viewModels on app startup
private val mainViewModel: MainViewModel by viewModels()
private val libraryViewModel: LibraryViewModel by viewModels()
private val historyViewModel: HistoryViewModel by viewModels()
private val browseViewModel: BrowseViewModel by viewModels()
private val bookInfoViewModel: BookInfoViewModel by viewModels()
private val readerViewModel: ReaderViewModel by viewModels()
+ private val helpViewModel: HelpViewModel by viewModels()
+ private val aboutViewModel: AboutViewModel by viewModels()
+ private val settingsViewModel: SettingsViewModel by viewModels()
+ private val startViewModel: StartViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -103,19 +98,6 @@ class Activity : AppCompatActivity() {
val isLoaded by mainViewModel.isReady.collectAsState()
val state by mainViewModel.state.collectAsState()
- val windowClass = calculateWindowSizeClass(activity = this)
- val tabletUI = remember(windowClass) {
- windowClass.widthSizeClass != WindowWidthSizeClass.Compact
- }
- val localLayoutDirection = LocalLayoutDirection.current
- val layoutDirection = remember(localLayoutDirection) {
- if (localLayoutDirection == LayoutDirection.Ltr) {
- LayoutDirection.Ltr
- } else {
- LayoutDirection.Rtl
- }
- }
-
val density = LocalDensity.current
val imeInsets = WindowInsets.ime
val focusManager = LocalFocusManager.current
@@ -124,6 +106,7 @@ class Activity : AppCompatActivity() {
imeInsets.getBottom(density) > 0
}
}
+
LaunchedEffect(isKeyboardVisible) {
if (!isKeyboardVisible) {
focusManager.clearFocus()
@@ -137,175 +120,90 @@ class Activity : AppCompatActivity() {
isPureDark = state.pureDark!!.isPureDark(this),
themeContrast = state.themeContrast!!
) {
- NavigationHost(startScreen = Screen.LIBRARY) {
- val currentScreen by this.getCurrentScreen().collectAsState()
-
- AnimatedVisibility(
- visible = currentScreen == Screen.LIBRARY ||
- currentScreen == Screen.HISTORY ||
- currentScreen == Screen.BROWSE,
- enter = Transitions.BackSlidingTransitionIn,
- exit = Transitions.SlidingTransitionOut
+ NavigationHost(
+ startScreen = if (state.showStartScreen!!) Screen.START
+ else Screen.LIBRARY
+ ) {
+ navigation(
+ screens = arrayOf(Screen.LIBRARY, Screen.HISTORY, Screen.BROWSE),
+ bottomBar = {
+ BottomNavigationBar()
+ },
+ navigationRail = {
+ CustomNavigationRail()
+ }
) {
- Scaffold(
- bottomBar = {
- if (!tabletUI) {
- BottomNavigationBar(navigator = this@NavigationHost)
- }
- },
- containerColor = MaterialTheme.colorScheme.surface
+ // Library
+ composable(
+ screen = Screen.LIBRARY,
+ enterAnim = Transitions.FadeTransitionIn,
+ exitAnim = Transitions.FadeTransitionOut
) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- ) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(
- end = it.calculateEndPadding(
- layoutDirection
- ),
- bottom = it.calculateBottomPadding(),
- start = if (tabletUI) {
- 80.dp + it.calculateStartPadding(
- layoutDirection
- )
- } else {
- it.calculateStartPadding(
- layoutDirection
- )
- },
- )
- ) {
- composable(screen = Screen.LIBRARY) {
- LibraryScreen(
- viewModel = libraryViewModel,
- historyViewModel = historyViewModel,
- browseViewModel = browseViewModel,
- navigator = this@NavigationHost
- )
- }
+ LibraryScreenRoot()
+ }
- composable(screen = Screen.HISTORY) {
- HistoryScreen(
- viewModel = historyViewModel,
- libraryViewModel = libraryViewModel,
- navigator = this@NavigationHost
- )
- }
+ // History
+ composable(
+ screen = Screen.HISTORY,
+ enterAnim = Transitions.FadeTransitionIn,
+ exitAnim = Transitions.FadeTransitionOut
+ ) {
+ HistoryScreenRoot()
+ }
- composable(screen = Screen.BROWSE) {
- BrowseScreen(
- viewModel = browseViewModel,
- libraryViewModel = libraryViewModel,
- navigator = this@NavigationHost
- )
- }
- }
-
- if (tabletUI) {
- CustomNavigationRail(
- navigator = this@NavigationHost,
- layoutDirection = layoutDirection
- )
- }
- }
+ // Browse
+ composable(
+ screen = Screen.BROWSE,
+ enterAnim = Transitions.FadeTransitionIn,
+ exitAnim = Transitions.FadeTransitionOut
+ ) {
+ BrowseScreenRoot()
}
}
// Book Info
- composable(
- screen = Screen.BOOK_INFO,
- enterAnim = Transitions.SlidingTransitionIn,
- exitAnim = Transitions.SlidingTransitionOut
- ) {
- BookInfoScreen(
- viewModel = bookInfoViewModel,
- libraryViewModel = libraryViewModel,
- browseViewModel = browseViewModel,
- historyViewModel = historyViewModel,
- navigator = this@NavigationHost
- )
+ composable(screen = Screen.BOOK_INFO) {
+ BookInfoScreenRoot()
}
- composable(
- screen = Screen.READER,
- enterAnim = Transitions.SlidingTransitionIn,
- exitAnim = Transitions.SlidingTransitionOut
- ) {
- ReaderScreen(
- viewModel = readerViewModel,
- mainViewModel = mainViewModel,
- libraryViewModel = libraryViewModel,
- historyViewModel = historyViewModel,
- navigator = this@NavigationHost
- )
+
+ // Reader
+ composable(screen = Screen.READER) {
+ ReaderScreenRoot()
}
// Settings
- composable(
- screen = Screen.SETTINGS,
- enterAnim = Transitions.SlidingTransitionIn,
- exitAnim = Transitions.SlidingTransitionOut
- ) {
- SettingsScreen(
- navigator = this@NavigationHost
- )
+ composable(screen = Screen.SETTINGS) {
+ SettingsScreenRoot()
}
- // Nested categories
- composable(
- screen = Screen.GENERAL_SETTINGS,
- enterAnim = Transitions.SlidingTransitionIn,
- exitAnim = Transitions.SlidingTransitionOut
- ) {
- GeneralSettings(
- mainViewModel = mainViewModel,
- navigator = this@NavigationHost
- )
+ // Nested settings categories
+ composable(screen = Screen.GENERAL_SETTINGS) {
+ GeneralSettingsRoot()
}
- composable(
- screen = Screen.APPEARANCE_SETTINGS,
- enterAnim = Transitions.SlidingTransitionIn,
- exitAnim = Transitions.SlidingTransitionOut
- ) {
- AppearanceSettings(
- mainViewModel = mainViewModel,
- navigator = this@NavigationHost
- )
+ composable(screen = Screen.APPEARANCE_SETTINGS) {
+ AppearanceSettingsRoot()
}
- composable(
- screen = Screen.READER_SETTINGS,
- enterAnim = Transitions.SlidingTransitionIn,
- exitAnim = Transitions.SlidingTransitionOut
- ) {
- ReaderSettings(
- mainViewModel = mainViewModel,
- navigator = this@NavigationHost
- )
+ composable(screen = Screen.READER_SETTINGS) {
+ ReaderSettingsRoot()
}
// About screen
- composable(
- screen = Screen.ABOUT,
- enterAnim = Transitions.SlidingTransitionIn,
- exitAnim = Transitions.SlidingTransitionOut
- ) {
- AboutScreen(navigator = this@NavigationHost)
- }
- // Help screen
- composable(
- screen = Screen.HELP,
- enterAnim = Transitions.SlidingTransitionIn,
- exitAnim = Transitions.SlidingTransitionOut
- ) {
- HelpScreen(navigator = this@NavigationHost)
+ composable(screen = Screen.ABOUT) {
+ AboutScreenRoot()
}
-// Start screen (later)
-// composable(screen = Screen.START) {
-// StartScreen()
-// }
+ // Help screen
+ composable(screen = Screen.HELP) {
+ HelpScreenRoot()
+ }
+
+ // Start screen
+ composable(
+ screen = Screen.START,
+ enterAnim = Transitions.FadeTransitionIn
+ ) {
+ StartScreenRoot()
+ }
}
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/Application.kt b/app/src/main/java/ua/acclorite/book_story/Application.kt
index 5c1589f7..4732ef7f 100644
--- a/app/src/main/java/ua/acclorite/book_story/Application.kt
+++ b/app/src/main/java/ua/acclorite/book_story/Application.kt
@@ -1,7 +1,36 @@
package ua.acclorite.book_story
import android.app.Application
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
import dagger.hilt.android.HiltAndroidApp
+import ua.acclorite.book_story.data.local.notification.UpdatesNotificationService
@HiltAndroidApp
-class Application : Application()
\ No newline at end of file
+class Application : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+ }
+
+ private fun createNotificationChannel() {
+ val channel = NotificationChannel(
+ UpdatesNotificationService.CHANNEL_ID,
+ getString(R.string.updates_notification_channel),
+ NotificationManager.IMPORTANCE_HIGH
+ )
+
+ val notificationManager = getSystemService(
+ Context.NOTIFICATION_SERVICE
+ ) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
+}
+
+
+
+
+
+
diff --git a/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt b/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt
index 1afd91e0..396e888a 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt
@@ -2,18 +2,35 @@ package ua.acclorite.book_story.data.di
import android.app.Application
import androidx.room.Room
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
+import retrofit2.Retrofit
+import retrofit2.converter.moshi.MoshiConverterFactory
import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.local.room.BookDatabase
+import ua.acclorite.book_story.data.remote.GithubAPI
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
+ @Provides
+ @Singleton
+ fun provideGithubApi(): GithubAPI {
+ val moshi = Moshi.Builder()
+ .add(KotlinJsonAdapterFactory())
+ .build()
+ return Retrofit.Builder()
+ .baseUrl(GithubAPI.BASE_URL)
+ .addConverterFactory(MoshiConverterFactory.create(moshi))
+ .build()
+ .create(GithubAPI::class.java)
+ }
@Provides
@Singleton
diff --git a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt
index 0235feff..71921e85 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt
@@ -6,6 +6,8 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ua.acclorite.book_story.data.local.data_store.DataStore
import ua.acclorite.book_story.data.local.data_store.DataStoreImpl
+import ua.acclorite.book_story.data.local.notification.UpdatesNotificationService
+import ua.acclorite.book_story.data.local.notification.UpdatesNotificationServiceImpl
import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.mapper.book.BookMapperImpl
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
@@ -56,4 +58,10 @@ abstract class RepositoryModule {
abstract fun bindTextParser(
textParserImpl: TextParserImpl
): TextParser
+
+ @Binds
+ @Singleton
+ abstract fun bindNotificationService(
+ updatesNotificationServiceImpl: UpdatesNotificationServiceImpl
+ ): UpdatesNotificationService
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/notification/UpdatesNotificationService.kt b/app/src/main/java/ua/acclorite/book_story/data/local/notification/UpdatesNotificationService.kt
new file mode 100644
index 00000000..93e95033
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/data/local/notification/UpdatesNotificationService.kt
@@ -0,0 +1,12 @@
+package ua.acclorite.book_story.data.local.notification
+
+import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
+
+interface UpdatesNotificationService {
+
+ fun postNotification(releaseInfo: ReleaseResponse)
+
+ companion object {
+ const val CHANNEL_ID = "updates_channel"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/notification/UpdatesNotificationServiceImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/local/notification/UpdatesNotificationServiceImpl.kt
new file mode 100644
index 00000000..09a30dbc
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/data/local/notification/UpdatesNotificationServiceImpl.kt
@@ -0,0 +1,69 @@
+package ua.acclorite.book_story.data.local.notification
+
+import android.app.Application
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
+import javax.inject.Inject
+
+class UpdatesNotificationServiceImpl @Inject constructor(
+ private val application: Application
+) : UpdatesNotificationService {
+
+ private val notificationManager = application.getSystemService(Context.NOTIFICATION_SERVICE)
+ as NotificationManager
+
+ override fun postNotification(releaseInfo: ReleaseResponse) {
+ val downloadLatestVersionIntent = Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(application.getString(R.string.download_latest_release_page))
+ )
+ val latestReleaseIntent = Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(application.getString(R.string.releases_page))
+ )
+
+ val downloadLatestVersionPendingIntent = PendingIntent.getActivity(
+ application,
+ 1,
+ downloadLatestVersionIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ val latestReleasePendingIntent = PendingIntent.getActivity(
+ application,
+ 1,
+ latestReleaseIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val notification = Notification.Builder(
+ application,
+ UpdatesNotificationService.CHANNEL_ID
+ )
+ .setSmallIcon(R.drawable.notification_icon)
+ .setContentTitle(application.getString(R.string.updates_notification_title))
+ .setContentText(
+ application.getString(
+ R.string.updates_notification_desc,
+ releaseInfo.name
+ )
+ )
+ .setContentIntent(downloadLatestVersionPendingIntent)
+ .addAction(
+ R.drawable.app_icon_monochrome,
+ application.getString(R.string.updates_notification_action),
+ latestReleasePendingIntent
+ )
+ .build()
+
+ notificationManager.notify(
+ 0,
+ notification
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/data/remote/GithubAPI.kt b/app/src/main/java/ua/acclorite/book_story/data/remote/GithubAPI.kt
new file mode 100644
index 00000000..fbfd1a90
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/data/remote/GithubAPI.kt
@@ -0,0 +1,13 @@
+package ua.acclorite.book_story.data.remote
+
+import retrofit2.http.GET
+import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
+
+interface GithubAPI {
+ @GET("repos/Acclorite/book-story/releases/latest")
+ suspend fun getLatestRelease(): ReleaseResponse
+
+ companion object {
+ const val BASE_URL = "https://api.github.com/"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/data/remote/dto/ReleaseResponse.kt b/app/src/main/java/ua/acclorite/book_story/data/remote/dto/ReleaseResponse.kt
new file mode 100644
index 00000000..9af2515c
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/data/remote/dto/ReleaseResponse.kt
@@ -0,0 +1,10 @@
+package ua.acclorite.book_story.data.remote.dto
+
+import androidx.compose.runtime.Immutable
+
+@Suppress("PropertyName")
+@Immutable
+data class ReleaseResponse(
+ val name: String,
+ val tag_name: String
+)
diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt
index 774a89ea..0f742b50 100644
--- a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt
+++ b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt
@@ -17,11 +17,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.local.data_store.DataStore
+import ua.acclorite.book_story.data.local.notification.UpdatesNotificationService
import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.data.parser.TextParser
+import ua.acclorite.book_story.data.remote.GithubAPI
+import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook
@@ -31,7 +34,7 @@ import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText
-import ua.acclorite.book_story.presentation.data.MainState
+import ua.acclorite.book_story.presentation.data.MainSettingsState
import java.io.File
import java.io.FileOutputStream
import java.util.UUID
@@ -44,6 +47,9 @@ class BookRepositoryImpl @Inject constructor(
private val database: BookDao,
private val dataStore: DataStore,
+ private val githubAPI: GithubAPI,
+ private val updatesNotificationService: UpdatesNotificationService,
+
private val bookMapper: BookMapper,
private val historyMapper: HistoryMapper,
@@ -385,8 +391,8 @@ class BookRepositoryImpl @Inject constructor(
dataStore.putData(key, value)
}
- override suspend fun getAllSettings(scope: CoroutineScope): MainState {
- val result = CompletableDeferred()
+ override suspend fun getAllSettings(scope: CoroutineScope): MainSettingsState {
+ val result = CompletableDeferred()
scope.launch {
val keys = dataStore.getAllData()
@@ -405,7 +411,7 @@ class BookRepositoryImpl @Inject constructor(
}
jobs?.awaitAll()
- result.complete(MainState.initialize(data))
+ result.complete(MainSettingsState.initialize(data))
}
return result.await()
@@ -601,6 +607,28 @@ class BookRepositoryImpl @Inject constructor(
override suspend fun deleteHistory(history: List) {
database.deleteHistory(history.map { historyMapper.toHistoryEntity(it) })
}
+
+ override suspend fun checkForUpdates(postNotification: Boolean): ReleaseResponse? {
+ return withContext(Dispatchers.IO) {
+ try {
+ val result = githubAPI.getLatestRelease()
+
+ val version = result.tag_name.substringAfterLast("v")
+ val currentVersion = application.getString(R.string.app_version)
+
+ if (version != currentVersion && postNotification) {
+ updatesNotificationService.postNotification(
+ result
+ )
+ }
+
+ result
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+ }
}
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt
index 7da83b8d..88de0407 100644
--- a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt
+++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt
@@ -3,13 +3,14 @@ package ua.acclorite.book_story.domain.repository
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.Resource
-import ua.acclorite.book_story.presentation.data.MainState
+import ua.acclorite.book_story.presentation.data.MainSettingsState
import java.io.File
interface BookRepository {
@@ -57,7 +58,7 @@ interface BookRepository {
value: T
)
- suspend fun getAllSettings(scope: CoroutineScope): MainState
+ suspend fun getAllSettings(scope: CoroutineScope): MainSettingsState
suspend fun getFilesFromDevice(query: String = ""): Flow>>
@@ -76,4 +77,6 @@ interface BookRepository {
suspend fun deleteHistory(
history: List
)
+
+ suspend fun checkForUpdates(postNotification: Boolean): ReleaseResponse?
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/CheckForUpdates.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/CheckForUpdates.kt
new file mode 100644
index 00000000..e8177898
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/CheckForUpdates.kt
@@ -0,0 +1,12 @@
+package ua.acclorite.book_story.domain.use_case
+
+import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
+import ua.acclorite.book_story.domain.repository.BookRepository
+import javax.inject.Inject
+
+class CheckForUpdates @Inject constructor(private val repository: BookRepository) {
+
+ suspend fun execute(postNotification: Boolean): ReleaseResponse? {
+ return repository.checkForUpdates(postNotification)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetAllSettings.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetAllSettings.kt
index ab8a124e..3d8ad810 100644
--- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetAllSettings.kt
+++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetAllSettings.kt
@@ -2,14 +2,14 @@ package ua.acclorite.book_story.domain.use_case
import kotlinx.coroutines.CoroutineScope
import ua.acclorite.book_story.domain.repository.BookRepository
-import ua.acclorite.book_story.presentation.data.MainState
+import ua.acclorite.book_story.presentation.data.MainSettingsState
import javax.inject.Inject
class GetAllSettings @Inject constructor(
private val repository: BookRepository
) {
- suspend fun execute(scope: CoroutineScope): MainState {
+ suspend fun execute(scope: CoroutineScope): MainSettingsState {
return repository.getAllSettings(scope)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt
index 598c24a1..717e483c 100644
--- a/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt
+++ b/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt
@@ -20,4 +20,5 @@ object DataStoreConstants {
val LINE_HEIGHT = intPreferencesKey("line_height")
val PARAGRAPH_HEIGHT = intPreferencesKey("paragraph_height")
val PARAGRAPH_INDENTATION = booleanPreferencesKey("paragraph_indentation")
+ val CHECK_FOR_UPDATES = booleanPreferencesKey("check_for_updates")
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt
index 0c5d8309..87835a8a 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt
@@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.components
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
@@ -95,7 +94,7 @@ fun AnimatedTopAppBar(
label = stringResource(id = R.string.top_app_bar_anim_content_desc)
)
- AnimatedVisibility(
+ CustomAnimatedVisibility(
visible = content1Visibility ?: true,
enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)),
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
@@ -115,7 +114,7 @@ fun AnimatedTopAppBar(
}
if (content2Visibility != null) {
- AnimatedVisibility(
+ CustomAnimatedVisibility(
visible = content2Visibility,
enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)),
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
@@ -136,7 +135,7 @@ fun AnimatedTopAppBar(
}
if (content3Visibility != null) {
- AnimatedVisibility(
+ CustomAnimatedVisibility(
visible = content3Visibility,
enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)),
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CollapsibleScrollBehaivor.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CollapsibleUntilExitScrollBehaivor.kt
similarity index 90%
rename from app/src/main/java/ua/acclorite/book_story/presentation/components/CollapsibleScrollBehaivor.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/components/CollapsibleUntilExitScrollBehaivor.kt
index e4d3a42f..eebb797e 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CollapsibleScrollBehaivor.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CollapsibleUntilExitScrollBehaivor.kt
@@ -14,9 +14,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.collectLatest
+/**
+ * Collapsible Scroll Behavior with Lazy List State.
+ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState(
+fun TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState(
): Pair {
val listState = rememberLazyListState()
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomAnimatedVisibility.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomAnimatedVisibility.kt
new file mode 100644
index 00000000..8ef428a6
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomAnimatedVisibility.kt
@@ -0,0 +1,58 @@
+package ua.acclorite.book_story.presentation.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import kotlinx.coroutines.flow.first
+
+/**
+ * Custom [AnimatedVisibility]. Prevents bug that animation freezes halfway.
+ */
+@Composable
+fun CustomAnimatedVisibility(
+ modifier: Modifier = Modifier,
+ visible: Boolean,
+ enter: EnterTransition,
+ exit: ExitTransition,
+ content: @Composable () -> Unit
+) {
+ val visibleState = remember {
+ MutableTransitionState(visible)
+ }
+
+ LaunchedEffect(visible) {
+ if (visible == visibleState.targetState) {
+ return@LaunchedEffect
+ }
+
+ if (visibleState.isIdle) {
+ visibleState.targetState = visible
+ return@LaunchedEffect
+ }
+
+ snapshotFlow {
+ visibleState.isIdle
+ }.first {
+ if (it) {
+ visibleState.targetState = visible
+ }
+ it
+ }
+ }
+
+ AnimatedVisibility(
+ visibleState = visibleState,
+ modifier = modifier,
+ enter = enter,
+ exit = exit,
+ label = ""
+ ) {
+ content()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCoverImage.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCoverImage.kt
index c616ef22..7483ced1 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCoverImage.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCoverImage.kt
@@ -10,6 +10,9 @@ import coil.compose.AsyncImage
import coil.request.ImageRequest
import ua.acclorite.book_story.R
+/**
+ * Custom Cover Image. Has smooth appearing.
+ */
@Composable
fun CustomCoverImage(
uri: Uri,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSearchTextField.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSearchTextField.kt
new file mode 100644
index 00000000..4572256c
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSearchTextField.kt
@@ -0,0 +1,72 @@
+package ua.acclorite.book_story.presentation.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.style.TextOverflow
+
+/**
+ * Custom Search Text Field. Used in main screens for searching.
+ */
+@Composable
+fun CustomSearchTextField(
+ modifier: Modifier = Modifier,
+ query: String,
+ onQueryChange: (String) -> Unit,
+ onSearch: () -> Unit,
+ placeholder: String
+) {
+ val keyboardManager = LocalSoftwareKeyboardController.current
+
+ BasicTextField(
+ value = query,
+ singleLine = true,
+ textStyle = TextStyle(
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = MaterialTheme.typography.titleLarge.fontSize,
+ lineHeight = MaterialTheme.typography.titleLarge.lineHeight,
+ fontFamily = MaterialTheme.typography.titleLarge.fontFamily
+ ),
+ modifier = modifier,
+ onValueChange = onQueryChange,
+ keyboardOptions = KeyboardOptions(
+ KeyboardCapitalization.Words,
+ imeAction = ImeAction.Search
+ ),
+ keyboardActions = KeyboardActions(
+ onSearch = {
+ onSearch()
+ keyboardManager?.hide()
+ }
+ ),
+ cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant)
+ ) { innerText ->
+ Box(
+ modifier = Modifier.fillMaxHeight(),
+ contentAlignment = Alignment.CenterStart
+ ) {
+ if (query.isEmpty()) {
+ Text(
+ placeholder,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.titleLarge,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ innerText()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackbar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackbar.kt
index 6cf8a871..8034730b 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackbar.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSnackbar.kt
@@ -1,6 +1,5 @@
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
@@ -82,7 +81,7 @@ fun CustomSnackbar(modifier: Modifier = Modifier, snackbarState: SnackbarHostSta
showSnackbar = true
}
- AnimatedVisibility(
+ CustomAnimatedVisibility(
visible = showSnackbar,
enter = slideInVertically(tween(350)) { it } + fadeIn(tween(350)),
exit = slideOutVertically(tween(200)) { it } + fadeOut(tween(100))
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomTooltip.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomTooltip.kt
index f9a55814..98b4933b 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomTooltip.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomTooltip.kt
@@ -10,6 +10,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+/**
+ * Custom Tooltip.
+ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomTooltip(text: String, padding: Dp = 14.dp, content: @Composable () -> Unit) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt
index ccc3c36a..c41d7af2 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt
@@ -17,14 +17,15 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen
/**
* More drop down. Navigates to Settings, About and Help screens.
*/
@Composable
-fun MoreDropDown(navigator: Navigator) {
+fun MoreDropDown() {
+ val navigator = LocalNavigator.current
var showDropDown by remember { mutableStateOf(false) }
val startPadding = remember { 12.dp }
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt
index c03f898c..ce4ebfed 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt
@@ -11,19 +11,17 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NavigationItem
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen
/**
* Bottom navigation bar, uses default [NavigationBar].
*
- * @param navigator Navigator.
*/
@Composable
-fun BottomNavigationBar(
- navigator: Navigator
-) {
+fun BottomNavigationBar() {
var currentScreen: Screen? by remember { mutableStateOf(null) }
+ val navigator = LocalNavigator.current
LaunchedEffect(Unit) {
navigator.getCurrentScreen().collect {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt
index 1275ae40..b68ce236 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt
@@ -47,7 +47,8 @@ import ua.acclorite.book_story.R
* @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 disableOnClick Whether should all actions be disabled after clicking it.
+ * @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.
@@ -63,6 +64,7 @@ fun CustomDialogWithContent(
title: String,
description: String?,
actionText: String?,
+ disableOnClick: Boolean = true,
isActionEnabled: Boolean?,
onDismiss: () -> Unit,
onAction: () -> Unit,
@@ -158,7 +160,9 @@ fun CustomDialogWithContent(
) {
TextButton(
onClick = {
- actionClicked = true
+ if (disableOnClick) {
+ actionClicked = true
+ }
onDismiss()
},
enabled = !actionClicked
@@ -173,7 +177,9 @@ fun CustomDialogWithContent(
Spacer(modifier = Modifier.width(4.dp))
TextButton(
onClick = {
- actionClicked = true
+ if (disableOnClick) {
+ actionClicked = true
+ }
onAction()
},
enabled = isActionEnabled == true && !actionClicked
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt
index 702980c9..3b44bd6c 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt
@@ -49,7 +49,8 @@ import ua.acclorite.book_story.R
* @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 disableOnClick Whether should all actions be disabled after clicking it.
+ * @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.
@@ -65,6 +66,7 @@ fun CustomDialogWithLazyColumn(
title: String,
description: String?,
actionText: String?,
+ disableOnClick: Boolean = true,
isActionEnabled: Boolean?,
onDismiss: () -> Unit,
onAction: () -> Unit,
@@ -162,7 +164,9 @@ fun CustomDialogWithLazyColumn(
) {
TextButton(
onClick = {
- actionClicked = true
+ if (disableOnClick) {
+ actionClicked = true
+ }
onDismiss()
},
enabled = !actionClicked
@@ -177,7 +181,9 @@ fun CustomDialogWithLazyColumn(
Spacer(modifier = Modifier.width(4.dp))
TextButton(
onClick = {
- actionClicked = true
+ if (disableOnClick) {
+ actionClicked = true
+ }
onAction()
},
enabled = isActionEnabled == true && !actionClicked
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/SelectableDialogItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/SelectableDialogItem.kt
index 7e476e36..7a197c98 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/SelectableDialogItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/SelectableDialogItem.kt
@@ -15,18 +15,24 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Selectable dialog item. Perfectly combines with [CustomDialogWithLazyColumn].
*/
@Composable
-fun SelectableDialogItem(selected: Boolean, title: String, onClick: () -> Unit) {
+fun SelectableDialogItem(
+ selected: Boolean,
+ title: String,
+ horizontalPadding: Dp = 24.dp,
+ onClick: () -> Unit
+) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = !selected) { onClick() }
- .padding(horizontal = 24.dp, vertical = 12.dp),
+ .padding(horizontal = horizontalPadding, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt
index 36abcf96..d6123fb7 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt
@@ -21,24 +21,23 @@ 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.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.LayoutDirection
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.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen
/**
* Custom Navigation Rail. It is used to be shown on Tablets.
*/
@Composable
-fun BoxScope.CustomNavigationRail(
- navigator: Navigator,
- layoutDirection: LayoutDirection
-) {
+fun BoxScope.CustomNavigationRail() {
var currentScreen: Screen? by remember { mutableStateOf(null) }
+ val navigator = LocalNavigator.current
+ val layoutDirection = LocalLayoutDirection.current
LaunchedEffect(Unit) {
navigator.getCurrentScreen().collect {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt
index 5c0ea80c..2217ea19 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt
@@ -15,4 +15,5 @@ sealed class MainEvent {
data class OnChangeBackgroundColor(val color: Long) : MainEvent()
data class OnChangeFontColor(val color: Long) : MainEvent()
data class OnChangeShowStartScreen(val bool: Boolean) : MainEvent()
+ data class OnChangeCheckForUpdates(val bool: Boolean) : MainEvent()
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainSettingsState.kt
similarity index 87%
rename from app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt
rename to app/src/main/java/ua/acclorite/book_story/presentation/data/MainSettingsState.kt
index c0e525b9..fe688c05 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainSettingsState.kt
@@ -21,7 +21,7 @@ import java.util.Locale
* Main State. All app's settings are here. Wrapped in SavedStateHandle, so it won't reset.
*/
@Immutable
-data class MainState(
+data class MainSettingsState(
val language: String? = null,
val theme: Theme? = null,
val darkTheme: DarkTheme? = null,
@@ -36,6 +36,7 @@ data class MainState(
val backgroundColor: Long? = null,
val fontColor: Long? = null,
val showStartScreen: Boolean? = null,
+ val checkForUpdates: Boolean? = null,
) : Parcelable {
constructor(parcel: Parcel) : this(
// String
@@ -68,6 +69,9 @@ data class MainState(
// Boolean
showStartScreen = if (parcel.readByte().toInt() != 0) parcel.readByte()
.toInt() == 1 else null,
+ // Boolean
+ checkForUpdates = if (parcel.readByte().toInt() != 0) parcel.readByte()
+ .toInt() == 1 else null,
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
@@ -107,25 +111,28 @@ data class MainState(
// Show Start Screen
parcel.writeByte((if (showStartScreen != null) 1 else 0).toByte())
showStartScreen?.let { parcel.writeByte(if (it) 1 else 0) }
+ // Check For Updates
+ parcel.writeByte((if (checkForUpdates != null) 1 else 0).toByte())
+ checkForUpdates?.let { parcel.writeByte(if (it) 1 else 0) }
}
override fun describeContents(): Int {
return 0
}
- companion object CREATOR : Parcelable.Creator {
- override fun createFromParcel(parcel: Parcel): MainState {
- return MainState(parcel)
+ companion object CREATOR : Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel): MainSettingsState {
+ return MainSettingsState(parcel)
}
- override fun newArray(size: Int): Array {
+ override fun newArray(size: Int): Array {
return arrayOfNulls(size)
}
/**
- * Initializes [MainState] by given [Map].
+ * Initializes [MainSettingsState] by given [Map].
*/
- fun initialize(data: Map): MainState {
+ fun initialize(data: Map): MainSettingsState {
DataStoreConstants.apply {
val language: String = data[LANGUAGE.name] as? String ?: if (
@@ -176,8 +183,10 @@ data class MainState(
val paragraphIndentation: Boolean = data[PARAGRAPH_INDENTATION.name] as? Boolean
?: false
+ val checkForUpdates: Boolean = data[CHECK_FOR_UPDATES.name] as? Boolean
+ ?: false
- return MainState(
+ return MainSettingsState(
language = language,
theme = theme.toTheme(),
darkTheme = darkTheme.toDarkTheme(),
@@ -191,7 +200,8 @@ data class MainState(
fontSize = fontSize,
lineHeight = lineHeight,
paragraphHeight = paragraphHeight,
- paragraphIndentation = paragraphIndentation
+ paragraphIndentation = paragraphIndentation,
+ checkForUpdates = checkForUpdates
)
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt
index c09b3abb..889688f5 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt
@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ua.acclorite.book_story.domain.use_case.ChangeLanguage
+import ua.acclorite.book_story.domain.use_case.CheckForUpdates
import ua.acclorite.book_story.domain.use_case.GetAllSettings
import ua.acclorite.book_story.domain.use_case.SetDatastore
import ua.acclorite.book_story.domain.util.Constants
@@ -33,6 +34,7 @@ class MainViewModel @Inject constructor(
private val setDatastore: SetDatastore,
private val changeLanguage: ChangeLanguage,
+ private val checkForUpdates: CheckForUpdates,
private val getAllSettings: GetAllSettings
) : ViewModel() {
@@ -42,8 +44,8 @@ class MainViewModel @Inject constructor(
private val isSettingsReady = MutableStateFlow(false)
private val isViewModelReady = MutableStateFlow(false)
- private val _state: MutableStateFlow = MutableStateFlow(
- stateHandle[Constants.MAIN_STATE] ?: MainState()
+ private val _state: MutableStateFlow = MutableStateFlow(
+ stateHandle[Constants.MAIN_STATE] ?: MainSettingsState()
)
val state = _state.asStateFlow()
@@ -203,11 +205,22 @@ class MainViewModel @Inject constructor(
}
}
}
+
+ is MainEvent.OnChangeCheckForUpdates -> {
+ viewModelScope.launch(Dispatchers.IO) {
+ setDatastore.execute(DataStoreConstants.CHECK_FOR_UPDATES, event.bool)
+ updateStateWithSavedHandle {
+ it.copy(
+ checkForUpdates = event.bool
+ )
+ }
+ }
+ }
}
}
fun init(
- libraryViewModel: LibraryViewModel,
+ libraryViewModel: LibraryViewModel
) {
viewModelScope.launch(Dispatchers.Main) {
val settings = getAllSettings.execute(viewModelScope)
@@ -215,6 +228,14 @@ class MainViewModel @Inject constructor(
// All additional execution
changeLanguage.execute(settings.language!!)
+ if (settings.checkForUpdates == true) {
+ viewModelScope.launch(Dispatchers.IO) {
+ checkForUpdates.execute(
+ postNotification = true
+ )
+ }
+ }
+
updateStateWithSavedHandle {
settings
}
@@ -249,10 +270,10 @@ class MainViewModel @Inject constructor(
}
/**
- * Updates [MainState] along with [SavedStateHandle].
+ * Updates [MainSettingsState] along with [SavedStateHandle].
*/
private fun updateStateWithSavedHandle(
- function: (MainState) -> MainState
+ function: (MainSettingsState) -> MainSettingsState
) {
_state.update {
stateHandle[Constants.MAIN_STATE] = function(it)
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt
index df0047fa..6799720d 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt
@@ -1,3 +1,5 @@
+@file:Suppress("MemberVisibilityCanBePrivate")
+
package ua.acclorite.book_story.presentation.data
import android.annotation.SuppressLint
@@ -5,19 +7,33 @@ import android.os.Parcelable
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.dp
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -30,6 +46,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
+import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.ui.Transitions
import java.io.Serializable
@@ -39,6 +56,10 @@ private const val BACKSTACK = "back_stack"
private const val USE_BACK_ANIM = "use_back_animation"
private const val ARGUMENTS = "arguments"
+val LocalNavigator = compositionLocalOf {
+ error("Cannot initialize Navigator.")
+}
+
/**
* All screens are listed here, later each screen will be passed as a param for [Navigator.composable] function.
*/
@@ -71,7 +92,7 @@ data class Argument(
) : Parcelable
/**
- * Navigator. Using for navigation between screens.
+ * Navigator. Using to navigate between screens.
*
* [Navigator.currentScreen] param represents current [Screen].
* [Navigator.navigate] navigates to [Screen] passed as param.
@@ -134,7 +155,7 @@ class Navigator @AssistedInject constructor(
useBackAnimation: Boolean,
vararg args: Argument
) = viewModelScope.launch(Dispatchers.Default) {
- if (backStack.value.last() == screen) {
+ if (backStack.value.lastOrNull() == screen) {
backStack.value.removeLast()
}
@@ -155,6 +176,10 @@ class Navigator @AssistedInject constructor(
}
}
+ fun clearBackStack() {
+ backStack.value.clear()
+ }
+
fun canGoBack(): Boolean {
return backStack.value.isNotEmpty()
}
@@ -163,6 +188,10 @@ class Navigator @AssistedInject constructor(
return currentScreen
}
+ fun getUseBackAnim(): StateFlow {
+ return useBackAnimation
+ }
+
/**
* Animated Screen. Used in [NavigationHost]. Be sure to not use the same [screen] parameter twice, it'll override the highest one in your code.
*
@@ -177,22 +206,94 @@ class Navigator @AssistedInject constructor(
@Composable
fun composable(
screen: Screen,
- enterAnim: EnterTransition = Transitions.FadeTransitionIn,
+ enterAnim: EnterTransition = Transitions.SlidingTransitionIn,
backEnterAnim: EnterTransition = Transitions.BackSlidingTransitionIn,
- exitAnim: ExitTransition = Transitions.FadeTransitionOut,
+ exitAnim: ExitTransition = Transitions.SlidingTransitionOut,
backExitAnim: ExitTransition = Transitions.BackSlidingTransitionOut,
content: @Composable () -> Unit
) {
- AnimatedVisibility(
+ CustomAnimatedVisibility(
visible = getCurrentScreen().collectAsState().value == screen,
enter = if (!useBackAnimation.collectAsState().value) enterAnim else backEnterAnim,
- exit = if (!useBackAnimation.collectAsState().value) exitAnim else backExitAnim,
- label = ""
+ exit = if (!useBackAnimation.collectAsState().value) exitAnim else backExitAnim
) {
content()
}
}
+ @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
+ @SuppressLint("ComposableNaming")
+ @Composable
+ fun navigation(
+ vararg screens: Screen,
+ bottomBar: @Composable () -> Unit,
+ navigationRail: @Composable BoxScope.() -> Unit,
+ content: @Composable () -> Unit
+ ) {
+ val activity = LocalContext.current as ComponentActivity
+ val currentScreen by getCurrentScreen().collectAsState()
+ val useBackAnimation by getUseBackAnim().collectAsState()
+ val shouldShow by remember(currentScreen) {
+ derivedStateOf {
+ screens.any { it == currentScreen }
+ }
+ }
+
+ val windowClass = calculateWindowSizeClass(activity = activity)
+ val tabletUI = remember(windowClass) {
+ windowClass.widthSizeClass != WindowWidthSizeClass.Compact
+ }
+ val layoutDirection = LocalLayoutDirection.current
+
+ CustomAnimatedVisibility(
+ visible = shouldShow,
+ enter = if (useBackAnimation) Transitions.BackSlidingTransitionIn
+ else Transitions.SlidingTransitionIn,
+ exit = if (useBackAnimation) Transitions.BackSlidingTransitionOut
+ else Transitions.SlidingTransitionOut
+ ) {
+ Scaffold(
+ bottomBar = {
+ if (!tabletUI) {
+ bottomBar()
+ }
+ },
+ containerColor = MaterialTheme.colorScheme.surface
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(
+ end = it.calculateEndPadding(
+ layoutDirection
+ ),
+ bottom = it.calculateBottomPadding(),
+ start = if (tabletUI) {
+ 80.dp + it.calculateStartPadding(
+ layoutDirection
+ )
+ } else {
+ it.calculateStartPadding(
+ layoutDirection
+ )
+ },
+ )
+ ) {
+ content()
+ }
+
+ if (tabletUI) {
+ navigationRail()
+ }
+ }
+ }
+ }
+ }
+
@AssistedFactory
interface Factory {
fun create(startScreen: Screen): Navigator
@@ -214,7 +315,6 @@ fun NavigationHost(
content: @Composable Navigator.() -> Unit
) {
val activity = LocalContext.current as ComponentActivity
-
val navigator by activity.viewModels(
extrasProducer = {
activity
@@ -234,10 +334,12 @@ fun NavigationHost(
}
Box(
- modifier = Modifier
+ Modifier
.fillMaxSize()
.background(colorBetweenAnimations)
)
- content(navigator)
+ CompositionLocalProvider(LocalNavigator provides navigator) {
+ content(navigator)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/AboutScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/AboutScreen.kt
index 34c6f759..1577cfb2 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/AboutScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/AboutScreen.kt
@@ -21,30 +21,58 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.GoBackButton
-import ua.acclorite.book_story.presentation.components.collapsibleScrollBehaviorWithLazyListState
+import ua.acclorite.book_story.presentation.components.collapsibleUntilExitScrollBehaviorWithLazyListState
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.about.components.AboutItem
+import ua.acclorite.book_story.presentation.screens.about.components.AboutUpdateDialog
import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent
+import ua.acclorite.book_story.presentation.screens.about.data.AboutState
import ua.acclorite.book_story.presentation.screens.about.data.AboutViewModel
+@Composable
+fun AboutScreenRoot() {
+ val navigator = LocalNavigator.current
+ val aboutViewModel: AboutViewModel = hiltViewModel()
+
+ val state = aboutViewModel.state.collectAsState()
+
+ AboutScreen(
+ state = state,
+ navigator = navigator,
+ onEvent = aboutViewModel::onEvent
+ )
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun AboutScreen(
- viewModel: AboutViewModel = hiltViewModel(),
- navigator: Navigator
+private fun AboutScreen(
+ state: State,
+ navigator: Navigator,
+ onEvent: (AboutEvent) -> Unit
) {
val context = LocalContext.current
- val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
+ val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
+
+ if (state.value.showUpdateDialog) {
+ AboutUpdateDialog(
+ state = state,
+ onEvent = onEvent
+ )
+ }
Scaffold(
Modifier
@@ -104,36 +132,32 @@ fun AboutScreen(
item {
AboutItem(
title = stringResource(id = R.string.app_version_option),
- description = stringResource(
- id = R.string.app_version_option_desc,
- stringResource(id = R.string.app_version)
- ),
- isOnClickEnabled = false
- )
- }
-
- item {
- AboutItem(
- title = stringResource(id = R.string.all_releases_option),
- description = stringResource(
- id = R.string.all_releases_option_desc
- )
+ description = buildAnnotatedString {
+ append(
+ stringResource(
+ id = R.string.app_version_option_desc_1,
+ context.getString(R.string.app_version)
+ )
+ )
+ append("\n")
+ append(stringResource(id = R.string.app_version_option_desc_2))
+ }
) {
- Toast.makeText(
- context,
- "${context.getString(R.string.app_version_option)}:" +
- " ${context.getString(R.string.app_version)}",
- Toast.LENGTH_LONG
- ).show()
- viewModel.onEvent(
- AboutEvent.OnNavigateToBrowserPage(
- context.getString(R.string.releases_page),
- context,
- noAppsFound = {
+ onEvent(
+ AboutEvent.OnCheckForUpdates(
+ context = context,
+ noUpdatesFound = {
Toast.makeText(
context,
- context.getString(R.string.error_no_browser),
- Toast.LENGTH_SHORT
+ context.getString(R.string.no_updates),
+ Toast.LENGTH_LONG
+ ).show()
+ },
+ error = {
+ Toast.makeText(
+ context,
+ context.getString(R.string.error_check_internet),
+ Toast.LENGTH_LONG
).show()
}
)
@@ -143,12 +167,10 @@ fun AboutScreen(
item {
AboutItem(
- title = stringResource(id = R.string.issues_option),
- description = stringResource(
- id = R.string.issues_option_desc
- )
+ title = stringResource(id = R.string.report_bug_option),
+ description = null
) {
- viewModel.onEvent(
+ onEvent(
AboutEvent.OnNavigateToBrowserPage(
context.getString(R.string.issues_page),
context,
@@ -167,11 +189,9 @@ fun AboutScreen(
item {
AboutItem(
title = stringResource(id = R.string.project_page_option),
- description = stringResource(
- id = R.string.project_page_option_desc
- )
+ description = null
) {
- viewModel.onEvent(
+ onEvent(
AboutEvent.OnNavigateToBrowserPage(
context.getString(R.string.project_page),
context,
@@ -187,37 +207,12 @@ fun AboutScreen(
}
}
- item {
- AboutItem(
- title = stringResource(id = R.string.my_page_option),
- description = stringResource(
- id = R.string.my_page_option_desc
- )
- ) {
- viewModel.onEvent(
- AboutEvent.OnNavigateToBrowserPage(
- context.getString(R.string.my_page),
- context,
- noAppsFound = {
- Toast.makeText(
- context,
- context.getString(R.string.error_no_browser),
- Toast.LENGTH_SHORT
- ).show()
- }
- )
- )
- }
- }
-
item {
AboutItem(
title = stringResource(id = R.string.support_option),
- description = stringResource(
- id = R.string.support_option_desc
- )
+ description = null
) {
- viewModel.onEvent(
+ onEvent(
AboutEvent.OnNavigateToBrowserPage(
context.getString(R.string.support_page),
context,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutItem.kt
index c35e2d4e..9d64e2a4 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutItem.kt
@@ -3,15 +3,13 @@ package ua.acclorite.book_story.presentation.screens.about.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -24,7 +22,7 @@ import androidx.compose.ui.unit.sp
fun AboutItem(
modifier: Modifier = Modifier,
title: String,
- description: String?,
+ description: AnnotatedString?,
verticalPadding: Dp = 12.dp,
isOnClickEnabled: Boolean = true,
onClick: () -> Unit = {}
@@ -36,21 +34,19 @@ fun AboutItem(
onClick()
}
.padding(horizontal = 18.dp, vertical = verticalPadding),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.Start
+ verticalArrangement = Arrangement.Center
) {
Text(
title,
- style = MaterialTheme.typography.bodyLarge,
- color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 18.sp
)
- Spacer(modifier = Modifier.height(2.dp))
description?.let {
Text(
it,
overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.bodyMedium,
+ style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutUpdateDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutUpdateDialog.kt
new file mode 100644
index 00000000..53446568
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutUpdateDialog.kt
@@ -0,0 +1,58 @@
+package ua.acclorite.book_story.presentation.screens.about.components
+
+import android.widget.Toast
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Update
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
+import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent
+import ua.acclorite.book_story.presentation.screens.about.data.AboutState
+
+/**
+ * Update dialog. If there is app update transfers to the download page.
+ */
+@Composable
+fun AboutUpdateDialog(
+ state: State,
+ onEvent: (AboutEvent) -> Unit
+) {
+ val context = LocalContext.current
+
+ val update = remember(state.value.showUpdateDialog) {
+ state.value.updateInfo!!
+ }
+
+ CustomDialogWithContent(
+ title = stringResource(id = R.string.update_query, update.tag_name),
+ imageVectorIcon = Icons.Default.Update,
+ description = stringResource(
+ id = R.string.update_app_description
+ ),
+ actionText = stringResource(id = R.string.download),
+ isActionEnabled = true,
+ disableOnClick = false,
+ onDismiss = { onEvent(AboutEvent.OnDismissUpdateDialog) },
+ onAction = {
+ onEvent(
+ AboutEvent.OnNavigateToBrowserPage(
+ page = context.getString(R.string.download_latest_release_page),
+ context = context,
+ noAppsFound = {
+ Toast.makeText(
+ context,
+ context.getString(R.string.error_no_browser),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ )
+ )
+ onEvent(AboutEvent.OnDismissUpdateDialog)
+ },
+ withDivider = false,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutEvent.kt
index 7466e796..d00c5fc2 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutEvent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutEvent.kt
@@ -1,11 +1,21 @@
package ua.acclorite.book_story.presentation.screens.about.data
import android.content.Context
+import androidx.compose.runtime.Immutable
+@Immutable
sealed class AboutEvent {
data class OnNavigateToBrowserPage(
val page: String,
val context: Context,
val noAppsFound: () -> Unit
) : AboutEvent()
+
+ data class OnCheckForUpdates(
+ val context: Context,
+ val noUpdatesFound: () -> Unit,
+ val error: () -> Unit
+ ) : AboutEvent()
+
+ data object OnDismissUpdateDialog : AboutEvent()
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutState.kt
new file mode 100644
index 00000000..c3f9b35e
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutState.kt
@@ -0,0 +1,10 @@
+package ua.acclorite.book_story.presentation.screens.about.data
+
+import androidx.compose.runtime.Immutable
+import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
+
+@Immutable
+data class AboutState(
+ val showUpdateDialog: Boolean = false,
+ val updateInfo: ReleaseResponse? = null
+)
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutViewModel.kt
index 7a6e7a1e..21b58e14 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutViewModel.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutViewModel.kt
@@ -5,14 +5,22 @@ import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.domain.use_case.CheckForUpdates
import javax.inject.Inject
@HiltViewModel
class AboutViewModel @Inject constructor(
-
+ private val checkForUpdates: CheckForUpdates,
) : ViewModel() {
+ private val _state = MutableStateFlow(AboutState())
+ val state = _state.asStateFlow()
+
fun onEvent(event: AboutEvent) {
when (event) {
is AboutEvent.OnNavigateToBrowserPage -> {
@@ -30,6 +38,40 @@ class AboutViewModel @Inject constructor(
event.noAppsFound()
}
}
+
+ is AboutEvent.OnCheckForUpdates -> {
+ viewModelScope.launch {
+ val result = checkForUpdates.execute(false)
+
+ if (result == null) {
+ event.error()
+ return@launch
+ }
+
+ val version = result.tag_name.substringAfterLast("v")
+ val currentVersion = event.context.getString(R.string.app_version)
+
+ if (version == currentVersion) {
+ event.noUpdatesFound()
+ return@launch
+ }
+
+ _state.update {
+ it.copy(
+ showUpdateDialog = true,
+ updateInfo = result
+ )
+ }
+ }
+ }
+
+ is AboutEvent.OnDismissUpdateDialog -> {
+ _state.update {
+ it.copy(
+ showUpdateDialog = false
+ )
+ }
+ }
}
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt
index 16f986ba..51591384 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt
@@ -2,9 +2,11 @@ package ua.acclorite.book_story.presentation.screens.book_info
import android.widget.Toast
import androidx.activity.compose.BackHandler
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandHorizontally
+import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -22,6 +24,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh
@@ -39,6 +42,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -51,11 +55,14 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
+import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.components.CustomSnackbar
import ua.acclorite.book_story.presentation.components.GoBackButton
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoBackground
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDescriptionSection
@@ -68,7 +75,9 @@ import ua.acclorite.book_story.presentation.screens.book_info.components.details
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.BookInfoState
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.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
@@ -77,25 +86,51 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions
+@Composable
+fun BookInfoScreenRoot() {
+ val navigator = LocalNavigator.current
+
+ val viewModel: BookInfoViewModel = hiltViewModel()
+ val libraryViewModel: LibraryViewModel = hiltViewModel()
+ val historyViewModel: HistoryViewModel = hiltViewModel()
+ val browseViewModel: BrowseViewModel = hiltViewModel()
+
+ val state = viewModel.state.collectAsState()
+
+ LaunchedEffect(Unit) {
+ viewModel.init(
+ navigator
+ )
+ }
+
+ BookInfoScreen(
+ state = state,
+ navigator = navigator,
+ onEvent = viewModel::onEvent,
+ onLibraryEvent = libraryViewModel::onEvent,
+ onBrowseEvent = browseViewModel::onEvent,
+ onHistoryEvent = historyViewModel::onEvent
+ )
+}
+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
-fun BookInfoScreen(
- viewModel: BookInfoViewModel,
- libraryViewModel: LibraryViewModel,
- browseViewModel: BrowseViewModel,
- historyViewModel: HistoryViewModel,
- navigator: Navigator
+private fun BookInfoScreen(
+ state: State,
+ navigator: Navigator,
+ onEvent: (BookInfoEvent) -> Unit,
+ onLibraryEvent: (LibraryEvent) -> Unit,
+ onBrowseEvent: (BrowseEvent) -> Unit,
+ onHistoryEvent: (HistoryEvent) -> Unit
) {
val context = LocalContext.current
-
- val state by viewModel.state.collectAsState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val listState = rememberLazyListState()
val snackbarState = remember { SnackbarHostState() }
val refreshState = rememberPullRefreshState(
- refreshing = state.isRefreshing || state.isLoadingUpdate,
+ refreshing = state.value.isRefreshing || state.value.isLoadingUpdate,
onRefresh = {
- viewModel.onEvent(
+ onEvent(
BookInfoEvent.OnLoadUpdate(
snackbarState,
context
@@ -105,45 +140,43 @@ fun BookInfoScreen(
)
val firstVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } }
- LaunchedEffect(Unit) {
- viewModel.init(
- navigator
- )
- }
-
- if (state.showChangeCoverBottomSheet) {
+ if (state.value.showChangeCoverBottomSheet) {
BookInfoChangeCoverBottomSheet(
- libraryViewModel = libraryViewModel,
- historyViewModel = historyViewModel,
- viewModel = viewModel
+ state = state,
+ onEvent = onEvent,
+ onLibraryUpdateEvent = onLibraryEvent,
+ onHistoryUpdateEvent = onHistoryEvent
)
}
- if (state.showDetailsBottomSheet) {
- BookInfoDetailsBottomSheet(viewModel = viewModel)
+ if (state.value.showDetailsBottomSheet) {
+ BookInfoDetailsBottomSheet(
+ state = state,
+ onEvent = onEvent
+ )
}
- if (state.showDeleteDialog) {
+ if (state.value.showDeleteDialog) {
BookInfoDeleteDialog(
- libraryViewModel = libraryViewModel,
- browseViewModel = browseViewModel,
- historyViewModel = historyViewModel,
- viewModel = viewModel,
- navigator = navigator
+ onEvent = onEvent,
+ onLibraryLoadEvent = onLibraryEvent,
+ onHistoryLoadEvent = onHistoryEvent,
+ onBrowseLoadEvent = onBrowseEvent
)
}
- if (state.showMoveDialog) {
+ if (state.value.showMoveDialog) {
BookInfoMoveDialog(
- libraryViewModel = libraryViewModel,
- historyViewModel = historyViewModel,
- viewModel = viewModel,
- navigator = navigator
+ state = state,
+ onEvent = onEvent,
+ onLibraryEvent = onLibraryEvent,
+ onHistoryUpdateEvent = onHistoryEvent
)
}
- if (state.showConfirmUpdateDialog) {
+ if (state.value.showConfirmUpdateDialog) {
BookInfoConfirmUpdateDialog(
- libraryViewModel = libraryViewModel,
- historyViewModel = historyViewModel,
- viewModel = viewModel,
- snackbarHostState = snackbarState
+ state = state,
+ snackbarHostState = snackbarState,
+ onEvent = onEvent,
+ onLibraryUpdateEvent = onLibraryEvent,
+ onHistoryUpdateEvent = onHistoryEvent
)
}
@@ -161,14 +194,24 @@ fun BookInfoScreen(
isTopBarScrolled = null,
content1NavigationIcon = {
- GoBackButton(navigator = navigator, enabled = !state.isRefreshing) {
- viewModel.onEvent(BookInfoEvent.OnCancelUpdate)
+ if (state.value.editTitle) {
+ CustomIconButton(
+ icon = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = R.string.hide_edit_title_content_desc,
+ disableOnClick = true
+ ) {
+ onEvent(BookInfoEvent.OnShowHideEditTitle)
+ }
+ } else {
+ GoBackButton(navigator = navigator, enabled = !state.value.isRefreshing) {
+ onEvent(BookInfoEvent.OnCancelUpdate)
+ }
}
},
content1Title = {
DefaultTransition(visible = firstVisibleItemIndex > 0) {
Text(
- state.book.title,
+ state.value.book.title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
@@ -181,9 +224,9 @@ fun BookInfoScreen(
icon = Icons.Default.Refresh,
contentDescription = R.string.refresh_book_content_desc,
disableOnClick = false,
- enabled = !state.isRefreshing && !state.isLoadingUpdate
+ enabled = !state.value.isRefreshing && !state.value.isLoadingUpdate
) {
- viewModel.onEvent(
+ onEvent(
BookInfoEvent.OnLoadUpdate(
snackbarState,
context
@@ -193,15 +236,16 @@ fun BookInfoScreen(
Box {
DefaultTransition(
- visible = !state.editTitle
+ visible = !state.value.editTitle
) {
BookInfoMoreDropDown(
- viewModel = viewModel,
+ state = state,
+ onEvent = onEvent,
snackbarState = snackbarState
)
}
- androidx.compose.animation.AnimatedVisibility(
- visible = state.editTitle,
+ CustomAnimatedVisibility(
+ visible = state.value.editTitle,
enter = Transitions.DefaultTransitionIn,
exit = fadeOut(tween(200))
) {
@@ -209,21 +253,22 @@ fun BookInfoScreen(
icon = Icons.Default.Done,
contentDescription = R.string.apply_changes_content_desc,
disableOnClick = true,
- enabled = !state.isRefreshing &&
- state.titleValue.isNotBlank() &&
- state.titleValue.trim() !=
- state.book.title.trim(),
- color = if (state.titleValue.isNotBlank()
- && state.titleValue != state.book.title
+ enabled = !state.value.isRefreshing &&
+ state.value.titleValue.isNotBlank() &&
+ state.value.titleValue.trim() !=
+ state.value.book.title.trim(),
+ color = if (state.value.titleValue.isNotBlank()
+ && state.value.titleValue != state.value.book.title
) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurfaceVariant
) {
- viewModel.onEvent(BookInfoEvent.OnUpdateTitle(
- refreshList = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
- }
- ))
+ onEvent(
+ BookInfoEvent.OnUpdateTitle(
+ refreshList = {
+ onLibraryEvent(LibraryEvent.OnUpdateBook(it))
+ onHistoryEvent(HistoryEvent.OnUpdateBook(it))
+ }
+ ))
Toast.makeText(
context,
context.getString(R.string.title_changed),
@@ -253,17 +298,17 @@ fun BookInfoScreen(
) {
item {
Box(modifier = Modifier.fillMaxWidth()) {
- if (state.book.coverImage != null) {
+ if (state.value.book.coverImage != null) {
BookInfoBackground(
height = paddingValues.calculateTopPadding() + 12.dp + 195.dp,
- image = state.book.coverImage!!
+ image = state.value.book.coverImage!!
)
}
Column(Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding() + 12.dp))
// Info
- BookInfoInfoSection(viewModel = viewModel, book = state.book)
+ BookInfoInfoSection(state = state, onEvent = onEvent)
}
}
}
@@ -271,20 +316,20 @@ fun BookInfoScreen(
item {
Spacer(modifier = Modifier.height(24.dp))
// Statistic
- BookInfoStatisticSection(book = state.book)
+ BookInfoStatisticSection(state = state)
}
item {
Spacer(modifier = Modifier.height(24.dp))
// Description
- BookInfoDescriptionSection(book = state.book)
+ BookInfoDescriptionSection(state = state)
}
}
FloatingActionButton(
onClick = {
- if (!state.isRefreshing) {
- viewModel.onEvent(BookInfoEvent.OnNavigateToReaderScreen(navigator))
+ if (!state.value.isRefreshing) {
+ onEvent(BookInfoEvent.OnNavigateToReaderScreen(navigator))
}
},
shape = MaterialTheme.shapes.large,
@@ -302,12 +347,14 @@ fun BookInfoScreen(
contentDescription = stringResource(id = R.string.continue_reading_content_desc),
Modifier.size(24.dp)
)
- AnimatedVisibility(
- visible = !listState.canScrollBackward
+ CustomAnimatedVisibility(
+ visible = !listState.canScrollBackward,
+ enter = fadeIn() + expandHorizontally(),
+ exit = fadeOut() + shrinkHorizontally()
) {
Text(
text = stringResource(
- if (state.book.progress == 0f) R.string.start
+ if (state.value.book.progress == 0f) R.string.start
else R.string.continue_read
),
style = MaterialTheme.typography.labelLarge,
@@ -321,7 +368,7 @@ fun BookInfoScreen(
)
PullRefreshIndicator(
- state.isRefreshing || state.isLoadingUpdate,
+ state.value.isRefreshing || state.value.isLoadingUpdate,
refreshState,
Modifier
.align(Alignment.TopCenter)
@@ -333,8 +380,13 @@ fun BookInfoScreen(
}
BackHandler {
- if (!state.isRefreshing) {
- viewModel.onEvent(BookInfoEvent.OnCancelUpdate)
+ if (state.value.editTitle) {
+ onEvent(BookInfoEvent.OnShowHideEditTitle)
+ return@BackHandler
+ }
+
+ if (!state.value.isRefreshing) {
+ onEvent(BookInfoEvent.OnCancelUpdate)
navigator.navigateBack()
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt
index 8c06c645..1ff0e56b 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt
@@ -7,21 +7,22 @@ import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.domain.model.Book
+import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
/**
* Description section.
*/
@Composable
-fun BookInfoDescriptionSection(book: Book) {
+fun BookInfoDescriptionSection(state: State) {
SelectionContainer {
Text(
- if (book.description?.isNotBlank() == true) book.description
+ if (state.value.book.description?.isNotBlank() == true) state.value.book.description!!
else stringResource(id = R.string.error_no_description),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt
index b7e8c29c..b2495a7f 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt
@@ -24,8 +24,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -35,23 +34,23 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.components.CustomCoverImage
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.BookInfoState
/**
* Info section.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
-fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
- val state by viewModel.state.collectAsState()
+fun BookInfoInfoSection(
+ state: State,
+ onEvent: (BookInfoEvent) -> Unit
+) {
val focusRequester = remember { FocusRequester() }
Row(
@@ -70,13 +69,13 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
indication = null,
onClick = {},
onLongClick = {
- viewModel.onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
+ onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
}
)
) {
- if (book.coverImage != null) {
+ if (state.value.book.coverImage != null) {
CustomCoverImage(
- uri = book.coverImage,
+ uri = state.value.book.coverImage!!,
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.medium)
@@ -97,9 +96,9 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
Spacer(modifier = Modifier.width(12.dp))
Column(verticalArrangement = Arrangement.Center) {
- if (!state.editTitle) {
+ if (!state.value.editTitle) {
Text(
- book.title,
+ state.value.book.title,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
@@ -109,7 +108,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
indication = null,
onClick = {},
onLongClick = {
- viewModel.onEvent(BookInfoEvent.OnShowHideEditTitle)
+ onEvent(BookInfoEvent.OnShowHideEditTitle)
}
),
maxLines = 4,
@@ -118,22 +117,19 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
} else {
BasicTextField(
maxLines = 4,
- value = state.titleValue,
- textStyle = TextStyle(
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.headlineSmall.fontSize,
- lineHeight = MaterialTheme.typography.headlineSmall.lineHeight,
- fontFamily = MaterialTheme.typography.headlineSmall.fontFamily
+ value = state.value.titleValue,
+ textStyle = MaterialTheme.typography.headlineSmall.copy(
+ color = MaterialTheme.colorScheme.onSurface
),
onValueChange = {
- if (it.length < 100 || it.length < state.titleValue.length) {
- viewModel.onEvent(BookInfoEvent.OnTitleValueChange(it))
+ if (it.length < 100 || it.length < state.value.titleValue.length) {
+ onEvent(BookInfoEvent.OnTitleValueChange(it))
}
},
modifier = Modifier
.focusRequester(focusRequester)
.onGloballyPositioned {
- viewModel.onEvent(BookInfoEvent.OnRequestFocus(focusRequester))
+ onEvent(BookInfoEvent.OnRequestFocus(focusRequester))
},
keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words),
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant)
@@ -149,7 +145,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
Spacer(modifier = Modifier.height(4.dp))
SelectionContainer {
Text(
- book.author.asString(),
+ state.value.book.author.asString(),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt
index 0324b020..dd8e1b11 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt
@@ -9,7 +9,7 @@ import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -21,14 +21,17 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CustomDropDownMenuItem
import ua.acclorite.book_story.presentation.components.CustomIconButton
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.BookInfoState
/**
* Book info more dropdown. Has three options: Delete, Move and Details.
*/
@Composable
-fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHostState) {
- val state by viewModel.state.collectAsState()
+fun BookInfoMoreDropDown(
+ state: State,
+ onEvent: (BookInfoEvent) -> Unit,
+ snackbarState: SnackbarHostState
+) {
var showDropDown by remember { mutableStateOf(false) }
Box {
@@ -37,7 +40,7 @@ fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHo
contentDescription = R.string.show_dropdown_content_desc,
disableOnClick = false,
enabled = !showDropDown &&
- !state.isRefreshing
+ !state.value.isRefreshing
) {
showDropDown = true
}
@@ -53,14 +56,14 @@ fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHo
leadingIcon = Icons.AutoMirrored.Outlined.DriveFileMove,
text = stringResource(id = R.string.move_this_book)
) {
- viewModel.onEvent(BookInfoEvent.OnShowHideMoveDialog)
+ onEvent(BookInfoEvent.OnShowHideMoveDialog)
showDropDown = false
}
CustomDropDownMenuItem(
leadingIcon = Icons.Outlined.Delete,
text = stringResource(id = R.string.delete_this_book)
) {
- viewModel.onEvent(BookInfoEvent.OnShowHideDeleteDialog)
+ onEvent(BookInfoEvent.OnShowHideDeleteDialog)
showDropDown = false
}
CustomDropDownMenuItem(
@@ -68,7 +71,7 @@ fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHo
text = stringResource(id = R.string.details_book)
) {
snackbarState.currentSnackbarData?.dismiss()
- viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
+ onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
showDropDown = false
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt
index 0c4a8dab..e342e859 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt
@@ -9,6 +9,7 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
@@ -16,26 +17,26 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero
+import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
/**
* Statistic section.
*/
@Composable
-fun BookInfoStatisticSection(book: Book) {
- val progress = remember(book) {
+fun BookInfoStatisticSection(state: State) {
+ val progress = remember(state.value.book) {
"${
- (book.progress * 100)
+ (state.value.book.progress * 100)
.toDouble()
.removeDigits(1)
.removeTrailingZero()
}%"
}
val description = stringResource(
- if (book.progress == 1f) R.string.read_done
- else if (book.progress > 0.2f) R.string.read_keep
+ if (state.value.book.progress == 1f) R.string.read_done
+ else if (state.value.book.progress > 0.2f) R.string.read_keep
else R.string.read_more
)
@@ -53,7 +54,7 @@ fun BookInfoStatisticSection(book: Book) {
Spacer(modifier = Modifier.height(6.dp))
LinearProgressIndicator(
- progress = { book.progress.coerceAtLeast(0.01f) },
+ progress = { state.value.book.progress.coerceAtLeast(0.01f) },
modifier = Modifier
.fillMaxWidth(),
color = MaterialTheme.colorScheme.primary,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt
index 84cdddf0..3688d899 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt
@@ -18,19 +18,16 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
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.BookInfoState
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
-import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
-import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
/**
* Change cover bottom sheet. Lets user select photo from the gallery and replaces old one.
@@ -38,26 +35,25 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BookInfoChangeCoverBottomSheet(
- libraryViewModel: LibraryViewModel,
- historyViewModel: HistoryViewModel,
- viewModel: BookInfoViewModel
+ state: State,
+ onEvent: (BookInfoEvent) -> Unit,
+ onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
+ onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit
) {
- val state by viewModel.state.collectAsState()
val context = LocalContext.current
val navigationBarPadding =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
-
val photoPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
if (uri != null) {
- viewModel.onEvent(
+ onEvent(
BookInfoEvent.OnChangeCover(
uri,
context,
refreshList = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
+ onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
+ onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
}
)
)
@@ -73,7 +69,7 @@ fun BookInfoChangeCoverBottomSheet(
ModalBottomSheet(
modifier = Modifier.fillMaxWidth(),
onDismissRequest = {
- viewModel.onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
+ onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
},
sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0),
@@ -90,17 +86,17 @@ fun BookInfoChangeCoverBottomSheet(
)
}
- if (state.book.coverImage != null) {
+ if (state.value.book.coverImage != null) {
BookInfoChangeCoverBottomSheetItem(
icon = Icons.Default.HideImage,
text = stringResource(id = R.string.delete_cover),
description = stringResource(id = R.string.delete_cover_desc)
) {
- viewModel.onEvent(
+ onEvent(
BookInfoEvent.OnDeleteCover(
refreshList = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
+ onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
+ onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
}
)
)
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt
index bbd0080e..fc8edcf4 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt
@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -58,7 +57,6 @@ fun BookInfoChangeCoverBottomSheetItem(
color = MaterialTheme.colorScheme.onSurface,
fontSize = 18.sp
)
- Spacer(modifier = Modifier.height(2.dp))
Text(
description,
style = MaterialTheme.typography.bodyMedium,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/confirm_update_dialog/BookInfoConfirmUpateDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/confirm_update_dialog/BookInfoConfirmUpateDialog.kt
index f347ffcc..f30a9292 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/confirm_update_dialog/BookInfoConfirmUpateDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/confirm_update_dialog/BookInfoConfirmUpateDialog.kt
@@ -4,28 +4,28 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn
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.BookInfoState
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
-import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
-import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
+/**
+ * Confirm update dialog. Updates book if action clicked.
+ */
@Composable
fun BookInfoConfirmUpdateDialog(
- libraryViewModel: LibraryViewModel,
- historyViewModel: HistoryViewModel,
- viewModel: BookInfoViewModel,
- snackbarHostState: SnackbarHostState
+ state: State,
+ snackbarHostState: SnackbarHostState,
+ onEvent: (BookInfoEvent) -> Unit,
+ onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
+ onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
) {
val context = LocalContext.current
- val state by viewModel.state.collectAsState()
CustomDialogWithLazyColumn(
title = stringResource(id = R.string.confirm_update),
@@ -35,32 +35,32 @@ fun BookInfoConfirmUpdateDialog(
),
actionText = stringResource(id = R.string.confirm),
isActionEnabled = true,
- onDismiss = { viewModel.onEvent(BookInfoEvent.OnDismissConfirmUpdateDialog) },
+ onDismiss = { onEvent(BookInfoEvent.OnDismissConfirmUpdateDialog) },
onAction = {
- viewModel.onEvent(
+ onEvent(
BookInfoEvent.OnConfirmUpdate(
snackbarState = snackbarHostState,
context = context,
refreshList = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
+ onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
+ onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
}
)
)
},
withDivider = false,
items = {
- if (state.authorChanged) {
+ if (state.value.authorChanged) {
item {
BookInfoConfirmUpdateDialogItem(title = stringResource(id = R.string.author))
}
}
- if (state.descriptionChanged) {
+ if (state.value.descriptionChanged) {
item {
BookInfoConfirmUpdateDialogItem(title = stringResource(id = R.string.description))
}
}
- if (state.textChanged) {
+ if (state.value.textChanged) {
item {
BookInfoConfirmUpdateDialogItem(title = stringResource(id = R.string.text))
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/confirm_update_dialog/BookInfoConfirmUpdateDialogItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/confirm_update_dialog/BookInfoConfirmUpdateDialogItem.kt
index 68549d1b..ad2c8d7a 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/confirm_update_dialog/BookInfoConfirmUpdateDialogItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/confirm_update_dialog/BookInfoConfirmUpdateDialogItem.kt
@@ -7,6 +7,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+/**
+ * Confirm update dialog item.
+ */
@Composable
fun BookInfoConfirmUpdateDialogItem(title: String) {
Text(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt
index 4acfa9c3..159dadd2 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt
@@ -12,8 +12,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -21,7 +20,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
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.BookInfoState
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -32,9 +31,9 @@ import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BookInfoDetailsBottomSheet(
- viewModel: BookInfoViewModel
+ state: State,
+ onEvent: (BookInfoEvent) -> Unit
) {
- val state by viewModel.state.collectAsState()
val context = LocalContext.current
val navigationBarPadding =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
@@ -43,11 +42,11 @@ fun BookInfoDetailsBottomSheet(
SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault())
}
val lastOpened = remember {
- pattern.format(Date(state.book.lastOpened ?: 0))
+ pattern.format(Date(state.value.book.lastOpened ?: 0))
}
val sizeBytes = remember {
- state.book.file?.length() ?: 0
+ state.value.book.file?.length() ?: 0
}
val fileSizeKB = remember {
@@ -66,7 +65,7 @@ fun BookInfoDetailsBottomSheet(
ModalBottomSheet(
modifier = Modifier.fillMaxWidth(),
onDismissRequest = {
- viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
+ onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
},
sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0),
@@ -74,45 +73,12 @@ fun BookInfoDetailsBottomSheet(
) {
BookInfoDetailsBottomSheetItem(
title = stringResource(id = R.string.file_name),
- description = state.book.filePath.substringAfterLast("/").trim()
+ description = state.value.book.filePath.substringAfterLast("/").trim()
) {
- viewModel.onEvent(BookInfoEvent.OnCopyToClipboard(
- context,
- state.book.filePath.substringAfterLast("/").trim(),
- success = {
- Toast.makeText(
- context,
- context.getString(R.string.copied),
- Toast.LENGTH_SHORT
- ).show()
- }
- ))
- }
- BookInfoDetailsBottomSheetItem(
- title = stringResource(id = R.string.file_path),
- description = state.book.filePath.trim()
- ) {
- viewModel.onEvent(BookInfoEvent.OnCopyToClipboard(
- context,
- state.book.filePath.trim(),
- success = {
- Toast.makeText(
- context,
- context.getString(R.string.copied),
- Toast.LENGTH_SHORT
- ).show()
- }
- ))
- }
- BookInfoDetailsBottomSheetItem(
- title = stringResource(id = R.string.file_last_opened),
- description = if (state.book.lastOpened != null) lastOpened
- else stringResource(id = R.string.never)
- ) {
- if (state.book.lastOpened != null) {
- viewModel.onEvent(BookInfoEvent.OnCopyToClipboard(
+ onEvent(
+ BookInfoEvent.OnCopyToClipboard(
context,
- lastOpened,
+ state.value.book.filePath.substringAfterLast("/").trim(),
success = {
Toast.makeText(
context,
@@ -121,6 +87,42 @@ fun BookInfoDetailsBottomSheet(
).show()
}
))
+ }
+ BookInfoDetailsBottomSheetItem(
+ title = stringResource(id = R.string.file_path),
+ description = state.value.book.filePath.trim()
+ ) {
+ onEvent(
+ BookInfoEvent.OnCopyToClipboard(
+ context,
+ state.value.book.filePath.trim(),
+ success = {
+ Toast.makeText(
+ context,
+ context.getString(R.string.copied),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ ))
+ }
+ BookInfoDetailsBottomSheetItem(
+ title = stringResource(id = R.string.file_last_opened),
+ description = if (state.value.book.lastOpened != null) lastOpened
+ else stringResource(id = R.string.never)
+ ) {
+ if (state.value.book.lastOpened != null) {
+ onEvent(
+ BookInfoEvent.OnCopyToClipboard(
+ context,
+ lastOpened,
+ success = {
+ Toast.makeText(
+ context,
+ context.getString(R.string.copied),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ ))
}
}
BookInfoDetailsBottomSheetItem(
@@ -128,17 +130,18 @@ fun BookInfoDetailsBottomSheet(
description = fileSize.ifBlank { stringResource(id = R.string.unknown) }
) {
if (fileSize.isNotBlank()) {
- viewModel.onEvent(BookInfoEvent.OnCopyToClipboard(
- context,
- fileSize,
- success = {
- Toast.makeText(
- context,
- context.getString(R.string.copied),
- Toast.LENGTH_SHORT
- ).show()
- }
- ))
+ onEvent(
+ BookInfoEvent.OnCopyToClipboard(
+ context,
+ fileSize,
+ success = {
+ Toast.makeText(
+ context,
+ context.getString(R.string.copied),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ ))
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt
index e395d483..1b65d5af 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt
@@ -8,27 +8,23 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
-import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
-import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
-import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
-import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
/**
* Delete dialog. Deletes current book.
*/
@Composable
fun BookInfoDeleteDialog(
- libraryViewModel: LibraryViewModel,
- browseViewModel: BrowseViewModel,
- historyViewModel: HistoryViewModel,
- viewModel: BookInfoViewModel,
- navigator: Navigator
+ onEvent: (BookInfoEvent) -> Unit,
+ onLibraryLoadEvent: (LibraryEvent.OnLoadList) -> Unit,
+ onHistoryLoadEvent: (HistoryEvent.OnLoadList) -> Unit,
+ onBrowseLoadEvent: (BrowseEvent.OnLoadList) -> Unit
) {
+ val navigator = LocalNavigator.current
val context = LocalContext.current
CustomDialogWithContent(
@@ -38,17 +34,17 @@ fun BookInfoDeleteDialog(
id = R.string.delete_book_description
),
actionText = stringResource(id = R.string.delete),
- onDismiss = { viewModel.onEvent(BookInfoEvent.OnShowHideDeleteDialog) },
+ onDismiss = { onEvent(BookInfoEvent.OnShowHideDeleteDialog) },
withDivider = false,
isActionEnabled = true,
onAction = {
- viewModel.onEvent(
+ onEvent(
BookInfoEvent.OnDeleteBook(
navigator = navigator,
refreshList = {
- libraryViewModel.onEvent(LibraryEvent.OnLoadList)
- browseViewModel.onEvent(BrowseEvent.OnLoadList)
- historyViewModel.onEvent(HistoryEvent.OnLoadList)
+ onLibraryLoadEvent(LibraryEvent.OnLoadList)
+ onBrowseLoadEvent(BrowseEvent.OnLoadList)
+ onHistoryLoadEvent(HistoryEvent.OnLoadList)
}
)
)
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt
index 952a0267..444da7b2 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt
@@ -6,8 +6,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -15,33 +14,31 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn
import ua.acclorite.book_story.presentation.components.custom_dialog.SelectableDialogItem
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
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.BookInfoState
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
-import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
-import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
/**
* Move dialog. Moves current book to the selected category.
*/
@Composable
fun BookInfoMoveDialog(
- libraryViewModel: LibraryViewModel,
- historyViewModel: HistoryViewModel,
- viewModel: BookInfoViewModel,
- navigator: Navigator
+ state: State,
+ onEvent: (BookInfoEvent) -> Unit,
+ onLibraryEvent: (LibraryEvent) -> Unit,
+ onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit
) {
- val state by viewModel.state.collectAsState()
+ val navigator = LocalNavigator.current
val context = LocalContext.current
val categories = remember {
- Category.entries.filter { state.book.category != it }
+ Category.entries.filter { state.value.book.category != it }
}
LaunchedEffect(Unit) {
- viewModel.onEvent(BookInfoEvent.OnSelectCategory(categories[0]))
+ onEvent(BookInfoEvent.OnSelectCategory(categories[0]))
}
CustomDialogWithLazyColumn(
@@ -52,16 +49,16 @@ fun BookInfoMoveDialog(
),
actionText = stringResource(id = R.string.move),
isActionEnabled = true,
- onDismiss = { viewModel.onEvent(BookInfoEvent.OnShowHideMoveDialog) },
+ onDismiss = { onEvent(BookInfoEvent.OnShowHideMoveDialog) },
onAction = {
- viewModel.onEvent(
+ onEvent(
BookInfoEvent.OnMoveBook(
refreshList = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
+ onLibraryEvent(LibraryEvent.OnUpdateBook(it))
+ onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
},
updatePage = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it))
+ onLibraryEvent(LibraryEvent.OnUpdateCurrentPage(it))
},
navigator = navigator
)
@@ -82,8 +79,11 @@ fun BookInfoMoveDialog(
Category.DROPPED -> stringResource(id = R.string.dropped_tab)
}
- SelectableDialogItem(selected = it == state.selectedCategory, title = category) {
- viewModel.onEvent(BookInfoEvent.OnSelectCategory(it))
+ SelectableDialogItem(
+ selected = it == state.value.selectedCategory,
+ title = category
+ ) {
+ onEvent(BookInfoEvent.OnSelectCategory(it))
}
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt
index 2b08d719..dd3c91c3 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt
@@ -3,11 +3,13 @@ package ua.acclorite.book_story.presentation.screens.book_info.data
import android.content.Context
import android.net.Uri
import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Immutable
import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.presentation.data.Navigator
+@Immutable
sealed class BookInfoEvent {
data object OnShowHideChangeCoverBottomSheet : BookInfoEvent()
data object OnShowHideDetailsBottomSheet : BookInfoEvent()
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt
index f73a1223..48ac5cf0 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt
@@ -3,18 +3,14 @@ package ua.acclorite.book_story.presentation.screens.browse
import android.Manifest
import android.widget.Toast
import androidx.activity.compose.BackHandler
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -30,6 +26,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -39,62 +36,81 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
+import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.CustomIconButton
+import ua.acclorite.book_story.presentation.components.CustomSearchTextField
import ua.acclorite.book_story.presentation.components.MoreDropDown
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.LocalNavigator
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.browse.components.BrowseFileItem
import ua.acclorite.book_story.presentation.screens.browse.components.BrowseStoragePermissionDialog
import ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog.BrowseAddingDialog
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
+import ua.acclorite.book_story.presentation.screens.browse.data.BrowseState
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
+import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions
+@Composable
+fun BrowseScreenRoot() {
+ val navigator = LocalNavigator.current
+ val viewModel: BrowseViewModel = hiltViewModel()
+ val libraryViewModel: LibraryViewModel = hiltViewModel()
+
+ val state = viewModel.state.collectAsState()
+
+ BrowseScreen(
+ state = state,
+ navigator = navigator,
+ onEvent = viewModel::onEvent,
+ onLibraryEvent = libraryViewModel::onEvent
+ )
+}
+
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalPermissionsApi::class,
ExperimentalMaterialApi::class
)
@Composable
-fun BrowseScreen(
- viewModel: BrowseViewModel,
- libraryViewModel: LibraryViewModel,
- navigator: Navigator
+private fun BrowseScreen(
+ state: State,
+ navigator: Navigator,
+ onEvent: (BrowseEvent) -> Unit,
+ onLibraryEvent: (LibraryEvent) -> Unit
) {
val context = LocalContext.current
val permissionState = rememberPermissionState(
permission = Manifest.permission.READ_EXTERNAL_STORAGE
)
- val state by viewModel.state.collectAsState()
val refreshState = rememberPullRefreshState(
- refreshing = state.isRefreshing,
+ refreshing = state.value.isRefreshing,
onRefresh = {
- viewModel.onEvent(BrowseEvent.OnRefreshList)
+ onEvent(BrowseEvent.OnRefreshList)
}
)
val focusRequester = remember { FocusRequester() }
var showErrorMessage by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
- viewModel.onEvent(BrowseEvent.OnUpdateScrollOffset)
- viewModel.onEvent(
+ onEvent(BrowseEvent.OnUpdateScrollOffset)
+ onEvent(
BrowseEvent.OnPermissionCheck(
permissionState,
hideErrorMessage = { showErrorMessage = false }
@@ -102,16 +118,16 @@ fun BrowseScreen(
)
}
- if (state.requestPermissionDialog) {
- BrowseStoragePermissionDialog(viewModel, permissionState) {
+ if (state.value.requestPermissionDialog) {
+ BrowseStoragePermissionDialog(onEvent, permissionState) {
showErrorMessage = it
}
}
- if (state.showAddingDialog) {
+ if (state.value.showAddingDialog) {
BrowseAddingDialog(
- viewModel = viewModel,
- navigator = navigator,
- libraryViewModel = libraryViewModel
+ state = state,
+ onEvent = onEvent,
+ onLibraryEvent = onLibraryEvent
)
}
@@ -123,9 +139,9 @@ fun BrowseScreen(
topBar = {
AnimatedTopAppBar(
scrollBehavior = null,
- isTopBarScrolled = state.hasSelectedItems || state.listState.canScrollBackward,
+ isTopBarScrolled = state.value.hasSelectedItems || state.value.listState.canScrollBackward,
- content1Visibility = !state.hasSelectedItems && !state.showSearch,
+ content1Visibility = !state.value.hasSelectedItems && !state.value.showSearch,
content1NavigationIcon = {},
content1Title = {
Text(stringResource(id = R.string.browse_screen))
@@ -136,26 +152,26 @@ fun BrowseScreen(
contentDescription = R.string.search_content_desc,
disableOnClick = true
) {
- viewModel.onEvent(BrowseEvent.OnSearchShowHide)
+ onEvent(BrowseEvent.OnSearchShowHide)
}
- MoreDropDown(navigator = navigator)
+ MoreDropDown()
},
- content2Visibility = state.hasSelectedItems,
+ content2Visibility = state.value.hasSelectedItems,
content2NavigationIcon = {
CustomIconButton(
icon = Icons.Default.Clear,
contentDescription = R.string.clear_selected_items_content_desc,
disableOnClick = true
) {
- viewModel.onEvent(BrowseEvent.OnClearSelectedFiles)
+ onEvent(BrowseEvent.OnClearSelectedFiles)
}
},
content2Title = {
Text(
stringResource(
id = R.string.selected_items_count_query,
- state.selectedItemsCount.coerceAtLeast(1)
+ state.value.selectedItemsCount.coerceAtLeast(1)
),
overflow = TextOverflow.Ellipsis,
maxLines = 1
@@ -166,65 +182,44 @@ fun BrowseScreen(
icon = Icons.Default.Check,
contentDescription = R.string.add_files_content_desc,
disableOnClick = false,
- enabled = !state.showAddingDialog
+ enabled = !state.value.showAddingDialog
) {
- viewModel.onEvent(BrowseEvent.OnAddingDialogRequest)
+ onEvent(BrowseEvent.OnAddingDialogRequest)
}
},
- content3Visibility = state.showSearch && !state.hasSelectedItems,
+ content3Visibility = state.value.showSearch && !state.value.hasSelectedItems,
content3NavigationIcon = {
CustomIconButton(
icon = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = R.string.exit_search_content_desc,
disableOnClick = true
) {
- viewModel.onEvent(BrowseEvent.OnSearchShowHide)
+ onEvent(BrowseEvent.OnSearchShowHide)
}
},
content3Title = {
- BasicTextField(
- value = state.searchQuery,
- singleLine = true,
- textStyle = TextStyle(
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.titleLarge.fontSize,
- lineHeight = MaterialTheme.typography.titleLarge.lineHeight,
- fontFamily = MaterialTheme.typography.titleLarge.fontFamily
- ),
+ CustomSearchTextField(
modifier = Modifier
.focusRequester(focusRequester)
.onGloballyPositioned {
- viewModel.onEvent(BrowseEvent.OnRequestFocus(focusRequester))
+ onEvent(BrowseEvent.OnRequestFocus(focusRequester))
},
- onValueChange = {
- viewModel.onEvent(BrowseEvent.OnSearchQueryChange(it))
+ query = state.value.searchQuery,
+ onQueryChange = {
+ onEvent(BrowseEvent.OnSearchQueryChange(it))
},
- keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words),
- cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant)
- ) { innerText ->
- Box(
- modifier = Modifier.fillMaxHeight(),
- contentAlignment = Alignment.CenterStart
- ) {
- if (state.searchQuery.isEmpty()) {
- Text(
- stringResource(
- id = R.string.search_query,
- stringResource(id = R.string.files)
- ),
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- style = MaterialTheme.typography.titleLarge,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- }
- innerText()
- }
- }
+ onSearch = {
+ onEvent(BrowseEvent.OnSearch)
+ },
+ placeholder = stringResource(
+ id = R.string.search_query,
+ stringResource(id = R.string.files)
+ )
+ )
},
content3Actions = {
- MoreDropDown(navigator = navigator)
+ MoreDropDown()
}
)
}
@@ -234,21 +229,21 @@ fun BrowseScreen(
.fillMaxSize()
.padding(top = padding.calculateTopPadding())
) {
- DefaultTransition(visible = !state.isLoading) {
+ DefaultTransition(visible = !state.value.isLoading) {
LazyColumn(
- state = state.listState,
+ state = state.value.listState,
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(vertical = 8.dp)
) {
items(
- state.selectableFiles,
+ state.value.selectableFiles,
key = { it.first.path }
) { selectableFile ->
BrowseFileItem(
file = selectableFile,
modifier = Modifier.animateItem(),
- hasSelectedFiles = state.selectableFiles.any { it.second },
+ hasSelectedFiles = state.value.selectableFiles.any { it.second },
onLongClick = {
Toast.makeText(
context,
@@ -260,14 +255,14 @@ fun BrowseScreen(
).show()
},
onClick = {
- viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile))
+ onEvent(BrowseEvent.OnSelectFile(selectableFile))
}
)
}
}
}
- AnimatedVisibility(
+ CustomAnimatedVisibility(
visible = showErrorMessage,
modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn,
@@ -279,7 +274,7 @@ fun BrowseScreen(
icon = painterResource(id = R.drawable.error),
actionTitle = stringResource(id = R.string.grant_permission)
) {
- viewModel.onEvent(
+ onEvent(
BrowseEvent.OnPermissionCheck(
permissionState,
hideErrorMessage = { showErrorMessage = false }
@@ -288,10 +283,10 @@ fun BrowseScreen(
}
}
- AnimatedVisibility(
- visible = !state.isLoading && state.selectableFiles.isEmpty()
- && !showErrorMessage && !state.requestPermissionDialog
- && !state.isRefreshing,
+ CustomAnimatedVisibility(
+ visible = !state.value.isLoading && state.value.selectableFiles.isEmpty()
+ && !showErrorMessage && !state.value.requestPermissionDialog
+ && !state.value.isRefreshing,
modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn,
exit = fadeOut(tween(0))
@@ -305,7 +300,7 @@ fun BrowseScreen(
}
PullRefreshIndicator(
- state.isRefreshing,
+ state.value.isRefreshing,
refreshState,
Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.inverseSurface,
@@ -315,13 +310,13 @@ fun BrowseScreen(
}
BackHandler {
- if (state.hasSelectedItems) {
- viewModel.onEvent(BrowseEvent.OnClearSelectedFiles)
+ if (state.value.hasSelectedItems) {
+ onEvent(BrowseEvent.OnClearSelectedFiles)
return@BackHandler
}
- if (state.showSearch) {
- viewModel.onEvent(BrowseEvent.OnSearchShowHide)
+ if (state.value.showSearch) {
+ onEvent(BrowseEvent.OnSearchShowHide)
return@BackHandler
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt
index 1db2f06e..c17817fe 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt
@@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.screens.browse.components
-import android.os.Build
import androidx.activity.ComponentActivity
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SdStorage
@@ -12,7 +11,6 @@ import com.google.accompanist.permissions.PermissionState
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
-import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
/**
* Storage permission dialog.
@@ -20,7 +18,7 @@ import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun BrowseStoragePermissionDialog(
- viewModel: BrowseViewModel,
+ onEvent: (BrowseEvent) -> Unit,
permissionState: PermissionState,
showErrorMessage: (Boolean) -> Unit
) {
@@ -32,7 +30,7 @@ fun BrowseStoragePermissionDialog(
actionText = stringResource(id = R.string.grant),
imageVectorIcon = Icons.Default.SdStorage,
onDismiss = {
- viewModel.onEvent(
+ onEvent(
BrowseEvent.OnStoragePermissionDismiss(
permissionState,
showErrorMessage = { showErrorMessage(true) }
@@ -41,19 +39,14 @@ fun BrowseStoragePermissionDialog(
},
isActionEnabled = true,
withDivider = false,
+ disableOnClick = false,
onAction = {
- viewModel.onEvent(
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- BrowseEvent.OnStoragePermissionRequest(
- activity,
- hideErrorMessage = { showErrorMessage(false) }
- )
- } else {
- BrowseEvent.OnLegacyStoragePermissionRequest(
- permissionState,
- hideErrorMessage = { showErrorMessage(false) }
- )
- }
+ onEvent(
+ BrowseEvent.OnStoragePermissionRequest(
+ activity,
+ storagePermissionState = permissionState,
+ hideErrorMessage = { showErrorMessage(false) }
+ )
)
}
)
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt
index 1526f145..17e36aef 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt
@@ -11,8 +11,7 @@ import androidx.compose.material.icons.filled.AddChart
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
@@ -22,39 +21,38 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
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.BrowseState
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
-import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
/**
* Adding dialog. Adds all selected books to the Library.
*/
@Composable
fun BrowseAddingDialog(
- libraryViewModel: LibraryViewModel,
- viewModel: BrowseViewModel,
- navigator: Navigator
+ state: State,
+ onEvent: (BrowseEvent) -> Unit,
+ onLibraryEvent: (LibraryEvent) -> Unit
) {
- val state by viewModel.state.collectAsState()
val context = LocalContext.current
+ val navigator = LocalNavigator.current
CustomDialogWithLazyColumn(
title = stringResource(id = R.string.add_books),
imageVectorIcon = Icons.Default.AddChart,
description = stringResource(id = R.string.add_books_description),
actionText = stringResource(id = R.string.add),
- isActionEnabled = !state.isBooksLoading &&
- state.selectedBooks.any { it.first is NullableBook.NotNull },
- onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) },
+ isActionEnabled = !state.value.isBooksLoading &&
+ state.value.selectedBooks.any { it.first is NullableBook.NotNull },
+ onDismiss = { onEvent(BrowseEvent.OnAddingDialogDismiss) },
onAction = {
- viewModel.onEvent(
+ onEvent(
BrowseEvent.OnAddBooks(
navigator,
resetScroll = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(0))
- libraryViewModel.onEvent(LibraryEvent.OnLoadList)
+ onLibraryEvent(LibraryEvent.OnUpdateCurrentPage(0))
+ onLibraryEvent(LibraryEvent.OnLoadList)
},
onFailed = {
Toast.makeText(
@@ -75,7 +73,7 @@ fun BrowseAddingDialog(
},
withDivider = true,
items = {
- if (state.isBooksLoading) {
+ if (state.value.isBooksLoading) {
item {
Box(
modifier = Modifier
@@ -92,12 +90,12 @@ fun BrowseAddingDialog(
}
}
} else {
- items(state.selectedBooks) { book ->
+ items(state.value.selectedBooks) { book ->
BrowseAddingDialogItem(
result = book
) {
if (it) {
- viewModel.onEvent(BrowseEvent.OnSelectBook(book))
+ onEvent(BrowseEvent.OnSelectBook(book))
} else {
Toast.makeText(
context,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt
index f93016e4..267326be 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt
@@ -3,6 +3,7 @@
package ua.acclorite.book_story.presentation.screens.browse.data
import androidx.activity.ComponentActivity
+import androidx.compose.runtime.Immutable
import androidx.compose.ui.focus.FocusRequester
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
@@ -11,13 +12,12 @@ import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.data.Navigator
import java.io.File
+@Immutable
sealed class BrowseEvent {
data class OnStoragePermissionRequest(
- val activity: ComponentActivity, val hideErrorMessage: () -> Unit
- ) : BrowseEvent()
-
- data class OnLegacyStoragePermissionRequest(
- val permissionState: PermissionState, val hideErrorMessage: () -> Unit
+ val activity: ComponentActivity,
+ val storagePermissionState: PermissionState,
+ val hideErrorMessage: () -> Unit
) : BrowseEvent()
data class OnStoragePermissionDismiss(
@@ -36,6 +36,7 @@ sealed class BrowseEvent {
data class OnRequestFocus(val focusRequester: FocusRequester) : BrowseEvent()
data object OnClearSelectedFiles : BrowseEvent()
data class OnSearchQueryChange(val query: String) : BrowseEvent()
+ data object OnSearch : BrowseEvent()
data object OnAddingDialogRequest : BrowseEvent()
data object OnAddingDialogDismiss : BrowseEvent()
data object OnGetBooksFromFiles : BrowseEvent()
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt
index a29cb9f8..c390f511 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt
@@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
+import com.google.accompanist.permissions.shouldShowRationale
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -41,6 +42,7 @@ class BrowseViewModel @Inject constructor(
private var job: Job? = null
private var job2: Job? = null
private var job3: Job? = null
+ private var storagePermissionJob: Job? = null
init {
viewModelScope.launch(Dispatchers.IO) {
@@ -55,15 +57,72 @@ class BrowseViewModel @Inject constructor(
fun onEvent(event: BrowseEvent) {
when (event) {
- is BrowseEvent.OnLegacyStoragePermissionRequest -> {
- event.permissionState.launchPermissionRequest()
+ is BrowseEvent.OnStoragePermissionRequest -> {
+ val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
- viewModelScope.launch {
- for (i in 0 until 100) {
- if (!event.permissionState.status.isGranted) {
- delay(100)
+ val isPermissionGranted = if (legacyStoragePermission) {
+ event.storagePermissionState.status.isGranted
+ } else {
+ Environment.isExternalStorageManager()
+ }
+
+ if (isPermissionGranted) {
+ _state.update {
+ it.copy(
+ requestPermissionDialog = false
+ )
+ }
+ event.hideErrorMessage()
+ onEvent(BrowseEvent.OnRefreshList)
+ return
+ }
+
+ if (legacyStoragePermission) {
+ if (event.storagePermissionState.status.shouldShowRationale) {
+ event.storagePermissionState.launchPermissionRequest()
+ } else {
+ val uri = Uri.parse("package:${event.activity.packageName}")
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
+
+ if (intent.resolveActivity(event.activity.packageManager) != null) {
+ event.activity.startActivity(intent)
+ } else {
+ return
+ }
+ }
+ }
+
+ if (!legacyStoragePermission) {
+ val uri = Uri.parse("package:${event.activity.packageName}")
+ val intent = Intent(
+ Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
+ uri
+ )
+
+ if (intent.resolveActivity(event.activity.packageManager) != null) {
+ event.activity.startActivity(intent)
+ } else {
+ return
+ }
+ }
+
+ storagePermissionJob?.cancel()
+ storagePermissionJob = viewModelScope.launch {
+ while (true) {
+ val granted = if (legacyStoragePermission) {
+ event.storagePermissionState.status.isGranted
+ } else {
+ Environment.isExternalStorageManager()
+ }
+
+ if (!granted) {
+ delay(1000)
+ yield()
continue
}
+
+ yield()
+
_state.update {
it.copy(
requestPermissionDialog = false
@@ -76,41 +135,13 @@ class BrowseViewModel @Inject constructor(
}
}
- is BrowseEvent.OnStoragePermissionRequest -> {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
-
- val uri = Uri.parse("package:${event.activity.packageName}")
- val intent = Intent(
- Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
- uri
- )
- event.activity.startActivity(intent)
-
- viewModelScope.launch {
- for (i in 0 until 20) {
- if (!Environment.isExternalStorageManager()) {
- delay(1000)
- continue
- }
- _state.update {
- it.copy(
- requestPermissionDialog = false
- )
- }
- event.hideErrorMessage()
- onEvent(BrowseEvent.OnRefreshList)
- break
- }
- }
- }
- }
-
is BrowseEvent.OnStoragePermissionDismiss -> {
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
val isPermissionGranted =
if (!legacyPermission) Environment.isExternalStorageManager()
else event.permissionState.status.isGranted
+ storagePermissionJob?.cancel()
_state.update {
it.copy(
requestPermissionDialog = false
@@ -270,6 +301,12 @@ class BrowseViewModel @Inject constructor(
job = viewModelScope.launch(Dispatchers.IO) {
delay(500)
yield()
+ onEvent(BrowseEvent.OnSearch)
+ }
+ }
+
+ is BrowseEvent.OnSearch -> {
+ viewModelScope.launch(Dispatchers.IO) {
getFilesFromDownloads()
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/HelpScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/HelpScreen.kt
index f330f4df..972436d2 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/HelpScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/HelpScreen.kt
@@ -1,14 +1,22 @@
package ua.acclorite.book_story.presentation.screens.help
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.RestartAlt
+import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
@@ -16,35 +24,73 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R
+import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.components.GoBackButton
-import ua.acclorite.book_story.presentation.components.collapsibleScrollBehaviorWithLazyListState
+import ua.acclorite.book_story.presentation.components.collapsibleUntilExitScrollBehaviorWithLazyListState
+import ua.acclorite.book_story.presentation.data.LocalNavigator
+import ua.acclorite.book_story.presentation.data.MainEvent
+import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpAddBooksItem
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpClickMeNoteItem
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpCustomizeApp
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpCustomizeReader
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpEditBook
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpFindBooksItem
+import ua.acclorite.book_story.presentation.screens.help.components.items.HelpManageHistory
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpMoveOrDeleteBooks
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpReadBook
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpUpdateBook
+import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent
+import ua.acclorite.book_story.presentation.screens.help.data.HelpState
import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
+import ua.acclorite.book_story.presentation.screens.start.data.StartEvent
+import ua.acclorite.book_story.presentation.screens.start.data.StartViewModel
+
+@Composable
+fun HelpScreenRoot() {
+ val navigator = LocalNavigator.current
+ val helpViewModel: HelpViewModel = hiltViewModel()
+ val mainViewModel: MainViewModel = hiltViewModel()
+ val startViewModel: StartViewModel = hiltViewModel()
+
+ val state = helpViewModel.state.collectAsState()
+
+ LaunchedEffect(Unit) {
+ helpViewModel.init(navigator)
+ }
+
+ HelpScreen(
+ state = state,
+ navigator = navigator,
+ onEvent = helpViewModel::onEvent,
+ onMainEvent = mainViewModel::onEvent,
+ onStartEvent = startViewModel::onEvent
+ )
+}
@OptIn(
ExperimentalMaterial3Api::class
)
@Composable
-fun HelpScreen(
- viewModel: HelpViewModel = hiltViewModel(),
- navigator: Navigator
+private fun HelpScreen(
+ state: State,
+ navigator: Navigator,
+ onEvent: (HelpEvent) -> Unit,
+ onMainEvent: (MainEvent) -> Unit,
+ onStartEvent: (StartEvent) -> Unit
) {
- val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
+ val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
Scaffold(
Modifier
@@ -58,7 +104,23 @@ fun HelpScreen(
Text(stringResource(id = R.string.help_screen))
},
navigationIcon = {
- GoBackButton(navigator = navigator)
+ if (!state.value.fromStart) {
+ GoBackButton(navigator = navigator)
+ }
+ },
+ actions = {
+ if (!state.value.fromStart) {
+ CustomIconButton(
+ icon = Icons.Default.RestartAlt,
+ contentDescription = R.string.reset_start_content_desc,
+ disableOnClick = false
+ ) {
+ onStartEvent(StartEvent.OnResetStartScreen)
+ onMainEvent(MainEvent.OnChangeShowStartScreen(true))
+ navigator.navigateWithoutBackStack(Screen.START, false)
+ navigator.clearBackStack()
+ }
+ }
},
scrollBehavior = scrollState.first,
colors = TopAppBarDefaults.largeTopAppBarColors(
@@ -66,14 +128,37 @@ fun HelpScreen(
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
)
)
+ },
+ bottomBar = {
+ if (state.value.fromStart) {
+ Column {
+ Spacer(modifier = Modifier.height(18.dp))
+ Button(
+ modifier = Modifier
+ .navigationBarsPadding()
+ .padding(bottom = 8.dp)
+ .padding(horizontal = 18.dp)
+ .fillMaxWidth(),
+ shape = RoundedCornerShape(100),
+ onClick = {
+ navigator.clearArgument("from_start")
+ onMainEvent(MainEvent.OnChangeShowStartScreen(false))
+ navigator.navigateWithoutBackStack(Screen.BROWSE, false)
+ }
+ ) {
+ Text(text = stringResource(id = R.string.done))
+ }
+ }
+ }
}
) { paddingValues ->
LazyColumn(
Modifier
.fillMaxSize()
- .padding(top = paddingValues.calculateTopPadding(), start = 18.dp, end = 18.dp)
+ .padding(paddingValues)
.imePadding(),
- state = scrollState.second
+ state = scrollState.second,
+ contentPadding = PaddingValues(horizontal = 18.dp)
) {
item {
Spacer(
@@ -84,7 +169,10 @@ fun HelpScreen(
}
item {
- HelpClickMeNoteItem(viewModel = viewModel)
+ HelpClickMeNoteItem(
+ state = state,
+ onEvent = onEvent
+ )
}
item {
@@ -95,54 +183,66 @@ fun HelpScreen(
)
}
-
item {
- HelpFindBooksItem(viewModel = viewModel)
+ HelpFindBooksItem(
+ state = state,
+ onEvent = onEvent
+ )
}
item {
- HelpAddBooksItem(navigator = navigator, viewModel = viewModel)
+ HelpAddBooksItem(
+ state = state,
+ onEvent = onEvent
+ )
}
item {
HelpCustomizeApp(
- navigator = navigator,
- viewModel = viewModel
+ state = state,
+ onEvent = onEvent
)
}
item {
HelpMoveOrDeleteBooks(
- navigator = navigator,
- viewModel = viewModel
+ state = state,
+ onEvent = onEvent
)
}
item {
HelpEditBook(
- navigator = navigator,
- viewModel = viewModel
+ state = state,
+ onEvent = onEvent
)
}
item {
HelpReadBook(
- navigator = navigator,
- viewModel = viewModel
+ state = state,
+ onEvent = onEvent
)
}
item {
HelpCustomizeReader(
- navigator = navigator,
- viewModel = viewModel
+ state = state,
+ onEvent = onEvent
)
}
item {
HelpUpdateBook(
- navigator = navigator,
- viewModel = viewModel
+ state = state,
+ onEvent = onEvent
+ )
+ }
+
+ item {
+ HelpManageHistory(
+ state = state,
+ onEvent = onEvent
)
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpAnnotation.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpAnnotation.kt
index 91fbf97c..d3d87982 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpAnnotation.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpAnnotation.kt
@@ -7,6 +7,9 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
+/**
+ * Help clickable annotation. Mainly used for highlighting screens.
+ */
@Composable
fun AnnotatedString.Builder.HelpAnnotation(
tag: String,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpItem.kt
index 16d5aaa5..36b71c25 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpItem.kt
@@ -29,6 +29,9 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.ui.SlidingTransition
+/**
+ * Help Item. Can be Expanded or Collapsed.
+ */
@Composable
fun LazyItemScope.HelpItem(
title: String,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpAddBooksItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpAddBooksItem.kt
index cd3e89ee..048bdd6f 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpAddBooksItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpAddBooksItem.kt
@@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation
import ua.acclorite.book_story.presentation.screens.help.components.HelpItem
-import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
+import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent
+import ua.acclorite.book_story.presentation.screens.help.data.HelpState
@Composable
-fun LazyItemScope.HelpAddBooksItem(navigator: Navigator, viewModel: HelpViewModel) {
- val state by viewModel.state.collectAsState()
+fun LazyItemScope.HelpAddBooksItem(
+ state: State,
+ onEvent: (HelpEvent) -> Unit
+) {
+ val navigator = LocalNavigator.current
HelpItem(
title = stringResource(id = R.string.help_title_how_to_add_books),
@@ -35,13 +38,15 @@ fun LazyItemScope.HelpAddBooksItem(navigator: Navigator, viewModel: HelpViewMode
append(".")
},
tags = listOf("browse", "library"),
- shouldShowDescription = state.showHelpItem2,
+ shouldShowDescription = state.value.showHelpItem2,
onTitleClick = {
- viewModel.onUpdate {
- it.copy(
- showHelpItem2 = !it.showHelpItem2
- )
- }
+ onEvent(
+ HelpEvent.OnUpdateState {
+ it.copy(
+ showHelpItem2 = !it.showHelpItem2
+ )
+ }
+ )
},
onTagClick = { tag ->
when (tag) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpClickMeNoteItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpClickMeNoteItem.kt
index 4f004195..e508f792 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpClickMeNoteItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpClickMeNoteItem.kt
@@ -3,19 +3,21 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import android.widget.Toast
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation
import ua.acclorite.book_story.presentation.screens.help.components.HelpClickableNote
-import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
+import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent
+import ua.acclorite.book_story.presentation.screens.help.data.HelpState
@Composable
-fun LazyItemScope.HelpClickMeNoteItem(viewModel: HelpViewModel) {
- val state by viewModel.state.collectAsState()
+fun LazyItemScope.HelpClickMeNoteItem(
+ state: State,
+ onEvent: (HelpEvent) -> Unit
+) {
val context = LocalContext.current
HelpClickableNote(
@@ -29,13 +31,15 @@ fun LazyItemScope.HelpClickMeNoteItem(viewModel: HelpViewModel) {
append(" " + stringResource(id = R.string.help_clickable_note_3))
},
tags = listOf("note"),
- isNoteFullyShown = state.showNote1,
+ isNoteFullyShown = state.value.showNote1,
onIconClick = {
- viewModel.onUpdate {
- it.copy(
- showNote1 = !it.showNote1
- )
- }
+ onEvent(
+ HelpEvent.OnUpdateState {
+ it.copy(
+ showNote1 = !it.showNote1
+ )
+ }
+ )
},
onTagClick = { tag ->
when (tag) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeApp.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeApp.kt
index b4f57d41..65aa744a 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeApp.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeApp.kt
@@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation
import ua.acclorite.book_story.presentation.screens.help.components.HelpItem
-import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
+import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent
+import ua.acclorite.book_story.presentation.screens.help.data.HelpState
@Composable
-fun LazyItemScope.HelpCustomizeApp(viewModel: HelpViewModel, navigator: Navigator) {
- val state by viewModel.state.collectAsState()
+fun LazyItemScope.HelpCustomizeApp(
+ state: State,
+ onEvent: (HelpEvent) -> Unit
+) {
+ val navigator = LocalNavigator.current
HelpItem(
title = stringResource(id = R.string.help_title_how_to_customize_app),
@@ -30,13 +33,15 @@ fun LazyItemScope.HelpCustomizeApp(viewModel: HelpViewModel, navigator: Navigato
append(stringResource(id = R.string.help_desc_how_to_customize_app_3))
},
tags = listOf("settings"),
- shouldShowDescription = state.showHelpItem3,
+ shouldShowDescription = state.value.showHelpItem3,
onTitleClick = {
- viewModel.onUpdate {
- it.copy(
- showHelpItem3 = !it.showHelpItem3
- )
- }
+ onEvent(
+ HelpEvent.OnUpdateState {
+ it.copy(
+ showHelpItem3 = !it.showHelpItem3
+ )
+ }
+ )
},
onTagClick = { tag ->
when (tag) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeReader.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeReader.kt
index 36a2e29b..7bb3e08f 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeReader.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeReader.kt
@@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation
import ua.acclorite.book_story.presentation.screens.help.components.HelpItem
-import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
+import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent
+import ua.acclorite.book_story.presentation.screens.help.data.HelpState
@Composable
-fun LazyItemScope.HelpCustomizeReader(viewModel: HelpViewModel, navigator: Navigator) {
- val state by viewModel.state.collectAsState()
+fun LazyItemScope.HelpCustomizeReader(
+ state: State,
+ onEvent: (HelpEvent) -> Unit
+) {
+ val navigator = LocalNavigator.current
HelpItem(
title = stringResource(id = R.string.help_title_how_to_customize_reader),
@@ -30,13 +33,15 @@ fun LazyItemScope.HelpCustomizeReader(viewModel: HelpViewModel, navigator: Navig
append(stringResource(id = R.string.help_desc_how_to_customize_reader_3))
},
tags = listOf("settings"),
- shouldShowDescription = state.showHelpItem7,
+ shouldShowDescription = state.value.showHelpItem7,
onTitleClick = {
- viewModel.onUpdate {
- it.copy(
- showHelpItem7 = !it.showHelpItem7
- )
- }
+ onEvent(
+ HelpEvent.OnUpdateState {
+ it.copy(
+ showHelpItem7 = !it.showHelpItem7
+ )
+ }
+ )
},
onTagClick = { tag ->
when (tag) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpEditBook.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpEditBook.kt
index 0574e305..f00258be 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpEditBook.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpEditBook.kt
@@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation
import ua.acclorite.book_story.presentation.screens.help.components.HelpItem
-import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
+import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent
+import ua.acclorite.book_story.presentation.screens.help.data.HelpState
@Composable
-fun LazyItemScope.HelpEditBook(viewModel: HelpViewModel, navigator: Navigator) {
- val state by viewModel.state.collectAsState()
+fun LazyItemScope.HelpEditBook(
+ state: State,
+ onEvent: (HelpEvent) -> Unit
+) {
+ val navigator = LocalNavigator.current
HelpItem(
title = stringResource(id = R.string.help_title_how_to_edit_book),
@@ -30,13 +33,15 @@ fun LazyItemScope.HelpEditBook(viewModel: HelpViewModel, navigator: Navigator) {
append(stringResource(id = R.string.help_desc_how_to_edit_book_3))
},
tags = listOf("library"),
- shouldShowDescription = state.showHelpItem5,
+ shouldShowDescription = state.value.showHelpItem5,
onTitleClick = {
- viewModel.onUpdate {
- it.copy(
- showHelpItem5 = !it.showHelpItem5
- )
- }
+ onEvent(
+ HelpEvent.OnUpdateState {
+ it.copy(
+ showHelpItem5 = !it.showHelpItem5
+ )
+ }
+ )
},
onTagClick = { tag ->
when (tag) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpFindBooksItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpFindBooksItem.kt
index 33b742ef..f14cd458 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpFindBooksItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpFindBooksItem.kt
@@ -16,8 +16,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
@@ -31,12 +30,14 @@ import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation
import ua.acclorite.book_story.presentation.screens.help.components.HelpItem
import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent
-import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
+import ua.acclorite.book_story.presentation.screens.help.data.HelpState
import ua.acclorite.book_story.presentation.ui.SlidingTransition
@Composable
-fun LazyItemScope.HelpFindBooksItem(viewModel: HelpViewModel) {
- val state by viewModel.state.collectAsState()
+fun LazyItemScope.HelpFindBooksItem(
+ state: State,
+ onEvent: (HelpEvent) -> Unit
+) {
val context = LocalContext.current
val sites = stringArrayResource(id = R.array.book_sites)
@@ -60,34 +61,38 @@ fun LazyItemScope.HelpFindBooksItem(viewModel: HelpViewModel) {
append(stringResource(id = R.string.help_desc_how_to_find_books))
},
tags = emptyList(),
- shouldShowDescription = state.showHelpItem1,
+ shouldShowDescription = state.value.showHelpItem1,
onTitleClick = {
- viewModel.onUpdate {
- it.copy(
- showHelpItem1 = !it.showHelpItem1
- )
- }
+ onEvent(
+ HelpEvent.OnUpdateState {
+ it.copy(
+ showHelpItem1 = !it.showHelpItem1
+ )
+ }
+ )
},
customContent = {
Spacer(modifier = Modifier.height(4.dp))
OutlinedTextField(
- value = state.textFieldValue,
+ value = state.value.textFieldValue,
modifier = Modifier
.fillMaxWidth(),
onValueChange = { value ->
- viewModel.onUpdate {
- it.copy(
- textFieldValue = value,
- showError = false
- )
- }
+ onEvent(
+ HelpEvent.OnUpdateState {
+ it.copy(
+ textFieldValue = value,
+ showError = false
+ )
+ }
+ )
},
label = {
Text(text = stringResource(id = R.string.help_field_placeholder))
},
keyboardActions = KeyboardActions(
onSearch = {
- viewModel.onEvent(
+ onEvent(
HelpEvent.OnSearchInWeb(
context = context,
noAppsFound = {
@@ -110,14 +115,14 @@ fun LazyItemScope.HelpFindBooksItem(viewModel: HelpViewModel) {
icon = Icons.Default.Search,
contentDescription = R.string.search_content_desc,
disableOnClick = false,
- enabled = !state.showError,
- color = if (state.showError) {
+ enabled = !state.value.showError,
+ color = if (state.value.showError) {
MaterialTheme.colorScheme.outline
} else {
MaterialTheme.colorScheme.primary
}
) {
- viewModel.onEvent(
+ onEvent(
HelpEvent.OnSearchInWeb(
context = context,
noAppsFound = {
@@ -135,14 +140,14 @@ fun LazyItemScope.HelpFindBooksItem(viewModel: HelpViewModel) {
Column {
Spacer(modifier = Modifier.height(2.dp))
Box {
- SlidingTransition(visible = state.showError) {
+ SlidingTransition(visible = state.value.showError) {
Text(
text = stringResource(id = R.string.help_field_error),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
- SlidingTransition(visible = !state.showError) {
+ SlidingTransition(visible = !state.value.showError) {
ClickableText(
text = text,
style = MaterialTheme.typography.bodySmall.copy(
@@ -155,7 +160,7 @@ fun LazyItemScope.HelpFindBooksItem(viewModel: HelpViewModel) {
start = offset,
end = offset
).firstOrNull()?.let {
- viewModel.onEvent(
+ onEvent(
HelpEvent.OnNavigateToBrowserPage(
page = tag.substringAfterLast("|"),
context = context,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpManageHistory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpManageHistory.kt
new file mode 100644
index 00000000..f3636012
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpManageHistory.kt
@@ -0,0 +1,54 @@
+package ua.acclorite.book_story.presentation.screens.help.components.items
+
+import androidx.compose.foundation.lazy.LazyItemScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.buildAnnotatedString
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.presentation.data.LocalNavigator
+import ua.acclorite.book_story.presentation.data.Screen
+import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation
+import ua.acclorite.book_story.presentation.screens.help.components.HelpItem
+import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent
+import ua.acclorite.book_story.presentation.screens.help.data.HelpState
+
+@Composable
+fun LazyItemScope.HelpManageHistory(
+ state: State,
+ onEvent: (HelpEvent) -> Unit
+) {
+ val navigator = LocalNavigator.current
+
+ HelpItem(
+ title = stringResource(id = R.string.help_title_how_to_manage_history),
+ description = buildAnnotatedString {
+ append(stringResource(id = R.string.help_desc_how_to_manage_history_1) + " ")
+
+ HelpAnnotation(tag = "history") {
+ append(stringResource(id = R.string.help_desc_how_to_manage_history_2))
+ }
+ append(". ")
+
+ append(stringResource(id = R.string.help_desc_how_to_manage_history_3))
+ },
+ tags = listOf("history"),
+ shouldShowDescription = state.value.showHelpItem9,
+ onTitleClick = {
+ onEvent(
+ HelpEvent.OnUpdateState {
+ it.copy(
+ showHelpItem9 = !it.showHelpItem9
+ )
+ }
+ )
+ },
+ onTagClick = { tag ->
+ when (tag) {
+ "history" -> {
+ navigator.navigate(Screen.HISTORY, true)
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpMoveOrDeleteBooks.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpMoveOrDeleteBooks.kt
index 878ac747..f08c26d6 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpMoveOrDeleteBooks.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpMoveOrDeleteBooks.kt
@@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation
import ua.acclorite.book_story.presentation.screens.help.components.HelpItem
-import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
+import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent
+import ua.acclorite.book_story.presentation.screens.help.data.HelpState
@Composable
-fun LazyItemScope.HelpMoveOrDeleteBooks(viewModel: HelpViewModel, navigator: Navigator) {
- val state by viewModel.state.collectAsState()
+fun LazyItemScope.HelpMoveOrDeleteBooks(
+ state: State,
+ onEvent: (HelpEvent) -> Unit
+) {
+ val navigator = LocalNavigator.current
HelpItem(
title = stringResource(id = R.string.help_title_how_to_move_or_delete_books),
@@ -30,13 +33,15 @@ fun LazyItemScope.HelpMoveOrDeleteBooks(viewModel: HelpViewModel, navigator: Nav
append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_3))
},
tags = listOf("library"),
- shouldShowDescription = state.showHelpItem4,
+ shouldShowDescription = state.value.showHelpItem4,
onTitleClick = {
- viewModel.onUpdate {
- it.copy(
- showHelpItem4 = !it.showHelpItem4
- )
- }
+ onEvent(
+ HelpEvent.OnUpdateState {
+ it.copy(
+ showHelpItem4 = !it.showHelpItem4
+ )
+ }
+ )
},
onTagClick = { tag ->
when (tag) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpReadBook.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpReadBook.kt
index efeaccbb..82bdb63c 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpReadBook.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpReadBook.kt
@@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation
import ua.acclorite.book_story.presentation.screens.help.components.HelpItem
-import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
+import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent
+import ua.acclorite.book_story.presentation.screens.help.data.HelpState
@Composable
-fun LazyItemScope.HelpReadBook(viewModel: HelpViewModel, navigator: Navigator) {
- val state by viewModel.state.collectAsState()
+fun LazyItemScope.HelpReadBook(
+ state: State,
+ onEvent: (HelpEvent) -> Unit
+) {
+ val navigator = LocalNavigator.current
HelpItem(
title = stringResource(id = R.string.help_title_how_to_read_book),
@@ -30,13 +33,15 @@ fun LazyItemScope.HelpReadBook(viewModel: HelpViewModel, navigator: Navigator) {
append(stringResource(id = R.string.help_desc_how_to_read_book_3))
},
tags = listOf("library"),
- shouldShowDescription = state.showHelpItem6,
+ shouldShowDescription = state.value.showHelpItem6,
onTitleClick = {
- viewModel.onUpdate {
- it.copy(
- showHelpItem6 = !it.showHelpItem6
- )
- }
+ onEvent(
+ HelpEvent.OnUpdateState {
+ it.copy(
+ showHelpItem6 = !it.showHelpItem6
+ )
+ }
+ )
},
onTagClick = { tag ->
when (tag) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpUpdateBook.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpUpdateBook.kt
index 070c184f..fe3927c8 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpUpdateBook.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpUpdateBook.kt
@@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation
import ua.acclorite.book_story.presentation.screens.help.components.HelpItem
-import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
+import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent
+import ua.acclorite.book_story.presentation.screens.help.data.HelpState
@Composable
-fun LazyItemScope.HelpUpdateBook(viewModel: HelpViewModel, navigator: Navigator) {
- val state by viewModel.state.collectAsState()
+fun LazyItemScope.HelpUpdateBook(
+ state: State,
+ onEvent: (HelpEvent) -> Unit
+) {
+ val navigator = LocalNavigator.current
HelpItem(
title = stringResource(id = R.string.help_title_how_to_update_book),
@@ -30,13 +33,15 @@ fun LazyItemScope.HelpUpdateBook(viewModel: HelpViewModel, navigator: Navigator)
append(stringResource(id = R.string.help_desc_how_to_update_book_3))
},
tags = listOf("library"),
- shouldShowDescription = state.showHelpItem8,
+ shouldShowDescription = state.value.showHelpItem8,
onTitleClick = {
- viewModel.onUpdate {
- it.copy(
- showHelpItem8 = !it.showHelpItem8
- )
- }
+ onEvent(
+ HelpEvent.OnUpdateState {
+ it.copy(
+ showHelpItem8 = !it.showHelpItem8
+ )
+ }
+ )
},
onTagClick = { tag ->
when (tag) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpEvent.kt
index 7fd97ecc..8835e771 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpEvent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpEvent.kt
@@ -1,8 +1,13 @@
package ua.acclorite.book_story.presentation.screens.help.data
import android.content.Context
+import androidx.compose.runtime.Immutable
+@Immutable
sealed class HelpEvent {
+
+ data class OnUpdateState(val block: (HelpState) -> HelpState) : HelpEvent()
+
data class OnSearchInWeb(
val context: Context,
val noAppsFound: () -> Unit
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpState.kt
index 7cdb15a4..94abdcc6 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpState.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpState.kt
@@ -1,6 +1,11 @@
package ua.acclorite.book_story.presentation.screens.help.data
+import androidx.compose.runtime.Immutable
+
+@Immutable
data class HelpState(
+ val fromStart: Boolean = false,
+
val showNote1: Boolean = true,
val showHelpItem1: Boolean = false,
@@ -14,4 +19,5 @@ data class HelpState(
val showHelpItem6: Boolean = false,
val showHelpItem7: Boolean = false,
val showHelpItem8: Boolean = false,
+ val showHelpItem9: Boolean = false,
)
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpViewModel.kt
index b6b56752..8236eebd 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpViewModel.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpViewModel.kt
@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import ua.acclorite.book_story.presentation.data.Navigator
import javax.inject.Inject
@HiltViewModel
@@ -65,12 +66,24 @@ class HelpViewModel @Inject constructor(
event.noAppsFound()
}
}
+
+ is HelpEvent.OnUpdateState -> {
+ _state.update {
+ event.block(it)
+ }
+ }
}
}
- fun onUpdate(calculation: (HelpState) -> HelpState) {
- _state.update {
- calculation(it)
+ fun init(navigator: Navigator) {
+ viewModelScope.launch {
+ val isFromStart = navigator.retrieveArgument("from_start") as? Boolean ?: false
+
+ _state.update {
+ it.copy(
+ fromStart = isFromStart
+ )
+ }
}
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt
index 33d85d82..fd7f73cc 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt
@@ -1,20 +1,16 @@
package ua.acclorite.book_story.presentation.screens.history
import androidx.activity.compose.BackHandler
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -30,68 +26,88 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.input.KeyboardCapitalization
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
import ua.acclorite.book_story.presentation.components.CategoryTitle
+import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.CustomIconButton
+import ua.acclorite.book_story.presentation.components.CustomSearchTextField
import ua.acclorite.book_story.presentation.components.CustomSnackbar
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.LocalNavigator
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.history.components.HistoryDeleteWholeHistoryDialog
import ua.acclorite.book_story.presentation.screens.history.components.HistoryItem
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
+import ua.acclorite.book_story.presentation.screens.history.data.HistoryState
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions
+@Composable
+fun HistoryScreenRoot() {
+ val navigator = LocalNavigator.current
+ val viewModel: HistoryViewModel = hiltViewModel()
+ val libraryViewModel: LibraryViewModel = hiltViewModel()
+
+ val state = viewModel.state.collectAsState()
+
+ HistoryScreen(
+ state = state,
+ navigator = navigator,
+ onEvent = viewModel::onEvent,
+ onLibraryEvent = libraryViewModel::onEvent
+ )
+}
+
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalMaterialApi::class
)
@Composable
-fun HistoryScreen(
- viewModel: HistoryViewModel,
- libraryViewModel: LibraryViewModel,
- navigator: Navigator
+private fun HistoryScreen(
+ state: State,
+ navigator: Navigator,
+ onEvent: (HistoryEvent) -> Unit,
+ onLibraryEvent: (LibraryEvent) -> Unit
) {
val context = LocalContext.current
- val state by viewModel.state.collectAsState()
val refreshState = rememberPullRefreshState(
- refreshing = state.isRefreshing,
+ refreshing = state.value.isRefreshing,
onRefresh = {
- viewModel.onEvent(HistoryEvent.OnRefreshList)
+ onEvent(HistoryEvent.OnRefreshList)
}
)
val focusRequester = remember { FocusRequester() }
val snackbarState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
- viewModel.onEvent(HistoryEvent.OnUpdateScrollOffset)
+ onEvent(HistoryEvent.OnUpdateScrollOffset)
}
- if (state.showDeleteWholeHistoryDialog) {
- HistoryDeleteWholeHistoryDialog(viewModel = viewModel, libraryViewModel = libraryViewModel)
+ if (state.value.showDeleteWholeHistoryDialog) {
+ HistoryDeleteWholeHistoryDialog(
+ onEvent = onEvent,
+ onLibraryLoadEvent = onLibraryEvent
+ )
}
Scaffold(
@@ -102,9 +118,9 @@ fun HistoryScreen(
topBar = {
AnimatedTopAppBar(
scrollBehavior = null,
- isTopBarScrolled = state.listState.canScrollBackward,
+ isTopBarScrolled = state.value.listState.canScrollBackward,
- content1Visibility = !state.showSearch,
+ content1Visibility = !state.value.showSearch,
content1NavigationIcon = {},
content1Title = {
Text(
@@ -117,75 +133,54 @@ fun HistoryScreen(
contentDescription = R.string.search_content_desc,
disableOnClick = true,
) {
- viewModel.onEvent(HistoryEvent.OnSearchShowHide)
+ onEvent(HistoryEvent.OnSearchShowHide)
}
CustomIconButton(
icon = Icons.Outlined.DeleteSweep,
contentDescription = R.string.delete_whole_history_content_desc,
disableOnClick = false,
- enabled = !state.isLoading
- && !state.isRefreshing
- && state.history.isNotEmpty()
- && !state.showDeleteWholeHistoryDialog
+ enabled = !state.value.isLoading
+ && !state.value.isRefreshing
+ && state.value.history.isNotEmpty()
+ && !state.value.showDeleteWholeHistoryDialog
) {
- viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog)
+ onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog)
}
- MoreDropDown(navigator = navigator)
+ MoreDropDown()
},
- content2Visibility = state.showSearch,
+ content2Visibility = state.value.showSearch,
content2NavigationIcon = {
CustomIconButton(
icon = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = R.string.exit_search_content_desc,
disableOnClick = true
) {
- viewModel.onEvent(HistoryEvent.OnSearchShowHide)
+ onEvent(HistoryEvent.OnSearchShowHide)
}
},
content2Title = {
- BasicTextField(
- value = state.searchQuery,
- singleLine = true,
- textStyle = TextStyle(
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.titleLarge.fontSize,
- lineHeight = MaterialTheme.typography.titleLarge.lineHeight,
- fontFamily = MaterialTheme.typography.titleLarge.fontFamily
- ),
+ CustomSearchTextField(
modifier = Modifier
.focusRequester(focusRequester)
.onGloballyPositioned {
- viewModel.onEvent(HistoryEvent.OnRequestFocus(focusRequester))
+ onEvent(HistoryEvent.OnRequestFocus(focusRequester))
},
- onValueChange = {
- viewModel.onEvent(HistoryEvent.OnSearchQueryChange(it))
+ query = state.value.searchQuery,
+ onQueryChange = {
+ onEvent(HistoryEvent.OnSearchQueryChange(it))
},
- keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words),
- cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant)
- ) { innerText ->
- Box(
- modifier = Modifier.fillMaxHeight(),
- contentAlignment = Alignment.CenterStart
- ) {
- if (state.searchQuery.isEmpty()) {
- Text(
- stringResource(
- id = R.string.search_query,
- stringResource(id = R.string.history)
- ),
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- style = MaterialTheme.typography.titleLarge,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- }
- innerText()
- }
- }
+ onSearch = {
+ onEvent(HistoryEvent.OnSearch)
+ },
+ placeholder = stringResource(
+ id = R.string.search_query,
+ stringResource(id = R.string.history)
+ )
+ )
},
content2Actions = {
- MoreDropDown(navigator = navigator)
+ MoreDropDown()
},
content3Visibility = false
)
@@ -199,14 +194,14 @@ fun HistoryScreen(
.fillMaxSize()
.padding(top = paddingValues.calculateTopPadding())
) {
- DefaultTransition(visible = !state.isLoading) {
+ DefaultTransition(visible = !state.value.isLoading) {
LazyColumn(
Modifier
.fillMaxSize(),
- state = state.listState,
+ state = state.value.listState,
contentPadding = PaddingValues(vertical = 12.dp)
) {
- state.history.forEachIndexed { index, groupedHistory ->
+ state.value.history.forEachIndexed { index, groupedHistory ->
item(key = groupedHistory.title) {
if (index > 0) {
Spacer(modifier = Modifier.height(8.dp))
@@ -231,7 +226,7 @@ fun HistoryScreen(
HistoryItem(
modifier = Modifier.animateItem(),
history = it,
- isOnClickEnabled = !state.isRefreshing,
+ isOnClickEnabled = !state.value.isRefreshing,
onBodyClick = {
navigator.navigate(
Screen.BOOK_INFO,
@@ -240,22 +235,22 @@ fun HistoryScreen(
)
},
onTitleClick = {
- viewModel.onEvent(
+ onEvent(
HistoryEvent.OnNavigateToReaderScreen(
navigator,
it.book!!
)
)
},
- isDeleteEnabled = !state.isRefreshing,
+ isDeleteEnabled = !state.value.isRefreshing,
onDeleteClick = {
- viewModel.onEvent(
+ onEvent(
HistoryEvent.OnDeleteHistoryElement(
historyToDelete = it,
snackbarState = snackbarState,
context = context,
refreshList = {
- libraryViewModel.onEvent(
+ onLibraryEvent(
LibraryEvent.OnLoadList
)
}
@@ -268,10 +263,10 @@ fun HistoryScreen(
}
}
- AnimatedVisibility(
- visible = !state.isLoading
- && state.history.isEmpty()
- && !state.isRefreshing,
+ CustomAnimatedVisibility(
+ visible = !state.value.isLoading
+ && state.value.history.isEmpty()
+ && !state.value.isRefreshing,
modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn,
exit = fadeOut(tween(0))
@@ -284,7 +279,7 @@ fun HistoryScreen(
}
PullRefreshIndicator(
- state.isRefreshing,
+ state.value.isRefreshing,
refreshState,
Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.inverseSurface,
@@ -294,8 +289,8 @@ fun HistoryScreen(
}
BackHandler {
- if (state.showSearch) {
- viewModel.onEvent(HistoryEvent.OnSearchShowHide)
+ if (state.value.showSearch) {
+ onEvent(HistoryEvent.OnSearchShowHide)
return@BackHandler
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt
index a71b7afa..d475a21f 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt
@@ -9,17 +9,15 @@ import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
-import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
-import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
/**
* Delete whole history dialog.
*/
@Composable
fun HistoryDeleteWholeHistoryDialog(
- viewModel: HistoryViewModel,
- libraryViewModel: LibraryViewModel
+ onEvent: (HistoryEvent) -> Unit,
+ onLibraryLoadEvent: (LibraryEvent.OnLoadList) -> Unit
) {
val context = LocalContext.current
@@ -30,13 +28,13 @@ fun HistoryDeleteWholeHistoryDialog(
id = R.string.delete_history_description
),
actionText = stringResource(id = R.string.delete),
- onDismiss = { viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog) },
+ onDismiss = { onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog) },
withDivider = false,
isActionEnabled = true,
onAction = {
- viewModel.onEvent(
+ onEvent(
HistoryEvent.OnDeleteWholeHistory {
- libraryViewModel.onEvent(LibraryEvent.OnLoadList)
+ onLibraryLoadEvent(LibraryEvent.OnLoadList)
}
)
Toast.makeText(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt
index fc6ded70..382fb2f9 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt
@@ -2,11 +2,13 @@ package ua.acclorite.book_story.presentation.screens.history.data
import android.content.Context
import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Immutable
import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.presentation.data.Navigator
+@Immutable
sealed class HistoryEvent {
data object OnRefreshList : HistoryEvent()
data object OnLoadList : HistoryEvent()
@@ -20,6 +22,7 @@ sealed class HistoryEvent {
) : HistoryEvent()
data class OnSearchQueryChange(val query: String) : HistoryEvent()
+ data object OnSearch : HistoryEvent()
data object OnSearchShowHide : HistoryEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : HistoryEvent()
data class OnUpdateBook(val book: Book) : HistoryEvent()
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryState.kt
index 3c02d5ab..f0dd9433 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryState.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryState.kt
@@ -1,8 +1,10 @@
package ua.acclorite.book_story.presentation.screens.history.data
import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.GroupedHistory
+@Immutable
data class HistoryState(
val history: List = emptyList(),
val listState: LazyListState = LazyListState(0, 0),
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt
index 081f868a..0e1a05fa 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt
@@ -208,6 +208,12 @@ class HistoryViewModel @Inject constructor(
job = viewModelScope.launch(Dispatchers.IO) {
delay(500)
yield()
+ onEvent(HistoryEvent.OnSearch)
+ }
+ }
+
+ is HistoryEvent.OnSearch -> {
+ viewModelScope.launch(Dispatchers.IO) {
getHistoryFromDatabase()
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt
index dfd7eacb..1d1a62aa 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt
@@ -3,17 +3,14 @@ package ua.acclorite.book_story.presentation.screens.library
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -25,8 +22,6 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -43,6 +38,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -54,16 +50,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ua.acclorite.book_story.R
@@ -71,69 +65,95 @@ import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
+import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.CustomIconButton
+import ua.acclorite.book_story.presentation.components.CustomSearchTextField
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.LocalNavigator
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.BrowseEvent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
+import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.components.LibraryBookItem
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.LibraryState
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions
+@Composable
+fun LibraryScreenRoot() {
+ val navigator = LocalNavigator.current
+ val viewModel: LibraryViewModel = hiltViewModel()
+ val browseViewModel: BrowseViewModel = hiltViewModel()
+ val historyViewModel: HistoryViewModel = hiltViewModel()
+
+ val state = viewModel.state.collectAsState()
+
+ LibraryScreen(
+ state = state,
+ navigator = navigator,
+ onEvent = viewModel::onEvent,
+ onBrowseEvent = browseViewModel::onEvent,
+ onHistoryEvent = historyViewModel::onEvent
+ )
+}
+
@OptIn(
- ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class,
+ ExperimentalMaterial3Api::class,
ExperimentalMaterialApi::class
)
@Composable
-fun LibraryScreen(
- viewModel: LibraryViewModel,
- browseViewModel: BrowseViewModel,
- historyViewModel: HistoryViewModel,
- navigator: Navigator
+private fun LibraryScreen(
+ state: State,
+ navigator: Navigator,
+ onEvent: (LibraryEvent) -> Unit,
+ onHistoryEvent: (HistoryEvent) -> Unit,
+ onBrowseEvent: (BrowseEvent) -> Unit
) {
- val state by viewModel.state.collectAsState()
val pagerState = rememberPagerState(
- initialPage = state.currentPage,
+ initialPage = state.value.currentPage,
pageCount = { Category.entries.size }
)
var isScrollInProgress by remember { mutableStateOf(false) }
val refreshState = rememberPullRefreshState(
- refreshing = state.isRefreshing,
+ refreshing = state.value.isRefreshing,
onRefresh = {
- viewModel.onEvent(LibraryEvent.OnRefreshList)
+ onEvent(LibraryEvent.OnRefreshList)
}
)
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
- viewModel.onEvent(LibraryEvent.OnScrollToPage(state.currentPage, pagerState))
+ onEvent(LibraryEvent.OnScrollToPage(state.value.currentPage, pagerState))
}
LaunchedEffect(pagerState.currentPage) {
- if (pagerState.currentPage != state.currentPage) {
- viewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage))
+ if (pagerState.currentPage != state.value.currentPage) {
+ onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage))
}
}
- if (state.showMoveDialog) {
+ if (state.value.showMoveDialog) {
LibraryMoveDialog(
- viewModel = viewModel,
- historyViewModel = historyViewModel,
+ state = state,
+ onEvent = onEvent,
+ onHistoryLoadEvent = onHistoryEvent,
pagerState = pagerState
)
}
- if (state.showDeleteDialog) {
+ if (state.value.showDeleteDialog) {
LibraryDeleteDialog(
- viewModel = viewModel,
- browseViewModel = browseViewModel,
- historyViewModel = historyViewModel
+ state = state,
+ onEvent = onEvent,
+ onBrowseLoadEvent = onBrowseEvent,
+ onHistoryLoadEvent = onHistoryEvent
)
}
@@ -146,16 +166,16 @@ fun LibraryScreen(
Column(Modifier.fillMaxWidth()) {
AnimatedTopAppBar(
scrollBehavior = null,
- isTopBarScrolled = state.hasSelectedItems,
+ isTopBarScrolled = state.value.hasSelectedItems,
- content1Visibility = !state.hasSelectedItems && !state.showSearch,
+ content1Visibility = !state.value.hasSelectedItems && !state.value.showSearch,
content1NavigationIcon = {},
content1Title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(id = R.string.library_screen))
Spacer(modifier = Modifier.width(6.dp))
Text(
- text = state.books.size.toString(),
+ text = state.value.books.size.toString(),
modifier = Modifier
.clip(RoundedCornerShape(14.dp))
.background(MaterialTheme.colorScheme.surfaceContainer)
@@ -171,26 +191,26 @@ fun LibraryScreen(
contentDescription = R.string.search_content_desc,
disableOnClick = true,
) {
- viewModel.onEvent(LibraryEvent.OnSearchShowHide)
+ onEvent(LibraryEvent.OnSearchShowHide)
}
- MoreDropDown(navigator = navigator)
+ MoreDropDown()
},
- content2Visibility = state.hasSelectedItems,
+ content2Visibility = state.value.hasSelectedItems,
content2NavigationIcon = {
CustomIconButton(
icon = Icons.Default.Clear,
contentDescription = R.string.clear_selected_items_content_desc,
disableOnClick = true
) {
- viewModel.onEvent(LibraryEvent.OnClearSelectedBooks)
+ onEvent(LibraryEvent.OnClearSelectedBooks)
}
},
content2Title = {
Text(
stringResource(
id = R.string.selected_items_count_query,
- state.selectedItemsCount.coerceAtLeast(1)
+ state.value.selectedItemsCount.coerceAtLeast(1)
),
overflow = TextOverflow.Ellipsis,
maxLines = 1
@@ -200,86 +220,65 @@ fun LibraryScreen(
CustomIconButton(
icon = Icons.AutoMirrored.Outlined.DriveFileMove,
contentDescription = R.string.move_books_content_desc,
- enabled = !state.isLoading
- && !state.isRefreshing
- && !state.showMoveDialog,
+ enabled = !state.value.isLoading
+ && !state.value.isRefreshing
+ && !state.value.showMoveDialog,
disableOnClick = false,
) {
- viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog)
+ onEvent(LibraryEvent.OnShowHideMoveDialog)
}
CustomIconButton(
icon = Icons.Outlined.Delete,
contentDescription = R.string.delete_books_content_desc,
- enabled = !state.isLoading
- && !state.isRefreshing
- && !state.showDeleteDialog,
+ enabled = !state.value.isLoading
+ && !state.value.isRefreshing
+ && !state.value.showDeleteDialog,
disableOnClick = false
) {
- viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog)
+ onEvent(LibraryEvent.OnShowHideDeleteDialog)
}
},
- content3Visibility = state.showSearch && !state.hasSelectedItems,
+ content3Visibility = state.value.showSearch && !state.value.hasSelectedItems,
content3NavigationIcon = {
CustomIconButton(
icon = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = R.string.exit_search_content_desc,
disableOnClick = true
) {
- viewModel.onEvent(
+ onEvent(
LibraryEvent.OnSearchShowHide
)
}
},
content3Title = {
- BasicTextField(
- value = state.searchQuery,
- singleLine = true,
- textStyle = TextStyle(
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = MaterialTheme.typography.titleLarge.fontSize,
- lineHeight = MaterialTheme.typography.titleLarge.lineHeight,
- fontFamily = MaterialTheme.typography.titleLarge.fontFamily
- ),
+ CustomSearchTextField(
modifier = Modifier
.focusRequester(focusRequester)
.onGloballyPositioned {
- viewModel.onEvent(LibraryEvent.OnRequestFocus(focusRequester))
+ onEvent(LibraryEvent.OnRequestFocus(focusRequester))
},
- onValueChange = {
- viewModel.onEvent(LibraryEvent.OnSearchQueryChange(it))
+ query = state.value.searchQuery,
+ onQueryChange = {
+ onEvent(LibraryEvent.OnSearchQueryChange(it))
},
- keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words),
- cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant)
- ) { innerText ->
- Box(
- modifier = Modifier.fillMaxHeight(),
- contentAlignment = Alignment.CenterStart
- ) {
- if (state.searchQuery.isEmpty()) {
- Text(
- stringResource(
- id = R.string.search_query,
- stringResource(id = R.string.books)
- ),
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- style = MaterialTheme.typography.titleLarge,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- }
- innerText()
- }
- }
+ onSearch = {
+ onEvent(LibraryEvent.OnSearch)
+ },
+ placeholder = stringResource(
+ id = R.string.search_query,
+ stringResource(id = R.string.books)
+ )
+ )
},
content3Actions = {
- MoreDropDown(navigator = navigator)
+ MoreDropDown()
}
)
LibraryTabRow(
- viewModel = viewModel,
- books = state.categorizedBooks,
+ onEvent = onEvent,
+ books = state.value.categorizedBooks,
pagerState = pagerState
)
}
@@ -295,8 +294,8 @@ fun LibraryScreen(
userScrollEnabled = !isScrollInProgress
) { index ->
val category = remember { Category.entries[index] }
- val books = remember(state.categorizedBooks) {
- state.categorizedBooks.find {
+ val books = remember(state.value.categorizedBooks) {
+ state.value.categorizedBooks.find {
it.category == category
}?.books?.sortedWith(
compareByDescending> { it.first.lastOpened }
@@ -307,14 +306,14 @@ fun LibraryScreen(
LaunchedEffect(listState.isScrollInProgress, pagerState.currentPage) {
if (listState.isScrollInProgress != isScrollInProgress
- && pagerState.currentPage == state.currentPage
+ && pagerState.currentPage == state.value.currentPage
) {
isScrollInProgress = listState.isScrollInProgress
}
}
Box(modifier = Modifier.fillMaxSize()) {
- DefaultTransition(visible = !state.isLoading) {
+ DefaultTransition(visible = !state.value.isLoading) {
LazyVerticalGrid(
columns = GridCells.Adaptive(120.dp),
modifier = Modifier.fillMaxSize(),
@@ -327,10 +326,13 @@ fun LibraryScreen(
) {
LibraryBookItem(
book = it,
- modifier = Modifier.animateItemPlacement(),
+ modifier = Modifier.animateItem(
+ fadeInSpec = null,
+ fadeOutSpec = null
+ ),
onCoverImageClick = {
- if (state.hasSelectedItems) {
- viewModel.onEvent(LibraryEvent.OnSelectBook(it))
+ if (state.value.hasSelectedItems) {
+ onEvent(LibraryEvent.OnSelectBook(it))
} else {
navigator.navigate(
Screen.BOOK_INFO,
@@ -341,11 +343,11 @@ fun LibraryScreen(
},
onLongClick = {
if (!it.second) {
- viewModel.onEvent(LibraryEvent.OnSelectBook(it, true))
+ onEvent(LibraryEvent.OnSelectBook(it, true))
}
},
onButtonClick = {
- viewModel.onEvent(
+ onEvent(
LibraryEvent.OnNavigateToReaderScreen(
navigator,
it.first
@@ -357,9 +359,9 @@ fun LibraryScreen(
}
}
- AnimatedVisibility(
- visible = !state.isLoading
- && !state.isRefreshing
+ CustomAnimatedVisibility(
+ visible = !state.value.isLoading
+ && !state.value.isRefreshing
&& books.isEmpty(),
modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn,
@@ -378,7 +380,7 @@ fun LibraryScreen(
}
PullRefreshIndicator(
- state.isRefreshing,
+ state.value.isRefreshing,
refreshState,
Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.inverseSurface,
@@ -391,18 +393,18 @@ fun LibraryScreen(
val scope = rememberCoroutineScope()
var shouldExit = remember { false }
BackHandler {
- if (state.hasSelectedItems) {
- viewModel.onEvent(LibraryEvent.OnClearSelectedBooks)
+ if (state.value.hasSelectedItems) {
+ onEvent(LibraryEvent.OnClearSelectedBooks)
return@BackHandler
}
- if (state.showSearch) {
- viewModel.onEvent(LibraryEvent.OnSearchShowHide)
+ if (state.value.showSearch) {
+ onEvent(LibraryEvent.OnSearchShowHide)
return@BackHandler
}
- if (state.currentPage > 0) {
- viewModel.onEvent(LibraryEvent.OnScrollToPage(0, pagerState))
+ if (state.value.currentPage > 0) {
+ onEvent(LibraryEvent.OnScrollToPage(0, pagerState))
return@BackHandler
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt
index 6fe5677f..f11d22b1 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt
@@ -30,7 +30,6 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.CategorizedBooks
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
-import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
/**
* Tab row, either scrollable or static, depends on screen width.
@@ -38,7 +37,7 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LibraryTabRow(
- viewModel: LibraryViewModel,
+ onEvent: (LibraryEvent) -> Unit,
books: List,
pagerState: PagerState
) {
@@ -99,7 +98,7 @@ fun LibraryTabRow(
modifier = Modifier,
selected = pagerState.currentPage == index,
onClick = {
- viewModel.onEvent(LibraryEvent.OnScrollToPage(index, pagerState))
+ onEvent(LibraryEvent.OnScrollToPage(index, pagerState))
},
selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt
index 5b1117df..207a1f1f 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt
@@ -4,29 +4,26 @@ import android.widget.Toast
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DeleteOutline
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
-import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
-import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
-import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
+import ua.acclorite.book_story.presentation.screens.library.data.LibraryState
/**
* Delete dialog. Deletes all selected books.
*/
@Composable
fun LibraryDeleteDialog(
- viewModel: LibraryViewModel,
- browseViewModel: BrowseViewModel,
- historyViewModel: HistoryViewModel
+ state: State,
+ onEvent: (LibraryEvent) -> Unit,
+ onBrowseLoadEvent: (BrowseEvent.OnLoadList) -> Unit,
+ onHistoryLoadEvent: (HistoryEvent.OnLoadList) -> Unit
) {
- val state by viewModel.state.collectAsState()
val context = LocalContext.current
CustomDialogWithContent(
@@ -34,17 +31,17 @@ fun LibraryDeleteDialog(
imageVectorIcon = Icons.Outlined.DeleteOutline,
description = stringResource(
id = R.string.delete_books_description,
- state.books.filter { it.second }.size
+ state.value.books.filter { it.second }.size
),
actionText = stringResource(id = R.string.delete),
- onDismiss = { viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog) },
+ onDismiss = { onEvent(LibraryEvent.OnShowHideDeleteDialog) },
withDivider = false,
isActionEnabled = true,
onAction = {
- viewModel.onEvent(
+ onEvent(
LibraryEvent.OnDeleteBooks {
- browseViewModel.onEvent(BrowseEvent.OnLoadList)
- historyViewModel.onEvent(HistoryEvent.OnLoadList)
+ onBrowseLoadEvent(BrowseEvent.OnLoadList)
+ onHistoryLoadEvent(HistoryEvent.OnLoadList)
}
)
Toast.makeText(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt
index 0dc7b9be..da38b685 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt
@@ -6,8 +6,7 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
@@ -15,20 +14,19 @@ import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn
import ua.acclorite.book_story.presentation.components.custom_dialog.SelectableDialogItem
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
-import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
-import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
+import ua.acclorite.book_story.presentation.screens.library.data.LibraryState
/**
* Move dialog. Moves all selected books to the selected category.
*/
@Composable
fun LibraryMoveDialog(
- viewModel: LibraryViewModel,
- historyViewModel: HistoryViewModel,
+ state: State,
+ onEvent: (LibraryEvent) -> Unit,
+ onHistoryLoadEvent: (HistoryEvent.OnLoadList) -> Unit,
pagerState: PagerState
) {
- val state by viewModel.state.collectAsState()
val context = LocalContext.current
CustomDialogWithLazyColumn(
@@ -36,17 +34,17 @@ fun LibraryMoveDialog(
imageVectorIcon = Icons.AutoMirrored.Outlined.DriveFileMove,
description = stringResource(
id = R.string.move_books_description,
- state.books.filter { it.second }.size
+ state.value.books.filter { it.second }.size
),
actionText = stringResource(id = R.string.move),
isActionEnabled = true,
- onDismiss = { viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog) },
+ onDismiss = { onEvent(LibraryEvent.OnShowHideMoveDialog) },
onAction = {
- viewModel.onEvent(
+ onEvent(
LibraryEvent.OnMoveBooks(
pagerState,
refreshList = {
- historyViewModel.onEvent(HistoryEvent.OnLoadList)
+ onHistoryLoadEvent(HistoryEvent.OnLoadList)
}
)
)
@@ -58,7 +56,7 @@ fun LibraryMoveDialog(
},
withDivider = false,
items = {
- items(state.categories) {
+ items(state.value.categories) {
val category = when (it) {
Category.READING -> stringResource(id = R.string.reading_tab)
Category.ALREADY_READ -> stringResource(id = R.string.already_read_tab)
@@ -66,8 +64,11 @@ fun LibraryMoveDialog(
Category.DROPPED -> stringResource(id = R.string.dropped_tab)
}
- SelectableDialogItem(selected = it == state.selectedCategory, title = category) {
- viewModel.onEvent(LibraryEvent.OnSelectCategory(it))
+ SelectableDialogItem(
+ selected = it == state.value.selectedCategory,
+ title = category
+ ) {
+ onEvent(LibraryEvent.OnSelectCategory(it))
}
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt
index 5c7669db..a08e046b 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt
@@ -1,12 +1,14 @@
package ua.acclorite.book_story.presentation.screens.library.data
import androidx.compose.foundation.pager.PagerState
+import androidx.compose.runtime.Immutable
import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.data.Navigator
+@Immutable
sealed class LibraryEvent {
data object OnRefreshList : LibraryEvent()
data object OnLoadList : LibraryEvent()
@@ -16,6 +18,7 @@ sealed class LibraryEvent {
data object OnSearchShowHide : LibraryEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : LibraryEvent()
data class OnSearchQueryChange(val query: String) : LibraryEvent()
+ data object OnSearch : LibraryEvent()
data class OnSelectBook(val book: Pair, val select: Boolean? = null) :
LibraryEvent()
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt
index 44d92a88..252f0d26 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt
@@ -154,6 +154,12 @@ class LibraryViewModel @Inject constructor(
job = viewModelScope.launch(Dispatchers.IO) {
delay(500)
yield()
+ onEvent(LibraryEvent.OnSearch)
+ }
+ }
+
+ is LibraryEvent.OnSearch -> {
+ viewModelScope.launch(Dispatchers.IO) {
getBooksFromDatabase()
}
}
@@ -237,7 +243,20 @@ class LibraryViewModel @Inject constructor(
}.map { it.first.copy(category = _state.value.selectedCategory) }
updateBooks.execute(books)
- getBooksFromDatabase()
+ _state.update {
+ it.copy(
+ books = it.books.map { book ->
+ if (book.second) {
+ book.copy(
+ first = book.first.copy(category = _state.value.selectedCategory)
+ )
+ } else {
+ book
+ }
+ }
+ )
+ }
+ onEvent(LibraryEvent.OnRecalculateCategories(_state.value.books))
event.refreshList()
onEvent(LibraryEvent.OnClearSelectedBooks)
@@ -264,8 +283,7 @@ class LibraryViewModel @Inject constructor(
viewModelScope.launch {
_state.update {
it.copy(
- showDeleteDialog = false,
- isLoading = true
+ showDeleteDialog = false
)
}
@@ -274,7 +292,12 @@ class LibraryViewModel @Inject constructor(
}.map { it.first }
deleteBooks.execute(books)
- getBooksFromDatabase()
+ _state.update {
+ it.copy(
+ books = it.books.filter { book -> !book.second }
+ )
+ }
+ onEvent(LibraryEvent.OnRecalculateCategories(_state.value.books))
event.refreshList()
onEvent(LibraryEvent.OnClearSelectedBooks)
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt
index 6e86c596..d90bb21e 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt
@@ -5,7 +5,6 @@ import android.os.Build
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
@@ -18,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.selection.DisableSelection
@@ -27,12 +27,12 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
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.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -51,13 +51,18 @@ import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.Constants
+import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.CustomSelectionContainer
import ua.acclorite.book_story.presentation.components.is_messages.IsError
+import ua.acclorite.book_story.presentation.data.LocalNavigator
+import ua.acclorite.book_story.presentation.data.MainEvent
+import ua.acclorite.book_story.presentation.data.MainSettingsState
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
@@ -70,54 +75,24 @@ import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.Re
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.ReaderState
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
-@OptIn(FlowPreview::class)
-@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
-fun ReaderScreen(
- viewModel: ReaderViewModel,
- mainViewModel: MainViewModel,
- libraryViewModel: LibraryViewModel,
- historyViewModel: HistoryViewModel,
- navigator: Navigator
-) {
+fun ReaderScreenRoot() {
+ val navigator = LocalNavigator.current
val context = LocalContext.current as ComponentActivity
- val systemBarsColor = MaterialTheme.colorScheme.surfaceContainerHighest.copy(0.85f)
- var loading by remember { mutableStateOf(true) }
+ val viewModel: ReaderViewModel = hiltViewModel()
+ val mainViewModel: MainViewModel = hiltViewModel()
+ val libraryViewModel: LibraryViewModel = hiltViewModel()
+ val historyViewModel: HistoryViewModel = hiltViewModel()
+
val listState = rememberLazyListState()
+ val loading = remember { mutableStateOf(true) }
- val state by viewModel.state.collectAsState()
- val canScroll by remember {
- derivedStateOf {
- listState.canScrollBackward && listState.canScrollForward
- }
- }
- val nestedScrollConnection = remember {
- object : NestedScrollConnection {
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- val velocity = consumed.y
-
- if ((velocity > 70 || velocity < -70) && state.showMenu && !state.lockMenu) {
- viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context))
- }
-
- return super.onPostScroll(consumed, available, source)
- }
- }
- }
-
- val mainState by mainViewModel.state.collectAsState()
- val fontFamily = remember(mainState.fontFamily) {
- Constants.FONTS.find {
- it.id == mainState.fontFamily
- } ?: Constants.FONTS[0]
- }
+ val state = viewModel.state.collectAsState()
+ val mainState = mainViewModel.state.collectAsState()
LaunchedEffect(Unit) {
viewModel.init(
@@ -129,13 +104,73 @@ fun ReaderScreen(
},
listState = listState,
onLoaded = {
- loading = false
+ loading.value = false
}
)
}
+
+ ReaderScreen(
+ state = state,
+ mainState = mainState,
+ listState = listState,
+ loading = loading,
+ navigator = navigator,
+ onEvent = viewModel::onEvent,
+ onMainEvent = mainViewModel::onEvent,
+ onLibraryEvent = libraryViewModel::onEvent,
+ onHistoryEvent = historyViewModel::onEvent
+ )
+}
+
+@OptIn(FlowPreview::class)
+@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
+@Composable
+private fun ReaderScreen(
+ state: State,
+ mainState: State,
+ listState: LazyListState,
+ loading: State,
+ navigator: Navigator,
+ onEvent: (ReaderEvent) -> Unit,
+ onMainEvent: (MainEvent) -> Unit,
+ onLibraryEvent: (LibraryEvent) -> Unit,
+ onHistoryEvent: (HistoryEvent) -> Unit,
+) {
+ val context = LocalContext.current as ComponentActivity
+ val systemBarsColor = MaterialTheme.colorScheme.surfaceContainerHighest.copy(0.85f)
+
+ val canScroll by remember {
+ derivedStateOf {
+ listState.canScrollBackward && listState.canScrollForward
+ }
+ }
+ val nestedScrollConnection = remember(state) {
+ object : NestedScrollConnection {
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ val velocity = consumed.y
+
+ if ((velocity > 70 || velocity < -70) && state.value.showMenu && !state.value.lockMenu) {
+ onEvent(ReaderEvent.OnShowHideMenu(context = context))
+ }
+
+ return super.onPostScroll(consumed, available, source)
+ }
+ }
+ }
+
+ val fontFamily = remember(mainState.value.fontFamily) {
+ Constants.FONTS.find {
+ it.id == mainState.value.fontFamily
+ } ?: Constants.FONTS[0]
+ }
+
LaunchedEffect(canScroll) {
- if (!canScroll && !state.showMenu && !loading) {
- viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context))
+ if (!canScroll && !state.value.showMenu && !loading.value) {
+ onEvent(ReaderEvent.OnShowHideMenu(context = context))
}
}
@@ -143,28 +178,28 @@ fun ReaderScreen(
snapshotFlow {
listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
}.debounce(500).collectLatest { items ->
- if (!loading && state.book.text.isNotEmpty() && listState.layoutInfo.totalItemsCount > 0) {
+ if (!loading.value && state.value.book.text.isNotEmpty() && listState.layoutInfo.totalItemsCount > 0) {
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index
val progress = if (items.first > 0) {
if (lastVisibleItemIndex >= (listState.layoutInfo.totalItemsCount - 1)) {
1f
} else {
- (items.first.toFloat() / (state.book.text.lastIndex).toFloat())
+ (items.first.toFloat() / (state.value.book.text.lastIndex).toFloat())
}
} else {
0f
}
- viewModel.onEvent(
+ onEvent(
ReaderEvent.OnChangeProgress(
progress = progress,
navigator = navigator,
firstVisibleItemIndex = items.first,
firstVisibleItemOffset = items.second,
refreshList = { book ->
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(book))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(book))
+ onLibraryEvent(LibraryEvent.OnUpdateBook(book))
+ onHistoryEvent(HistoryEvent.OnUpdateBook(book))
}
)
)
@@ -172,8 +207,12 @@ fun ReaderScreen(
}
}
- if (state.showSettingsBottomSheet) {
- ReaderSettingsBottomSheet(viewModel = viewModel, mainViewModel = mainViewModel)
+ if (state.value.showSettingsBottomSheet) {
+ ReaderSettingsBottomSheet(
+ mainState = mainState,
+ onEvent = onEvent,
+ onMainEvent = onMainEvent
+ )
}
Scaffold(
@@ -182,34 +221,34 @@ fun ReaderScreen(
.nestedScroll(nestedScrollConnection),
containerColor = MaterialTheme.colorScheme.surface,
topBar = {
- AnimatedVisibility(
- visible = state.showMenu,
+ CustomAnimatedVisibility(
+ visible = state.value.showMenu,
enter = slideInVertically { -it },
exit = slideOutVertically { -it }
) {
ReaderTopBar(
- viewModel = viewModel,
- libraryViewModel = libraryViewModel,
- historyViewModel = historyViewModel,
- navigator = navigator,
+ state = state,
+ onEvent = onEvent,
+ onLibraryUpdateEvent = onLibraryEvent,
+ onHistoryUpdateEvent = onHistoryEvent,
listState = listState,
containerColor = systemBarsColor
)
}
},
bottomBar = {
- AnimatedVisibility(
- visible = state.showMenu,
+ CustomAnimatedVisibility(
+ visible = state.value.showMenu,
enter = slideInVertically { it },
exit = slideOutVertically { it }
) {
ReaderBottomBar(
- viewModel = viewModel,
- libraryViewModel = libraryViewModel,
- historyViewModel = historyViewModel,
+ state = state,
+ onEvent = onEvent,
+ onLibraryUpdateEvent = onLibraryEvent,
+ onHistoryUpdateEvent = onHistoryEvent,
listState = listState,
- navigator = navigator,
- systemBarsColor = systemBarsColor
+ containerColor = systemBarsColor
)
}
}
@@ -225,7 +264,7 @@ fun ReaderScreen(
}
},
onTranslateRequested = { textToTranslate ->
- viewModel.onEvent(
+ onEvent(
ReaderEvent.OnTranslateText(
textToTranslate,
context,
@@ -240,7 +279,7 @@ fun ReaderScreen(
)
},
onDictionaryRequested = { textToDefine ->
- viewModel.onEvent(
+ onEvent(
ReaderEvent.OnOpenDictionary(
textToDefine,
context,
@@ -260,13 +299,13 @@ fun ReaderScreen(
modifier = Modifier
.fillMaxSize()
.then(
- if (!loading && toolbarShowed) {
+ if (!loading.value && toolbarShowed) {
Modifier
.clickable(
interactionSource = null,
indication = null,
onClick = {
- viewModel.onEvent(
+ onEvent(
ReaderEvent.OnShowHideMenu(context = context)
)
}
@@ -276,29 +315,30 @@ fun ReaderScreen(
}
)
) {
- if (state.book.text.isNotEmpty()) {
+ if (state.value.book.text.isNotEmpty()) {
item {
DisableSelection {
- ReaderStartItem(viewModel = viewModel)
+ ReaderStartItem(state = state)
}
}
}
itemsIndexed(
- state.book.text, key = { _, key -> key.id }
+ state.value.book.text, key = { _, key -> key.id }
) { index, line ->
- val text = remember(mainState.paragraphIndentation) {
- "${if (mainState.paragraphIndentation!!) " " else ""}${line.line}"
+ val text = remember(mainState.value.paragraphIndentation) {
+ "${if (mainState.value.paragraphIndentation!!) " " else ""}${line.line}"
}
- val fontColor = remember(mainState.fontColor) {
- Color(mainState.fontColor!!.toULong())
+ val fontColor = remember(mainState.value.fontColor) {
+ Color(mainState.value.fontColor!!.toULong())
}
- val backgroundColor = remember(mainState.backgroundColor) {
- Color(mainState.backgroundColor!!.toULong())
- }
- val lineHeight = remember(mainState.fontSize, mainState.lineHeight) {
- (mainState.fontSize!! + mainState.lineHeight!!).sp
+ val backgroundColor = remember(mainState.value.backgroundColor) {
+ Color(mainState.value.backgroundColor!!.toULong())
}
+ val lineHeight =
+ remember(mainState.value.fontSize, mainState.value.lineHeight) {
+ (mainState.value.fontSize!! + mainState.value.lineHeight!!).sp
+ }
Column(
Modifier
@@ -308,8 +348,8 @@ fun ReaderScreen(
top = if (index == 0) 18.dp else 0.dp,
start = 18.dp,
end = 18.dp,
- bottom = if (index == state.book.text.lastIndex) 18.dp
- else (mainState.paragraphHeight!! * 3).dp
+ bottom = if (index == state.value.book.text.lastIndex) 18.dp
+ else (mainState.value.paragraphHeight!! * 3).dp
)
) {
Text(
@@ -319,25 +359,25 @@ fun ReaderScreen(
lineBreak = LineBreak.Paragraph
),
fontFamily = fontFamily.font,
- fontStyle = when (mainState.isItalic!!) {
+ fontStyle = when (mainState.value.isItalic!!) {
true -> FontStyle.Italic
false -> FontStyle.Normal
},
- fontSize = mainState.fontSize!!.sp,
+ fontSize = mainState.value.fontSize!!.sp,
lineHeight = lineHeight
)
}
}
- if (state.book.text.isNotEmpty()) {
+ if (state.value.book.text.isNotEmpty()) {
item {
DisableSelection {
ReaderEndItem(
- libraryViewModel = libraryViewModel,
- historyViewModel = historyViewModel,
- viewModel = viewModel,
- listState = listState,
- navigator = navigator
+ state = state,
+ onEvent = onEvent,
+ onLibraryEvent = onLibraryEvent,
+ onHistoryUpdateEvent = onHistoryEvent,
+ listState = listState
)
}
}
@@ -346,7 +386,7 @@ fun ReaderScreen(
}
}
- if (loading || state.errorMessage != null) {
+ if (loading.value || state.value.errorMessage != null) {
Column(
modifier = Modifier
.fillMaxSize()
@@ -354,20 +394,20 @@ fun ReaderScreen(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
- if (!loading && state.errorMessage != null) {
+ if (!loading.value) {
IsError(
- errorMessage = state.errorMessage!!.asString(),
+ errorMessage = state.value.errorMessage!!.asString(),
icon = painterResource(id = R.drawable.error),
actionTitle = stringResource(id = R.string.go_back),
action = {
- libraryViewModel.onEvent(LibraryEvent.OnLoadList)
- viewModel.onEvent(
+ onLibraryEvent(LibraryEvent.OnLoadList)
+ onEvent(
ReaderEvent.OnGoBack(
context,
navigator,
refreshList = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
+ onLibraryEvent(LibraryEvent.OnUpdateBook(it))
+ onHistoryEvent(HistoryEvent.OnUpdateBook(it))
},
listState = listState,
navigate = {
@@ -396,13 +436,13 @@ fun ReaderScreen(
}
BackHandler {
- viewModel.onEvent(
+ onEvent(
ReaderEvent.OnGoBack(
context,
navigator,
refreshList = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
+ onLibraryEvent(LibraryEvent.OnUpdateBook(it))
+ onHistoryEvent(HistoryEvent.OnUpdateBook(it))
},
listState = listState,
navigate = {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt
index b930042a..ae5f1610 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt
@@ -19,8 +19,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -32,14 +31,12 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Category
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
-import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
-import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
-import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
+import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
/**
* Reader end item. Displays at the end of the book.
@@ -47,18 +44,18 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ReaderEndItem(
- libraryViewModel: LibraryViewModel,
- historyViewModel: HistoryViewModel,
- viewModel: ReaderViewModel,
+ state: State,
+ onEvent: (ReaderEvent) -> Unit,
+ onLibraryEvent: (LibraryEvent) -> Unit,
+ onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
listState: LazyListState,
- navigator: Navigator
) {
- val state by viewModel.state.collectAsState()
+ val navigator = LocalNavigator.current
val context = LocalContext.current as ComponentActivity
val buttonText = remember {
context.getString(
- if (state.book.category != Category.ALREADY_READ) R.string.move_to_read
+ if (state.value.book.category != Category.ALREADY_READ) R.string.move_to_read
else R.string.back_to_library
)
}
@@ -76,7 +73,7 @@ fun ReaderEndItem(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
- stringResource(id = R.string.thanks_for_reading, state.book.title),
+ stringResource(id = R.string.thanks_for_reading, state.value.book.title),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
@@ -86,8 +83,8 @@ fun ReaderEndItem(
Text(
stringResource(
id = R.string.letters_and_words,
- state.book.letters,
- state.book.words
+ state.value.book.letters,
+ state.value.book.words
),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge,
@@ -96,15 +93,15 @@ fun ReaderEndItem(
Spacer(modifier = Modifier.height(12.dp))
TextButton(
onClick = {
- if (state.book.category != Category.ALREADY_READ) {
- viewModel.onEvent(
+ if (state.value.book.category != Category.ALREADY_READ) {
+ onEvent(
ReaderEvent.OnMoveBookToAlreadyRead(
onUpdateCategories = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
+ onLibraryEvent(LibraryEvent.OnUpdateBook(it))
+ onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
},
updatePage = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it))
+ onLibraryEvent(LibraryEvent.OnUpdateCurrentPage(it))
},
navigator = navigator,
listState = listState,
@@ -118,13 +115,13 @@ fun ReaderEndItem(
Toast.LENGTH_LONG
).show()
} else {
- viewModel.onEvent(
+ onEvent(
ReaderEvent.OnGoBack(
context,
navigator,
refreshList = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
+ onLibraryEvent(LibraryEvent.OnUpdateBook(it))
+ onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
},
listState = listState,
navigate = {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt
index a4d3862b..573f7ad9 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt
@@ -23,8 +23,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -33,15 +32,14 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CustomCoverImage
-import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
+import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
/**
* Reader start item. Displays at the beginning of the book.
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
-fun ReaderStartItem(viewModel: ReaderViewModel) {
- val state by viewModel.state.collectAsState()
+fun ReaderStartItem(state: State) {
Column(
modifier = Modifier
@@ -61,9 +59,9 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colorScheme.surfaceContainerLow)
) {
- if (state.book.coverImage != null) {
+ if (state.value.book.coverImage != null) {
CustomCoverImage(
- uri = state.book.coverImage!!,
+ uri = state.value.book.coverImage!!,
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(10.dp))
@@ -85,7 +83,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
Column(verticalArrangement = Arrangement.Center) {
Text(
- state.book.title,
+ state.value.book.title,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt
index 71d75518..2291b4d3 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt
@@ -15,7 +15,7 @@ import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -23,32 +23,30 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
-import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
-import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
-import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
+import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
/**
* Reader bottom bar. Has a slider to change progress.
*/
@Composable
fun ReaderBottomBar(
- viewModel: ReaderViewModel,
- libraryViewModel: LibraryViewModel,
- historyViewModel: HistoryViewModel,
+ state: State,
+ onEvent: (ReaderEvent) -> Unit,
+ onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
+ onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
listState: LazyListState,
- navigator: Navigator,
- systemBarsColor: Color
+ containerColor: Color
) {
- val state by viewModel.state.collectAsState()
- val progress by remember(state.book.progress) {
+ val navigator = LocalNavigator.current
+ val progress by remember(state.value.book.progress) {
derivedStateOf {
- (state.book.progress * 100)
+ (state.value.book.progress * 100)
.toDouble()
.removeDigits(4)
.removeTrailingZero()
@@ -59,7 +57,7 @@ fun ReaderBottomBar(
Column(
Modifier
.fillMaxWidth()
- .background(systemBarsColor)
+ .background(containerColor)
.clickable(
interactionSource = null,
indication = null,
@@ -78,20 +76,20 @@ fun ReaderBottomBar(
)
Spacer(modifier = Modifier.height(3.dp))
Slider(
- value = state.book.progress,
- enabled = !state.lockMenu,
+ value = state.value.book.progress,
+ enabled = !state.value.lockMenu,
onValueChange = {
if (listState.layoutInfo.totalItemsCount > 0) {
- viewModel.onEvent(ReaderEvent.OnScroll(listState, it))
- viewModel.onEvent(
+ onEvent(ReaderEvent.OnScroll(listState, it))
+ onEvent(
ReaderEvent.OnChangeProgress(
progress = it,
navigator = navigator,
firstVisibleItemIndex = listState.firstVisibleItemIndex,
firstVisibleItemOffset = 0,
refreshList = { book ->
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(book))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(book))
+ onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(book))
+ onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(book))
}
)
)
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt
index 1a8e6ee8..4d63334c 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt
@@ -15,7 +15,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -28,16 +28,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CustomIconButton
-import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
-import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
-import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
-import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
+import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
/**
* Reader top bar. Displays title of the book.
@@ -45,19 +43,19 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReaderTopBar(
- viewModel: ReaderViewModel,
- libraryViewModel: LibraryViewModel,
- historyViewModel: HistoryViewModel,
+ state: State,
+ onEvent: (ReaderEvent) -> Unit,
+ onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
+ onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
listState: LazyListState,
- navigator: Navigator,
containerColor: Color
) {
+ val navigator = LocalNavigator.current
val context = LocalContext.current as ComponentActivity
- val state by viewModel.state.collectAsState()
- val progress by remember(state.book.progress) {
+ val progress by remember(state.value.book.progress) {
derivedStateOf {
- (state.book.progress * 100)
+ (state.value.book.progress * 100)
.toDouble()
.removeDigits(2)
.removeTrailingZero()
@@ -72,13 +70,13 @@ fun ReaderTopBar(
contentDescription = R.string.go_back_content_desc,
disableOnClick = true
) {
- viewModel.onEvent(
+ onEvent(
ReaderEvent.OnGoBack(
context,
navigator,
refreshList = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
+ onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
+ onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
},
listState = listState,
navigate = {
@@ -91,7 +89,7 @@ fun ReaderTopBar(
title = {
Column(verticalArrangement = Arrangement.Center) {
Text(
- state.book.title,
+ state.value.book.title,
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 20.sp,
@@ -99,17 +97,17 @@ fun ReaderTopBar(
maxLines = 1,
modifier = Modifier
.clickable(
- enabled = !state.lockMenu,
+ enabled = !state.value.lockMenu,
interactionSource = null,
indication = null,
onClick = {
- viewModel.onEvent(
+ onEvent(
ReaderEvent.OnGoBack(
context,
navigator,
refreshList = {
- libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
- historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
+ onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
+ onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
},
listState = listState,
navigate = {
@@ -141,9 +139,9 @@ fun ReaderTopBar(
icon = Icons.Default.Settings,
contentDescription = R.string.open_reader_settings_content_desc,
disableOnClick = false,
- enabled = !state.lockMenu
+ enabled = !state.value.lockMenu
) {
- viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
+ onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
}
},
colors = TopAppBarDefaults.topAppBarColors(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt
index d76d68c7..eab6691f 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt
@@ -23,7 +23,7 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -39,9 +39,8 @@ import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.data.MainEvent
-import ua.acclorite.book_story.presentation.data.MainViewModel
+import ua.acclorite.book_story.presentation.data.MainSettingsState
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.settings.components.CheckboxWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle
@@ -54,7 +53,11 @@ import ua.acclorite.book_story.presentation.screens.settings.components.SliderWi
ExperimentalMaterial3Api::class
)
@Composable
-fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderViewModel) {
+fun ReaderSettingsBottomSheet(
+ mainState: State,
+ onEvent: (ReaderEvent) -> Unit,
+ onMainEvent: (MainEvent) -> Unit
+) {
val pagerState = rememberPagerState { 2 }
val currentPage by remember { derivedStateOf { pagerState.currentPage } }
val context = LocalContext.current
@@ -62,11 +65,9 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
val navigationBarPadding =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
- val state by mainViewModel.state.collectAsState()
-
- val fontFamily = remember(state.fontFamily) {
+ val fontFamily = remember(mainState.value.fontFamily) {
Constants.FONTS.find {
- it.id == state.fontFamily
+ it.id == mainState.value.fontFamily
} ?: Constants.FONTS[0]
}
@@ -89,7 +90,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
)
LaunchedEffect(currentPage) {
- viewModel.onEvent(
+ onEvent(
ReaderEvent.OnShowHideMenu(
currentPage != 1,
context as ComponentActivity
@@ -106,13 +107,16 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
.fillMaxWidth()
.fillMaxHeight(animatedHeight),
onDismissRequest = {
- viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
+ onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
},
sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0),
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
) {
- ReaderSettingsBottomSheetTabRow(viewModel = viewModel, pagerState = pagerState)
+ ReaderSettingsBottomSheetTabRow(
+ onEvent = onEvent,
+ pagerState = pagerState
+ )
HorizontalPager(state = pagerState) { page ->
LazyColumn {
@@ -144,7 +148,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
)
},
onClick = {
- mainViewModel.onEvent(MainEvent.OnChangeFontFamily(it.id))
+ onMainEvent(MainEvent.OnChangeFontFamily(it.id))
}
)
}
@@ -159,7 +163,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
fontFamily = fontFamily.font,
fontStyle = FontStyle.Normal
),
- !state.isItalic!!
+ !mainState.value.isItalic!!
),
ButtonItem(
"italic",
@@ -168,11 +172,11 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
fontFamily = fontFamily.font,
fontStyle = FontStyle.Italic
),
- state.isItalic!!
+ mainState.value.isItalic!!
),
),
onClick = {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeFontStyle(
when (it.id) {
"italic" -> true
@@ -185,12 +189,12 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
}
item {
SliderWithTitle(
- value = state.fontSize!! to "pt",
+ value = mainState.value.fontSize!! to "pt",
fromValue = 10,
toValue = 35,
title = stringResource(id = R.string.font_size_option),
onValueChange = {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeFontSize(it)
)
}
@@ -210,12 +214,12 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
}
item {
SliderWithTitle(
- value = state.lineHeight!! to "pt",
+ value = mainState.value.lineHeight!! to "pt",
fromValue = 1,
toValue = 16,
title = stringResource(id = R.string.line_height_option),
onValueChange = {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeLineHeight(it)
)
}
@@ -223,12 +227,12 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
}
item {
SliderWithTitle(
- value = state.paragraphHeight!! to "pt",
+ value = mainState.value.paragraphHeight!! to "pt",
fromValue = 0,
toValue = 24,
title = stringResource(id = R.string.paragraph_height_option),
onValueChange = {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeParagraphHeight(it)
)
}
@@ -236,11 +240,11 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
}
item {
CheckboxWithTitle(
- selected = state.paragraphIndentation!!,
+ selected = mainState.value.paragraphIndentation!!,
title = stringResource(id = R.string.paragraph_indentation_option)
) {
- mainViewModel.onEvent(
- MainEvent.OnChangeParagraphIndentation(!state.paragraphIndentation!!)
+ onMainEvent(
+ MainEvent.OnChangeParagraphIndentation(!mainState.value.paragraphIndentation!!)
)
}
}
@@ -252,10 +256,10 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
}
item {
ColorPickerWithTitle(
- value = Color(state.backgroundColor!!.toULong()),
+ value = Color(mainState.value.backgroundColor!!.toULong()),
title = stringResource(id = R.string.background_color_option),
onValueChange = {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeBackgroundColor(
it.value.toLong()
)
@@ -265,10 +269,10 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
}
item {
ColorPickerWithTitle(
- value = Color(state.fontColor!!.toULong()),
+ value = Color(mainState.value.fontColor!!.toULong()),
title = stringResource(id = R.string.font_color_option),
onValueChange = {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeFontColor(
it.value.toLong()
)
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt
index 45a4e7bd..784d2b53 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt
@@ -13,14 +13,16 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
-import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
/**
* Settings bottom sheet tab row. It is used to switch between categories.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: PagerState) {
+fun ReaderSettingsBottomSheetTabRow(
+ onEvent: (ReaderEvent) -> Unit,
+ pagerState: PagerState
+) {
val tabItems = listOf(
stringResource(id = R.string.general_tab),
stringResource(id = R.string.color_tab)
@@ -37,7 +39,7 @@ fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: Page
modifier = Modifier,
selected = pagerState.currentPage == index,
onClick = {
- viewModel.onEvent(ReaderEvent.OnScrollToSettingsPage(index, pagerState))
+ onEvent(ReaderEvent.OnScrollToSettingsPage(index, pagerState))
},
selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt
index c006f49f..b42b731b 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt
@@ -3,9 +3,11 @@ package ua.acclorite.book_story.presentation.screens.reader.data
import androidx.activity.ComponentActivity
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.pager.PagerState
+import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.data.Navigator
+@Immutable
sealed class ReaderEvent {
data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent()
data class OnLoadText(
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt
index 820090e7..8cc8e6e3 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt
@@ -25,17 +25,25 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.GoBackButton
-import ua.acclorite.book_story.presentation.components.collapsibleScrollBehaviorWithLazyListState
+import ua.acclorite.book_story.presentation.components.collapsibleUntilExitScrollBehaviorWithLazyListState
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem
+@Composable
+fun SettingsScreenRoot() {
+ val navigator = LocalNavigator.current
+
+ SettingsScreen(navigator = navigator)
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun SettingsScreen(
+private fun SettingsScreen(
navigator: Navigator
) {
- val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
+ val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
Scaffold(
Modifier
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/CheckboxWIthTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/CheckboxWIthTitle.kt
index afa52624..c9d3ac29 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/CheckboxWIthTitle.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/CheckboxWIthTitle.kt
@@ -1,18 +1,21 @@
package ua.acclorite.book_story.presentation.screens.settings.components
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
-import androidx.compose.material3.Checkbox
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import ua.acclorite.book_story.presentation.components.CategoryTitle
/**
* Checkbox with title.
@@ -21,24 +24,58 @@ import ua.acclorite.book_story.presentation.components.CategoryTitle
fun CheckboxWithTitle(
selected: Boolean,
title: String,
+ description: String? = null,
horizontalPadding: Dp = 18.dp,
- verticalPadding: Dp = 12.dp,
+ verticalPadding: Dp = 8.dp,
onClick: () -> Unit
) {
Row(
- Modifier
- .fillMaxWidth()
- .clickable {
- onClick()
- }
+ modifier = Modifier
+ .clickable { onClick() }
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
- verticalAlignment = Alignment.CenterVertically,
+ verticalAlignment = Alignment.CenterVertically
) {
- Checkbox(
+ Column(Modifier.weight(1f), verticalArrangement = Arrangement.Center) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = title,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.titleMedium
+ )
+ }
+ description?.let {
+ Text(
+ text = it,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Switch(
checked = selected,
- onCheckedChange = null,
+ onCheckedChange = null
)
- Spacer(modifier = Modifier.width(24.dp))
- CategoryTitle(title = title, padding = 0.dp)
}
+//
+// Row(
+// Modifier
+// .fillMaxWidth()
+// .clickable {
+// onClick()
+// }
+// .padding(horizontal = horizontalPadding, vertical = verticalPadding),
+// verticalAlignment = Alignment.CenterVertically,
+// ) {
+// Checkbox(
+// checked = selected,
+// onCheckedChange = null,
+// )
+// Spacer(modifier = Modifier.width(24.dp))
+// CategoryTitle(title = title, padding = 0.dp)
+// }
}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt
index 6742a3d1..8974fa33 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt
@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -61,7 +60,6 @@ fun SettingsCategoryItem(
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
- Spacer(modifier = Modifier.height(2.dp))
Text(
description,
style = MaterialTheme.typography.bodyMedium,
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsEvent.kt
new file mode 100644
index 00000000..c9bfa87f
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsEvent.kt
@@ -0,0 +1,18 @@
+@file:OptIn(ExperimentalPermissionsApi::class)
+
+package ua.acclorite.book_story.presentation.screens.settings.data
+
+import androidx.activity.ComponentActivity
+import androidx.compose.runtime.Immutable
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.PermissionState
+
+@Immutable
+sealed class SettingsEvent {
+ data class OnGeneralChangeCheckForUpdates(
+ val enable: Boolean,
+ val activity: ComponentActivity,
+ val notificationsPermissionState: PermissionState,
+ val onChangeCheckForUpdates: (Boolean) -> Unit
+ ) : SettingsEvent()
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsViewModel.kt
new file mode 100644
index 00000000..8ca40fd3
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsViewModel.kt
@@ -0,0 +1,85 @@
+package ua.acclorite.book_story.presentation.screens.settings.data
+
+import android.content.Intent
+import android.os.Build
+import android.provider.Settings
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.isGranted
+import com.google.accompanist.permissions.shouldShowRationale
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.yield
+import javax.inject.Inject
+
+@OptIn(ExperimentalPermissionsApi::class)
+@HiltViewModel
+class SettingsViewModel @Inject constructor(
+
+) : ViewModel() {
+
+ private var notificationsPermissionJob: Job? = null
+
+ fun onEvent(event: SettingsEvent) {
+ when (event) {
+
+ is SettingsEvent.OnGeneralChangeCheckForUpdates -> {
+ if (!event.enable) {
+ event.onChangeCheckForUpdates(false)
+ return
+ }
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ event.onChangeCheckForUpdates(true)
+ return
+ }
+
+ if (event.notificationsPermissionState.status.isGranted) {
+ event.onChangeCheckForUpdates(true)
+ return
+ }
+
+ if (event.notificationsPermissionState.status.shouldShowRationale) {
+ event.notificationsPermissionState.launchPermissionRequest()
+ } else {
+ val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
+ intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName)
+
+ if (intent.resolveActivity(event.activity.packageManager) != null) {
+ event.activity.startActivity(intent)
+ } else {
+ return
+ }
+ }
+
+ notificationsPermissionJob?.cancel()
+ notificationsPermissionJob = viewModelScope.launch {
+ for (i in 1..10) {
+ if (!event.notificationsPermissionState.status.isGranted) {
+ delay(1000)
+ yield()
+ continue
+ }
+
+ yield()
+
+ event.onChangeCheckForUpdates(true)
+ break
+ }
+ }
+ }
+ }
+ }
+}
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt
index c6a47ecf..c9987f48 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt
@@ -16,23 +16,25 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.GoBackButton
-import ua.acclorite.book_story.presentation.components.collapsibleScrollBehaviorWithLazyListState
+import ua.acclorite.book_story.presentation.components.collapsibleUntilExitScrollBehaviorWithLazyListState
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.MainEvent
+import ua.acclorite.book_story.presentation.data.MainSettingsState
import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle
@@ -47,20 +49,33 @@ import ua.acclorite.book_story.presentation.ui.ThemeContrast
import ua.acclorite.book_story.presentation.ui.isDark
import ua.acclorite.book_story.presentation.ui.isPureDark
+@Composable
+fun AppearanceSettingsRoot() {
+ val navigator = LocalNavigator.current
+ val mainViewModel: MainViewModel = hiltViewModel()
+
+ val state = mainViewModel.state.collectAsState()
+
+ AppearanceSettings(
+ state = state,
+ navigator = navigator,
+ onMainEvent = mainViewModel::onEvent
+ )
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun AppearanceSettings(
- mainViewModel: MainViewModel,
- navigator: Navigator
+private fun AppearanceSettings(
+ state: State,
+ navigator: Navigator,
+ onMainEvent: (MainEvent) -> Unit
) {
- val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
+ val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
+ val themeContrastTheme = remember { mutableStateOf(state.value.theme!!) }
- val state by mainViewModel.state.collectAsState()
- var themeContrastTheme by remember { mutableStateOf(state.theme!!) }
-
- LaunchedEffect(state.theme) {
- if (themeContrastTheme != state.theme && state.theme != Theme.DYNAMIC) {
- themeContrastTheme = state.theme!!
+ LaunchedEffect(state.value.theme) {
+ if (themeContrastTheme.value != state.value.theme && state.value.theme != Theme.DYNAMIC) {
+ themeContrastTheme.value = state.value.theme!!
}
}
@@ -128,11 +143,11 @@ fun AppearanceSettings(
DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system)
},
MaterialTheme.typography.labelLarge,
- it == state.darkTheme
+ it == state.value.darkTheme
)
}
) {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeDarkTheme(
it.id
)
@@ -143,27 +158,28 @@ fun AppearanceSettings(
item {
AppearanceSettingsThemeSwitcher(
modifier = Modifier.animateItem(),
- mainViewModel = mainViewModel
+ state = state,
+ onMainEvent = onMainEvent
)
}
item {
BookStoryTheme(
- theme = themeContrastTheme,
- isDark = state.darkTheme!!.isDark(),
- isPureDark = state.pureDark!!.isPureDark(context = LocalContext.current),
- themeContrast = state.themeContrast!!
+ theme = themeContrastTheme.value,
+ isDark = state.value.darkTheme!!.isDark(),
+ isPureDark = state.value.pureDark!!.isPureDark(context = LocalContext.current),
+ themeContrast = state.value.themeContrast!!
) {
SlidingTransition(
modifier = Modifier.animateItem(
fadeInSpec = null,
fadeOutSpec = null
),
- visible = state.theme != Theme.DYNAMIC,
+ visible = state.value.theme != Theme.DYNAMIC,
) {
SegmentedButtonWithTitle(
title = stringResource(id = R.string.theme_contrast_option),
- locked = state.theme != Theme.DYNAMIC,
+ locked = state.value.theme != Theme.DYNAMIC,
buttons = ThemeContrast.entries.map {
ButtonItem(
it.toString(),
@@ -173,11 +189,11 @@ fun AppearanceSettings(
ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high)
},
MaterialTheme.typography.labelLarge,
- it == state.themeContrast
+ it == state.value.themeContrast
)
}
) {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeThemeContrast(
it.id
)
@@ -193,7 +209,7 @@ fun AppearanceSettings(
fadeInSpec = null,
fadeOutSpec = null
),
- visible = state.darkTheme!!.isDark(),
+ visible = state.value.darkTheme!!.isDark(),
) {
SegmentedButtonWithTitle(
title = stringResource(id = R.string.pure_dark_option),
@@ -207,11 +223,11 @@ fun AppearanceSettings(
PureDark.SAVER -> stringResource(id = R.string.pure_dark_power_saver)
},
MaterialTheme.typography.labelLarge,
- it == state.pureDark
+ it == state.value.pureDark
)
}
) {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangePureDark(
it.id
)
@@ -241,10 +257,10 @@ fun AppearanceSettings(
item {
ColorPickerWithTitle(
modifier = Modifier.animateItem(),
- value = Color(state.backgroundColor!!.toULong()),
+ value = Color(state.value.backgroundColor!!.toULong()),
title = stringResource(id = R.string.background_color_option),
onValueChange = {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeBackgroundColor(
it.value.toLong()
)
@@ -255,10 +271,10 @@ fun AppearanceSettings(
item {
ColorPickerWithTitle(
modifier = Modifier.animateItem(),
- value = Color(state.fontColor!!.toULong()),
+ value = Color(state.value.fontColor!!.toULong()),
title = stringResource(id = R.string.font_color_option),
onValueChange = {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeFontColor(
it.value.toLong()
)
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt
index 1108e26c..af1f723d 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt
@@ -10,8 +10,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -22,7 +21,7 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.data.MainEvent
-import ua.acclorite.book_story.presentation.data.MainViewModel
+import ua.acclorite.book_story.presentation.data.MainSettingsState
import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.isDark
import ua.acclorite.book_story.presentation.ui.isPureDark
@@ -33,10 +32,10 @@ import ua.acclorite.book_story.presentation.ui.isPureDark
@Composable
fun AppearanceSettingsThemeSwitcher(
modifier: Modifier = Modifier,
- mainViewModel: MainViewModel,
+ state: State,
+ onMainEvent: (MainEvent) -> Unit,
verticalPadding: Dp = 8.dp
) {
- val state by mainViewModel.state.collectAsState()
val themes = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES
else Constants.THEMES.dropWhile { it.first == Theme.DYNAMIC }
@@ -64,12 +63,12 @@ fun AppearanceSettingsThemeSwitcher(
AppearanceSettingsThemeSwitcherItem(
theme = themeEntry,
- darkTheme = state.darkTheme!!.isDark(),
- themeContrast = state.themeContrast!!,
- isPureDark = state.pureDark!!.isPureDark(context = LocalContext.current),
- selected = state.theme == themeEntry.first
+ darkTheme = state.value.darkTheme!!.isDark(),
+ themeContrast = state.value.themeContrast!!,
+ isPureDark = state.value.pureDark!!.isPureDark(context = LocalContext.current),
+ selected = state.value.theme == themeEntry.first
) {
- mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.first.toString()))
+ onMainEvent(MainEvent.OnChangeTheme(themeEntry.first.toString()))
}
if (index == themes.lastIndex) {
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt
index b7611d99..f86179d1 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt
@@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -34,6 +33,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.UIText
+import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.ThemeContrast
import ua.acclorite.book_story.presentation.ui.animatedColorScheme
@@ -94,7 +94,7 @@ fun AppearanceSettingsThemeSwitcherItem(
)
Spacer(modifier = Modifier.width(8.dp))
- AnimatedVisibility(
+ CustomAnimatedVisibility(
visible = selected,
enter = scaleIn(tween(300), initialScale = 0.5f) +
fadeIn(tween(300)),
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt
index b738d306..8012747f 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt
@@ -1,5 +1,8 @@
package ua.acclorite.book_story.presentation.screens.settings.nested.general
+import android.Manifest
+import android.annotation.SuppressLint
+import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
@@ -15,30 +18,62 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.rememberPermissionState
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.GoBackButton
-import ua.acclorite.book_story.presentation.components.collapsibleScrollBehaviorWithLazyListState
+import ua.acclorite.book_story.presentation.components.collapsibleUntilExitScrollBehaviorWithLazyListState
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.MainEvent
+import ua.acclorite.book_story.presentation.data.MainSettingsState
import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator
+import ua.acclorite.book_story.presentation.screens.settings.components.CheckboxWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
+import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent
+import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun GeneralSettings(
- mainViewModel: MainViewModel,
- navigator: Navigator
+fun GeneralSettingsRoot() {
+ val navigator = LocalNavigator.current
+ val settingsViewModel: SettingsViewModel = hiltViewModel()
+ val mainViewModel: MainViewModel = hiltViewModel()
+
+ val state = mainViewModel.state.collectAsState()
+
+ GeneralSettings(
+ state = state,
+ navigator = navigator,
+ onSettingsEvent = settingsViewModel::onEvent,
+ onMainEvent = mainViewModel::onEvent
+ )
+}
+
+@SuppressLint("InlinedApi")
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
+@Composable
+private fun GeneralSettings(
+ state: State,
+ navigator: Navigator,
+ onSettingsEvent: (SettingsEvent) -> Unit,
+ onMainEvent: (MainEvent) -> Unit
) {
- val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
- val state by mainViewModel.state.collectAsState()
+ val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
+ val activity = LocalContext.current as ComponentActivity
+
+ val notificationsPermissionState = rememberPermissionState(
+ permission = Manifest.permission.POST_NOTIFICATIONS
+ )
Scaffold(
Modifier
@@ -79,11 +114,11 @@ fun GeneralSettings(
it.first,
it.second,
MaterialTheme.typography.labelLarge,
- it.first == state.language
+ it.first == state.value.language
)
}.sortedBy { it.title }
) {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeLanguage(
it.id
)
@@ -91,6 +126,29 @@ fun GeneralSettings(
}
}
+ item {
+ CheckboxWithTitle(
+ selected = state.value.checkForUpdates!!,
+ title = stringResource(id = R.string.check_for_updates_option),
+ description = stringResource(id = R.string.check_for_updates_option_desc)
+ ) {
+ onSettingsEvent(
+ SettingsEvent.OnGeneralChangeCheckForUpdates(
+ enable = !state.value.checkForUpdates!!,
+ activity = activity,
+ notificationsPermissionState = notificationsPermissionState,
+ onChangeCheckForUpdates = {
+ onMainEvent(
+ MainEvent.OnChangeCheckForUpdates(
+ it
+ )
+ )
+ }
+ )
+ )
+ }
+ }
+
item { Spacer(modifier = Modifier.height(48.dp)) }
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt
index f1cc5042..523815ee 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt
@@ -15,39 +15,56 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.GoBackButton
-import ua.acclorite.book_story.presentation.components.collapsibleScrollBehaviorWithLazyListState
+import ua.acclorite.book_story.presentation.components.collapsibleUntilExitScrollBehaviorWithLazyListState
+import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.MainEvent
+import ua.acclorite.book_story.presentation.data.MainSettingsState
import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.settings.components.CheckboxWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle
+@Composable
+fun ReaderSettingsRoot() {
+ val navigator = LocalNavigator.current
+ val mainViewModel: MainViewModel = hiltViewModel()
+
+ val state = mainViewModel.state.collectAsState()
+
+ ReaderSettings(
+ state = state,
+ navigator = navigator,
+ onMainEvent = mainViewModel::onEvent
+ )
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun ReaderSettings(
- mainViewModel: MainViewModel,
- navigator: Navigator
+private fun ReaderSettings(
+ state: State,
+ navigator: Navigator,
+ onMainEvent: (MainEvent) -> Unit
) {
- val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
+ val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
- val state by mainViewModel.state.collectAsState()
- val fontFamily = remember(state.fontFamily) {
+ val fontFamily = remember(state.value.fontFamily) {
Constants.FONTS.find {
- it.id == state.fontFamily
+ it.id == state.value.fontFamily
} ?: Constants.FONTS[0]
}
@@ -106,7 +123,7 @@ fun ReaderSettings(
)
},
onClick = {
- mainViewModel.onEvent(MainEvent.OnChangeFontFamily(it.id))
+ onMainEvent(MainEvent.OnChangeFontFamily(it.id))
}
)
}
@@ -121,7 +138,7 @@ fun ReaderSettings(
fontFamily = fontFamily.font,
fontStyle = FontStyle.Normal
),
- !state.isItalic!!
+ !state.value.isItalic!!
),
ButtonItem(
"italic",
@@ -130,11 +147,11 @@ fun ReaderSettings(
fontFamily = fontFamily.font,
fontStyle = FontStyle.Italic
),
- state.isItalic!!
+ state.value.isItalic!!
),
),
onClick = {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeFontStyle(
when (it.id) {
"italic" -> true
@@ -147,12 +164,12 @@ fun ReaderSettings(
}
item {
SliderWithTitle(
- value = state.fontSize!! to "pt",
+ value = state.value.fontSize!! to "pt",
fromValue = 10,
toValue = 35,
title = stringResource(id = R.string.font_size_option),
onValueChange = {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeFontSize(it)
)
}
@@ -172,12 +189,12 @@ fun ReaderSettings(
}
item {
SliderWithTitle(
- value = state.lineHeight!! to "pt",
+ value = state.value.lineHeight!! to "pt",
fromValue = 1,
toValue = 16,
title = stringResource(id = R.string.line_height_option),
onValueChange = {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeLineHeight(it)
)
}
@@ -185,12 +202,12 @@ fun ReaderSettings(
}
item {
SliderWithTitle(
- value = state.paragraphHeight!! to "pt",
+ value = state.value.paragraphHeight!! to "pt",
fromValue = 0,
toValue = 24,
title = stringResource(id = R.string.paragraph_height_option),
onValueChange = {
- mainViewModel.onEvent(
+ onMainEvent(
MainEvent.OnChangeParagraphHeight(it)
)
}
@@ -198,11 +215,11 @@ fun ReaderSettings(
}
item {
CheckboxWithTitle(
- selected = state.paragraphIndentation!!,
+ selected = state.value.paragraphIndentation!!,
title = stringResource(id = R.string.paragraph_indentation_option)
) {
- mainViewModel.onEvent(
- MainEvent.OnChangeParagraphIndentation(!state.paragraphIndentation!!)
+ onMainEvent(
+ MainEvent.OnChangeParagraphIndentation(!state.value.paragraphIndentation!!)
)
}
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/StartScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/StartScreen.kt
new file mode 100644
index 00000000..a2c21f30
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/StartScreen.kt
@@ -0,0 +1,106 @@
+@file:OptIn(ExperimentalPermissionsApi::class)
+
+package ua.acclorite.book_story.presentation.screens.start
+
+import android.Manifest
+import android.annotation.SuppressLint
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.BackHandler
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.platform.LocalContext
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.PermissionState
+import com.google.accompanist.permissions.rememberPermissionState
+import ua.acclorite.book_story.presentation.data.MainEvent
+import ua.acclorite.book_story.presentation.data.MainSettingsState
+import ua.acclorite.book_story.presentation.data.MainViewModel
+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.start.components.StartDone
+import ua.acclorite.book_story.presentation.screens.start.components.StartSettings
+import ua.acclorite.book_story.presentation.screens.start.data.StartEvent
+import ua.acclorite.book_story.presentation.screens.start.data.StartState
+import ua.acclorite.book_story.presentation.screens.start.data.StartViewModel
+
+@SuppressLint("InlinedApi")
+@Composable
+fun StartScreenRoot() {
+ val startViewModel: StartViewModel = hiltViewModel()
+ val mainViewModel: MainViewModel = hiltViewModel()
+ val browseViewModel: BrowseViewModel = hiltViewModel()
+
+ val state = startViewModel.state.collectAsState()
+ val mainState = mainViewModel.state.collectAsState()
+ val storagePermissionState = rememberPermissionState(
+ permission = Manifest.permission.READ_EXTERNAL_STORAGE
+ )
+ val notificationsPermissionState = rememberPermissionState(
+ permission = Manifest.permission.POST_NOTIFICATIONS
+ )
+
+ LaunchedEffect(Unit) {
+ startViewModel.checkPermissions(
+ storagePermissionState = storagePermissionState,
+ notificationPermissionState = notificationsPermissionState,
+ isCheckForUpdatesEnabled = mainState.value.checkForUpdates!!,
+ onEnableCheckForUpdates = {
+ mainViewModel.onEvent(MainEvent.OnChangeCheckForUpdates(true))
+ }
+ )
+ }
+
+ StartScreen(
+ state = state,
+ mainState = mainState,
+ storagePermissionState = storagePermissionState,
+ notificationsPermissionState = notificationsPermissionState,
+ onEvent = startViewModel::onEvent,
+ onMainEvent = mainViewModel::onEvent,
+ onBrowseEvent = browseViewModel::onEvent
+ )
+}
+
+@OptIn(ExperimentalPermissionsApi::class)
+@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "InlinedApi")
+@Composable
+private fun StartScreen(
+ state: State,
+ mainState: State,
+ storagePermissionState: PermissionState,
+ notificationsPermissionState: PermissionState,
+ onEvent: (StartEvent) -> Unit,
+ onMainEvent: (MainEvent) -> Unit,
+ onBrowseEvent: (BrowseEvent) -> Unit,
+) {
+ val activity = LocalContext.current as ComponentActivity
+
+ // Settings
+ StartSettings(
+ state = state,
+ mainState = mainState,
+ onEvent = onEvent,
+ onMainEvent = onMainEvent,
+ storagePermissionState = storagePermissionState,
+ notificationsPermissionState = notificationsPermissionState
+ )
+
+ // Done
+ StartDone(
+ state = state,
+ onEvent = onEvent,
+ onMainEvent = onMainEvent,
+ onBrowseEvent = onBrowseEvent
+ )
+
+ BackHandler {
+ onEvent(
+ StartEvent.OnGoBack {
+ activity.finish()
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartAppearanceScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartAppearanceScreen.kt
new file mode 100644
index 00000000..5a1da06b
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartAppearanceScreen.kt
@@ -0,0 +1,172 @@
+package ua.acclorite.book_story.presentation.screens.start.components
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.State
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+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.ButtonItem
+import ua.acclorite.book_story.presentation.components.CategoryTitle
+import ua.acclorite.book_story.presentation.data.MainEvent
+import ua.acclorite.book_story.presentation.data.MainSettingsState
+import ua.acclorite.book_story.presentation.screens.settings.components.SegmentedButtonWithTitle
+import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher.AppearanceSettingsThemeSwitcher
+import ua.acclorite.book_story.presentation.ui.BookStoryTheme
+import ua.acclorite.book_story.presentation.ui.DarkTheme
+import ua.acclorite.book_story.presentation.ui.PureDark
+import ua.acclorite.book_story.presentation.ui.SlidingTransition
+import ua.acclorite.book_story.presentation.ui.Theme
+import ua.acclorite.book_story.presentation.ui.ThemeContrast
+import ua.acclorite.book_story.presentation.ui.isDark
+import ua.acclorite.book_story.presentation.ui.isPureDark
+
+/**
+ * Appearance settings.
+ */
+fun LazyListScope.startAppearanceScreen(
+ mainState: State,
+ onMainEvent: (MainEvent) -> Unit,
+ themeContrastTheme: State
+) {
+ item {
+ Spacer(
+ modifier = Modifier
+ .animateItem()
+ .height(16.dp)
+ )
+ }
+ item {
+ CategoryTitle(
+ modifier = Modifier.animateItem(),
+ title = stringResource(id = R.string.start_theme_preferences),
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ item {
+ Spacer(
+ modifier = Modifier
+ .animateItem()
+ .height(8.dp)
+ )
+ }
+
+ item {
+ SegmentedButtonWithTitle(
+ modifier = Modifier.animateItem(),
+ title = stringResource(id = R.string.dark_theme_option),
+ locked = true,
+ buttons = DarkTheme.entries.map {
+ ButtonItem(
+ it.toString(),
+ when (it) {
+ DarkTheme.OFF -> stringResource(id = R.string.dark_theme_off)
+ DarkTheme.ON -> stringResource(id = R.string.dark_theme_on)
+ DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system)
+ },
+ MaterialTheme.typography.labelLarge,
+ it == mainState.value.darkTheme
+ )
+ }
+ ) {
+ onMainEvent(
+ MainEvent.OnChangeDarkTheme(
+ it.id
+ )
+ )
+ }
+ }
+
+ item {
+ AppearanceSettingsThemeSwitcher(
+ modifier = Modifier.animateItem(),
+ state = mainState,
+ onMainEvent = onMainEvent
+ )
+ }
+
+ item {
+ BookStoryTheme(
+ theme = themeContrastTheme.value,
+ isDark = mainState.value.darkTheme!!.isDark(),
+ isPureDark = mainState.value.pureDark!!.isPureDark(context = LocalContext.current),
+ themeContrast = mainState.value.themeContrast!!
+ ) {
+ SlidingTransition(
+ modifier = Modifier.animateItem(
+ fadeInSpec = null,
+ fadeOutSpec = null
+ ),
+ visible = mainState.value.theme != Theme.DYNAMIC,
+ ) {
+ SegmentedButtonWithTitle(
+ title = stringResource(id = R.string.theme_contrast_option),
+ locked = mainState.value.theme != Theme.DYNAMIC,
+ buttons = ThemeContrast.entries.map {
+ ButtonItem(
+ it.toString(),
+ when (it) {
+ ThemeContrast.STANDARD -> stringResource(id = R.string.theme_contrast_standard)
+ ThemeContrast.MEDIUM -> stringResource(id = R.string.theme_contrast_medium)
+ ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high)
+ },
+ MaterialTheme.typography.labelLarge,
+ it == mainState.value.themeContrast
+ )
+ }
+ ) {
+ onMainEvent(
+ MainEvent.OnChangeThemeContrast(
+ it.id
+ )
+ )
+ }
+ }
+ }
+ }
+
+ item {
+ SlidingTransition(
+ modifier = Modifier.animateItem(
+ fadeInSpec = null,
+ fadeOutSpec = null
+ ),
+ visible = mainState.value.darkTheme!!.isDark(),
+ ) {
+ SegmentedButtonWithTitle(
+ title = stringResource(id = R.string.pure_dark_option),
+ locked = true,
+ buttons = PureDark.entries.map {
+ ButtonItem(
+ it.toString(),
+ when (it) {
+ PureDark.OFF -> stringResource(id = R.string.pure_dark_off)
+ PureDark.ON -> stringResource(id = R.string.pure_dark_on)
+ PureDark.SAVER -> stringResource(id = R.string.pure_dark_power_saver)
+ },
+ MaterialTheme.typography.labelLarge,
+ it == mainState.value.pureDark
+ )
+ }
+ ) {
+ onMainEvent(
+ MainEvent.OnChangePureDark(
+ it.id
+ )
+ )
+ }
+ }
+ }
+
+ item {
+ Spacer(
+ modifier = Modifier
+ .animateItem()
+ .height(8.dp)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartDone.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartDone.kt
new file mode 100644
index 00000000..1ec4d65c
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartDone.kt
@@ -0,0 +1,129 @@
+package ua.acclorite.book_story.presentation.screens.start.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+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.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.presentation.data.LocalNavigator
+import ua.acclorite.book_story.presentation.data.MainEvent
+import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
+import ua.acclorite.book_story.presentation.screens.start.data.StartEvent
+import ua.acclorite.book_story.presentation.screens.start.data.StartState
+
+/**
+ * Done screen.
+ */
+@Composable
+fun StartDone(
+ state: State,
+ onEvent: (StartEvent) -> Unit,
+ onMainEvent: (MainEvent) -> Unit,
+ onBrowseEvent: (BrowseEvent) -> Unit
+) {
+ val navigator = LocalNavigator.current
+
+ StartNavigationTransition(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .padding(48.dp),
+ state = state,
+ visible = state.value.isDone,
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ bottomBar = {
+ Column {
+ Spacer(modifier = Modifier.height(18.dp))
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .navigationBarsPadding()
+ .padding(bottom = 8.dp)
+ .padding(horizontal = 18.dp)
+ .fillMaxWidth()
+ ) {
+ TextButton(
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(100),
+ contentPadding = ButtonDefaults.ContentPadding,
+ onClick = {
+ onEvent(
+ StartEvent.OnGoToBrowse(
+ navigator,
+ onCompletedStartGuide = {
+ onBrowseEvent(BrowseEvent.OnLoadList)
+ onMainEvent(
+ MainEvent.OnChangeShowStartScreen(false)
+ )
+ }
+ )
+ )
+ }
+ ) {
+ Text(text = stringResource(id = R.string.no))
+ }
+ Spacer(modifier = Modifier.width(18.dp))
+ Button(
+ modifier = Modifier.weight(3f),
+ shape = RoundedCornerShape(100),
+ onClick = {
+ onEvent(
+ StartEvent.OnGoToHelp(
+ navigator
+ )
+ )
+ }
+ ) {
+ Text(text = stringResource(id = R.string.yes_go_to_help))
+ }
+ }
+
+
+ }
+ }
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.checklist),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(150.dp)
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = stringResource(id = R.string.start_done),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.headlineMedium,
+ textAlign = TextAlign.Center
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = stringResource(id = R.string.start_done_desc),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodyMedium,
+ textAlign = TextAlign.Center
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartLanguageScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartLanguageScreen.kt
new file mode 100644
index 00000000..2c918275
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartLanguageScreen.kt
@@ -0,0 +1,46 @@
+package ua.acclorite.book_story.presentation.screens.start.components
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.ui.Modifier
+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.ButtonItem
+import ua.acclorite.book_story.presentation.components.CategoryTitle
+import ua.acclorite.book_story.presentation.components.custom_dialog.SelectableDialogItem
+import ua.acclorite.book_story.presentation.data.MainEvent
+
+/**
+ * Language settings.
+ */
+fun LazyListScope.startLanguageScreen(
+ onMainEvent: (MainEvent) -> Unit,
+ languages: List
+) {
+ item {
+ Spacer(modifier = Modifier.height(16.dp))
+ CategoryTitle(
+ title = stringResource(id = R.string.start_language_preferences),
+ color = MaterialTheme.colorScheme.primary
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ }
+
+ items(languages) {
+ SelectableDialogItem(
+ selected = it.selected,
+ title = it.title,
+ horizontalPadding = 18.dp
+ ) {
+ onMainEvent(MainEvent.OnChangeLanguage(it.id))
+ }
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartNavigationScreenItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartNavigationScreenItem.kt
new file mode 100644
index 00000000..f7aa423b
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartNavigationScreenItem.kt
@@ -0,0 +1,44 @@
+package ua.acclorite.book_story.presentation.screens.start.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.ui.Modifier
+import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
+import ua.acclorite.book_story.presentation.screens.start.data.StartNavigationScreen
+import ua.acclorite.book_story.presentation.screens.start.data.StartState
+import ua.acclorite.book_story.presentation.ui.Transitions
+
+/**
+ * Navigation item screen. Uses sliding anim.
+ */
+@Composable
+fun StartNavigationScreenItem(
+ state: State,
+ screen: StartNavigationScreen,
+ content: LazyListScope.() -> Unit
+) {
+ val listState = rememberLazyListState()
+
+ CustomAnimatedVisibility(
+ visible = state.value.currentScreen == screen,
+ enter = if (state.value.useBackAnimation) Transitions.BackSlidingTransitionIn
+ else Transitions.SlidingTransitionIn,
+ exit = if (state.value.useBackAnimation) Transitions.BackSlidingTransitionOut
+ else Transitions.SlidingTransitionOut
+ ) {
+ LazyColumn(
+ Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.surfaceContainerLow),
+ state = listState
+ ) {
+ content()
+ }
+ }
+}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartNavigationTransition.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartNavigationTransition.kt
new file mode 100644
index 00000000..1b84a208
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartNavigationTransition.kt
@@ -0,0 +1,55 @@
+package ua.acclorite.book_story.presentation.screens.start.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
+import ua.acclorite.book_story.presentation.screens.start.data.StartState
+import ua.acclorite.book_story.presentation.ui.Transitions
+
+/**
+ * Transition between [StartSettings] and [StartDone].
+ */
+@Composable
+fun StartNavigationTransition(
+ modifier: Modifier = Modifier,
+ state: State,
+ visible: Boolean,
+ bottomBar: @Composable () -> Unit,
+ horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+ verticalArrangement: Arrangement.Vertical = Arrangement.Top,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ CustomAnimatedVisibility(
+ visible = visible,
+ enter = if (state.value.useBackAnimation) Transitions.BackSlidingTransitionIn
+ else Transitions.SlidingTransitionIn,
+ exit = if (state.value.useBackAnimation) Transitions.BackSlidingTransitionOut
+ else Transitions.SlidingTransitionOut
+ ) {
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ bottomBar = bottomBar,
+ containerColor = MaterialTheme.colorScheme.surface
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it)
+ .then(modifier),
+ verticalArrangement = verticalArrangement,
+ horizontalAlignment = horizontalAlignment
+ ) {
+ content()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartSettings.kt
new file mode 100644
index 00000000..c9aeccb2
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartSettings.kt
@@ -0,0 +1,175 @@
+package ua.acclorite.book_story.presentation.screens.start.components
+
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.GolfCourse
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.PermissionState
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.domain.model.ButtonItem
+import ua.acclorite.book_story.domain.util.Constants
+import ua.acclorite.book_story.presentation.data.MainEvent
+import ua.acclorite.book_story.presentation.data.MainSettingsState
+import ua.acclorite.book_story.presentation.screens.start.components.permissions.startPermissionsScreen
+import ua.acclorite.book_story.presentation.screens.start.data.StartEvent
+import ua.acclorite.book_story.presentation.screens.start.data.StartNavigationScreen
+import ua.acclorite.book_story.presentation.screens.start.data.StartState
+import ua.acclorite.book_story.presentation.ui.Theme
+
+/**
+ * Settings screen.
+ */
+@OptIn(ExperimentalPermissionsApi::class)
+@Composable
+fun StartSettings(
+ state: State,
+ mainState: State,
+ onEvent: (StartEvent) -> Unit,
+ onMainEvent: (MainEvent) -> Unit,
+ storagePermissionState: PermissionState,
+ notificationsPermissionState: PermissionState
+) {
+ val activity = LocalContext.current as ComponentActivity
+
+ val languages = remember(mainState.value.language) {
+ Constants.LANGUAGES.sortedBy { it.second }.map {
+ ButtonItem(
+ it.first,
+ it.second,
+ TextStyle(),
+ it.first == mainState.value.language
+ )
+ }.sortedBy { it.title }
+ }
+ val themeContrastTheme = remember { mutableStateOf(mainState.value.theme!!) }
+
+ LaunchedEffect(mainState.value.theme) {
+ if (themeContrastTheme.value != mainState.value.theme && mainState.value.theme != Theme.DYNAMIC) {
+ themeContrastTheme.value = mainState.value.theme!!
+ }
+ }
+
+ StartNavigationTransition(
+ modifier = Modifier.padding(horizontal = 18.dp),
+ state = state,
+ visible = !state.value.isDone,
+ bottomBar = {
+ Column {
+ Spacer(modifier = Modifier.height(18.dp))
+ Button(
+ modifier = Modifier
+ .navigationBarsPadding()
+ .padding(bottom = 8.dp)
+ .padding(horizontal = 18.dp)
+ .fillMaxWidth(),
+ shape = RoundedCornerShape(100),
+ onClick = { onEvent(StartEvent.OnGoForward) },
+ enabled = state.value.storagePermissionGranted ||
+ state.value.currentScreen != StartNavigationScreen.PERMISSIONS_THIRD
+ ) {
+ Text(text = stringResource(id = R.string.next))
+ }
+ }
+ }
+ ) {
+ Spacer(modifier = Modifier.height(24.dp))
+ Icon(
+ imageVector = Icons.Default.GolfCourse,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(64.dp)
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(
+ text = stringResource(id = R.string.start_welcome),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.headlineSmall
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = stringResource(id = R.string.start_welcome_desc),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ Spacer(modifier = Modifier.height(18.dp))
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(MaterialTheme.shapes.large)
+ .background(MaterialTheme.colorScheme.surfaceContainerLow),
+ ) {
+ StartNavigationScreenItem(
+ state = state,
+ screen = StartNavigationScreen.LANGUAGE_FIRST
+ ) {
+ startLanguageScreen(onMainEvent = onMainEvent, languages = languages)
+ }
+
+ StartNavigationScreenItem(
+ state = state,
+ screen = StartNavigationScreen.APPEARANCE_SECOND
+ ) {
+ startAppearanceScreen(
+ mainState = mainState,
+ onMainEvent = onMainEvent,
+ themeContrastTheme = themeContrastTheme
+ )
+ }
+
+ StartNavigationScreenItem(
+ state = state,
+ screen = StartNavigationScreen.PERMISSIONS_THIRD
+ ) {
+ startPermissionsScreen(
+ state = state,
+ onGrantStoragePermission = {
+ onEvent(
+ StartEvent.OnStoragePermissionRequest(
+ activity,
+ storagePermissionState
+ )
+ )
+ },
+ onGrantNotificationsPermission = {
+ onEvent(
+ StartEvent.OnNotificationsPermissionRequest(
+ activity,
+ notificationsPermissionState,
+ onEnableUpdates = {
+ onMainEvent(MainEvent.OnChangeCheckForUpdates(true))
+ }
+ )
+ )
+ }
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/permissions/StartPermissionItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/permissions/StartPermissionItem.kt
new file mode 100644
index 00000000..73f08c29
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/permissions/StartPermissionItem.kt
@@ -0,0 +1,93 @@
+package ua.acclorite.book_story.presentation.screens.start.components.permissions
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Star
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import ua.acclorite.book_story.R
+
+/**
+ * Permission item. Can be optional.
+ */
+@Composable
+fun StartPermissionItem(
+ title: String,
+ description: String,
+ isOptional: Boolean,
+ isGranted: Boolean,
+ onGrantClick: () -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .padding(horizontal = 18.dp, vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(Modifier.weight(1f), verticalArrangement = Arrangement.Center) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = title,
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ if (!isOptional) {
+ Spacer(modifier = Modifier.width(4.dp))
+ Icon(
+ imageVector = Icons.Default.Star,
+ contentDescription = stringResource(id = R.string.required_content_desc),
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(15.dp)
+ )
+ }
+ }
+ Text(
+ text = description,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ if (!isGranted) {
+ if (!isOptional) {
+ Button(
+ onClick = onGrantClick,
+ contentPadding = ButtonDefaults.TextButtonContentPadding
+ ) {
+ Text(text = stringResource(id = R.string.grant))
+ }
+ } else {
+ TextButton(onClick = onGrantClick) {
+ Text(text = stringResource(id = R.string.grant))
+ }
+ }
+ } else {
+ Text(
+ text = stringResource(id = R.string.start_permissions_granted),
+ color = MaterialTheme.colorScheme.primary.copy(0.7f),
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.padding(ButtonDefaults.TextButtonContentPadding)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/permissions/StartPermissionsScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/permissions/StartPermissionsScreen.kt
new file mode 100644
index 00000000..454e6e61
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/permissions/StartPermissionsScreen.kt
@@ -0,0 +1,60 @@
+package ua.acclorite.book_story.presentation.screens.start.components.permissions
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.State
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import ua.acclorite.book_story.R
+import ua.acclorite.book_story.presentation.components.CategoryTitle
+import ua.acclorite.book_story.presentation.screens.start.data.StartState
+
+/**
+ * Permissions settings.
+ */
+fun LazyListScope.startPermissionsScreen(
+ state: State,
+ onGrantStoragePermission: () -> Unit,
+ onGrantNotificationsPermission: () -> Unit
+) {
+ item {
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ item {
+ CategoryTitle(
+ modifier = Modifier,
+ title = stringResource(id = R.string.start_permissions_preferences),
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+
+ item {
+ StartPermissionItem(
+ title = stringResource(id = R.string.start_permissions_storage),
+ description = stringResource(id = R.string.start_permissions_storage_desc),
+ isOptional = false,
+ isGranted = state.value.storagePermissionGranted,
+ onGrantClick = onGrantStoragePermission
+ )
+ }
+
+ item {
+ StartPermissionItem(
+ title = stringResource(id = R.string.start_permissions_notifications),
+ description = stringResource(id = R.string.start_permissions_notifications_desc),
+ isOptional = true,
+ isGranted = state.value.notificationsPermissionGranted,
+ onGrantClick = onGrantNotificationsPermission
+ )
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartEvent.kt
new file mode 100644
index 00000000..852ba1e5
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartEvent.kt
@@ -0,0 +1,39 @@
+@file:OptIn(ExperimentalPermissionsApi::class)
+
+package ua.acclorite.book_story.presentation.screens.start.data
+
+import androidx.activity.ComponentActivity
+import androidx.compose.runtime.Immutable
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.PermissionState
+import ua.acclorite.book_story.presentation.data.Navigator
+
+@Immutable
+sealed class StartEvent {
+ data class OnGoBack(val onQuit: () -> Unit) : StartEvent()
+ data object OnGoForward : StartEvent()
+ data class OnStoragePermissionRequest(
+ val activity: ComponentActivity,
+ val legacyStoragePermissionState: PermissionState
+ ) : StartEvent()
+
+ data class OnNotificationsPermissionRequest(
+ val activity: ComponentActivity,
+ val notificationsPermissionState: PermissionState,
+ val onEnableUpdates: () -> Unit
+ ) : StartEvent()
+
+ data class OnGoToBrowse(val navigator: Navigator, val onCompletedStartGuide: () -> Unit) :
+ StartEvent()
+
+ data class OnGoToHelp(val navigator: Navigator) : StartEvent()
+
+ data object OnResetStartScreen : StartEvent()
+}
+
+
+
+
+
+
+
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartNavigationScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartNavigationScreen.kt
new file mode 100644
index 00000000..9bf3ffb9
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartNavigationScreen.kt
@@ -0,0 +1,10 @@
+package ua.acclorite.book_story.presentation.screens.start.data
+
+import androidx.compose.runtime.Immutable
+
+@Immutable
+enum class StartNavigationScreen {
+ LANGUAGE_FIRST,
+ APPEARANCE_SECOND,
+ PERMISSIONS_THIRD
+}
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartState.kt
new file mode 100644
index 00000000..a066b7fd
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartState.kt
@@ -0,0 +1,14 @@
+package ua.acclorite.book_story.presentation.screens.start.data
+
+import androidx.compose.runtime.Immutable
+
+@Immutable
+data class StartState(
+ val isDone: Boolean = false,
+
+ val currentScreen: StartNavigationScreen = StartNavigationScreen.entries.first(),
+ val useBackAnimation: Boolean = false,
+
+ val storagePermissionGranted: Boolean = false,
+ val notificationsPermissionGranted: Boolean = false,
+)
\ No newline at end of file
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartViewModel.kt
new file mode 100644
index 00000000..ae907eee
--- /dev/null
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartViewModel.kt
@@ -0,0 +1,305 @@
+package ua.acclorite.book_story.presentation.screens.start.data
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.Settings
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.PermissionState
+import com.google.accompanist.permissions.isGranted
+import com.google.accompanist.permissions.shouldShowRationale
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.yield
+import ua.acclorite.book_story.domain.use_case.CheckForUpdates
+import ua.acclorite.book_story.presentation.data.Argument
+import ua.acclorite.book_story.presentation.data.Screen
+import javax.inject.Inject
+
+@OptIn(ExperimentalPermissionsApi::class)
+@HiltViewModel
+class StartViewModel @Inject constructor(
+ private val checkForUpdates: CheckForUpdates,
+) : ViewModel() {
+
+ private val _state = MutableStateFlow(StartState())
+ val state = _state.asStateFlow()
+
+ private var storagePermissionJob: Job? = null
+ private var notificationsPermissionJob: Job? = null
+
+ fun onEvent(event: StartEvent) {
+ when (event) {
+ is StartEvent.OnGoBack -> {
+ storagePermissionJob?.cancel()
+ notificationsPermissionJob?.cancel()
+
+ if (_state.value.currentScreen == StartNavigationScreen.entries.first()) {
+ event.onQuit()
+ return
+ }
+
+ _state.update {
+ it.copy(
+ useBackAnimation = true
+ )
+ }
+
+ if (_state.value.isDone) {
+ _state.update {
+ it.copy(
+ isDone = false,
+ currentScreen = StartNavigationScreen.entries.last()
+ )
+ }
+ return
+ }
+
+ _state.update {
+ val previousScreenIndex = StartNavigationScreen.entries.indexOf(
+ it.currentScreen
+ ) - 1
+
+ if (previousScreenIndex == -1) {
+ return
+ }
+
+ it.copy(
+ currentScreen = StartNavigationScreen.entries[previousScreenIndex]
+ )
+ }
+ }
+
+ is StartEvent.OnGoForward -> {
+ storagePermissionJob?.cancel()
+ notificationsPermissionJob?.cancel()
+
+ _state.update {
+ it.copy(
+ useBackAnimation = false
+ )
+ }
+
+ if (_state.value.currentScreen == StartNavigationScreen.entries.last()) {
+ _state.update {
+ it.copy(
+ isDone = true
+ )
+ }
+ return
+ }
+
+ _state.update {
+ val nextScreenIndex = StartNavigationScreen.entries.indexOf(
+ it.currentScreen
+ ) + 1
+
+ if (nextScreenIndex == 0) {
+ return
+ }
+
+ it.copy(
+ currentScreen = StartNavigationScreen.entries[nextScreenIndex]
+ )
+ }
+ }
+
+ is StartEvent.OnStoragePermissionRequest -> {
+ val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
+
+ val isPermissionGranted = if (legacyStoragePermission) {
+ event.legacyStoragePermissionState.status.isGranted
+ } else {
+ Environment.isExternalStorageManager()
+ }
+
+ if (isPermissionGranted) {
+ _state.update {
+ it.copy(
+ storagePermissionGranted = true
+ )
+ }
+ return
+ }
+
+ if (legacyStoragePermission) {
+ if (event.legacyStoragePermissionState.status.shouldShowRationale) {
+ event.legacyStoragePermissionState.launchPermissionRequest()
+ } else {
+ val uri = Uri.parse("package:${event.activity.packageName}")
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
+
+ if (intent.resolveActivity(event.activity.packageManager) != null) {
+ event.activity.startActivity(intent)
+ } else {
+ return
+ }
+ }
+ }
+
+ if (!legacyStoragePermission) {
+ val uri = Uri.parse("package:${event.activity.packageName}")
+ val intent = Intent(
+ Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
+ uri
+ )
+
+ if (intent.resolveActivity(event.activity.packageManager) != null) {
+ event.activity.startActivity(intent)
+ } else {
+ return
+ }
+ }
+
+ storagePermissionJob?.cancel()
+ storagePermissionJob = viewModelScope.launch {
+ while (true) {
+ val granted = if (legacyStoragePermission) {
+ event.legacyStoragePermissionState.status.isGranted
+ } else {
+ Environment.isExternalStorageManager()
+ }
+
+ if (!granted) {
+ delay(1000)
+ yield()
+ continue
+ }
+
+ yield()
+
+ _state.update {
+ it.copy(
+ storagePermissionGranted = true
+ )
+ }
+ break
+ }
+ }
+ }
+
+ is StartEvent.OnNotificationsPermissionRequest -> {
+ if (event.notificationsPermissionState.status.isGranted) {
+ _state.update {
+ it.copy(
+ notificationsPermissionGranted = true
+ )
+ }
+ event.onEnableUpdates()
+ return
+ }
+
+ if (event.notificationsPermissionState.status.shouldShowRationale) {
+ event.notificationsPermissionState.launchPermissionRequest()
+ } else {
+ val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
+ intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName)
+
+ if (intent.resolveActivity(event.activity.packageManager) != null) {
+ event.activity.startActivity(intent)
+ } else {
+ return
+ }
+ }
+
+ notificationsPermissionJob?.cancel()
+ notificationsPermissionJob = viewModelScope.launch {
+ while (true) {
+ if (!event.notificationsPermissionState.status.isGranted) {
+ delay(1000)
+ yield()
+ continue
+ }
+
+ yield()
+
+ _state.update {
+ it.copy(
+ notificationsPermissionGranted = true
+ )
+ }
+
+ event.onEnableUpdates()
+ checkForUpdates.execute(
+ postNotification = true
+ )
+
+ break
+ }
+ }
+ }
+
+ is StartEvent.OnGoToBrowse -> {
+ event.navigator.navigateWithoutBackStack(Screen.BROWSE, false)
+ event.onCompletedStartGuide()
+ }
+
+ is StartEvent.OnGoToHelp -> {
+ event.navigator.putArgument(Argument("from_start", true))
+ event.navigator.navigateWithoutBackStack(Screen.HELP, false)
+ }
+
+ is StartEvent.OnResetStartScreen -> {
+ _state.update {
+ StartState()
+ }
+ }
+ }
+ }
+
+ fun checkPermissions(
+ storagePermissionState: PermissionState,
+ notificationPermissionState: PermissionState,
+ isCheckForUpdatesEnabled: Boolean,
+ onEnableCheckForUpdates: () -> Unit
+ ) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
+ val legacyNotificationPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
+
+ val storagePermissionGranted = if (!legacyStoragePermission) {
+ Environment.isExternalStorageManager()
+ } else {
+ storagePermissionState.status.isGranted
+ }
+ val notificationPermissionGranted = if (!legacyNotificationPermission) {
+ notificationPermissionState.status.isGranted
+ } else {
+ true
+ }
+
+ if (notificationPermissionGranted && !isCheckForUpdatesEnabled) {
+ onEnableCheckForUpdates()
+ checkForUpdates.execute(
+ postNotification = true
+ )
+ }
+
+ _state.update {
+ it.copy(
+ storagePermissionGranted = storagePermissionGranted,
+ notificationsPermissionGranted = notificationPermissionGranted
+ )
+ }
+ }
+ }
+}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt
index 2bbfea0c..a07f4a2a 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt
@@ -10,6 +10,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Shapes
+import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.SideEffect
@@ -109,7 +110,7 @@ fun BookStoryTheme(
MaterialTheme(
colorScheme = animatedColorScheme,
shapes = Shapes(),
- typography = Typography,
+ typography = Typography(),
content = content
)
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt
index 09bd24de..3cf42ec0 100644
--- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt
+++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt
@@ -1,22 +1,23 @@
package ua.acclorite.book_story.presentation.ui
-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.scaleIn
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
object Transitions {
val DefaultTransitionIn = fadeIn(tween(300))
val DefaultTransitionOut = fadeOut(tween(300))
- val FadeTransitionIn = fadeIn(tween(350))
- val FadeTransitionOut = fadeOut(tween(100))
+ val FadeTransitionIn = fadeIn(tween(300)) + scaleIn(tween(300), initialScale = 0.98f)
+ val FadeTransitionOut = fadeOut(tween(50))
val SlidingTransitionIn = fadeIn(tween(350)) +
slideInHorizontally(tween(350)) { it / 16 }
@@ -37,8 +38,8 @@ fun DefaultTransition(
modifier: Modifier = Modifier,
content: @Composable (() -> Unit)
) {
- AnimatedVisibility(
- visible,
+ CustomAnimatedVisibility(
+ visible = visible,
modifier = modifier,
enter = Transitions.DefaultTransitionIn,
exit = Transitions.DefaultTransitionOut
@@ -53,16 +54,15 @@ fun SlidingTransition(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
- AnimatedVisibility(
+ CustomAnimatedVisibility(
visible = visible,
modifier = modifier,
enter = slideInVertically(tween(300)) { -it / 10 } +
fadeIn(tween(300)),
exit = slideOutVertically(tween(150)) { -it / 10 } +
fadeOut(tween(100)),
- ) {
- content()
- }
+ content = content
+ )
}
diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Typography.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Typography.kt
deleted file mode 100644
index c8e919e8..00000000
--- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Typography.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package ua.acclorite.book_story.presentation.ui
-
-import androidx.compose.material3.Typography
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.sp
-
-val Typography = Typography(
- headlineSmall = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 24.sp,
- lineHeight = 32.sp
- ),
-
- titleLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 22.sp,
- lineHeight = 28.sp
- ),
- titleMedium = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Medium,
- fontSize = 16.sp,
- lineHeight = 20.sp
- ),
- titleSmall = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Medium,
- fontSize = 14.sp,
- lineHeight = 20.sp
- ),
-
- bodyLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 16.sp,
- lineHeight = 24.sp
- ),
- bodyMedium = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 14.sp,
- lineHeight = 20.sp
- ),
- bodySmall = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 12.sp,
- lineHeight = 16.sp
- ),
-
- labelLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Medium,
- fontSize = 14.sp,
- lineHeight = 20.sp
- ),
- labelMedium = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Medium,
- fontSize = 13.sp,
- lineHeight = 16.sp
- ),
-
- )
\ No newline at end of file
diff --git a/app/src/main/res/drawable-hdpi/notification_icon.png b/app/src/main/res/drawable-hdpi/notification_icon.png
new file mode 100644
index 00000000..9976ad2c
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/notification_icon.png differ
diff --git a/app/src/main/res/drawable-mdpi/notification_icon.png b/app/src/main/res/drawable-mdpi/notification_icon.png
new file mode 100644
index 00000000..7551dc02
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/notification_icon.png differ
diff --git a/app/src/main/res/drawable-xhdpi/notification_icon.png b/app/src/main/res/drawable-xhdpi/notification_icon.png
new file mode 100644
index 00000000..5ad52672
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/notification_icon.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/notification_icon.png b/app/src/main/res/drawable-xxhdpi/notification_icon.png
new file mode 100644
index 00000000..5988be78
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/notification_icon.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/notification_icon.png b/app/src/main/res/drawable-xxxhdpi/notification_icon.png
new file mode 100644
index 00000000..590b4ffb
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/notification_icon.png differ
diff --git a/app/src/main/res/drawable/checklist.png b/app/src/main/res/drawable/checklist.png
new file mode 100644
index 00000000..0f29eed7
Binary files /dev/null and b/app/src/main/res/drawable/checklist.png differ
diff --git a/app/src/main/res/mipmap-anydpi/app_icon.xml b/app/src/main/res/mipmap-anydpi/app_icon.xml
index e32ce33f..be2d22d2 100644
--- a/app/src/main/res/mipmap-anydpi/app_icon.xml
+++ b/app/src/main/res/mipmap-anydpi/app_icon.xml
@@ -1,6 +1,6 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/app_icon_round.xml b/app/src/main/res/mipmap-anydpi/app_icon_round.xml
index e32ce33f..be2d22d2 100644
--- a/app/src/main/res/mipmap-anydpi/app_icon_round.xml
+++ b/app/src/main/res/mipmap-anydpi/app_icon_round.xml
@@ -1,6 +1,6 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index b84a8b23..b9ea46ee 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -2,6 +2,17 @@
Історія Книги
+
+ Нові оновлення
+ Нове оновлення Історії Книги!
+
+ Оновлення %1$s вийшло!
+ Натиснувши на це сповіщення, ви завантажете останню версію застосунку.
+
+
+ Подробиці
+
+
- Укрліб|https://www.ukrlib.com.ua
@@ -53,12 +64,19 @@
Це видалить всю історію з бази даних.
+ Після видалення всієї історії порядок книг буде скинутий.
При підтвердженні ця книга буде оновлена.
Будь ласка, запам\'ятайте де ви зупинилися перед оновленням тексту.
Наступне буде оновлено:
+
+ Нова версія Історії Книги була знайдена.
+ Чи бажаєте завантажити її?
+ Натиснувши на кнопку «Завантажити», ви перейдете в ваш браузер
+ та завантажете останнє оновлення автоматично.
+
@@ -81,6 +99,10 @@
Вміст книги пустий. Будь ласка, перевірте вміст книги та оновіть її.
+
+ Щось пішло не так.
+ Будь ласка, перевірте ваше підключення до інтернету та спробуйте ще раз.
+
@@ -99,6 +121,7 @@
Знайти %1$s…
%1$s прочитано
Шлях до файлу: %1$s
+ Оновлення %1$s
Відмінити
@@ -121,6 +144,11 @@
Словник
Ще раз
Підтвердити
+ Далі
+ Так, перейти в Допомога
+ Ні
+ Завантажити
+ Готово
файли
@@ -176,6 +204,8 @@
Абзацний відступ
Колір фона
Колір шрифта
+ Перевіряти на оновлення
+ Перевіряти на новішу версію застосунку при запуску
Червоний
@@ -223,6 +253,7 @@
Ця книга була успішно переміщена.
Скопійовано
Історія була успішно видалена.
+ Остання версія застосунку вже встановлена.
Елемент історії був успішно видалений.
@@ -253,17 +284,11 @@
Версія застосунку
- Версія %1$s
- Всі релізи
- Перевірити на нову версію
- Знайшли помилку?
- Розкажіть мені!
+ Версія v%1$s
+ Натисніть, щоб перевірити на оновлення
+ Повідомити про помилку
Сторінка проєкту
- Головна сторінка застосунку
- Сподобався застосунок?
- Перевірте мій профіль!
- Хочете підтримати мою роботу?
- Купіть мені каву ♥
+ Підтримати мою роботу
Слава Україні! 🇺🇦
Героям Слава! 🐉
@@ -368,6 +393,36 @@
Якщо файл книги змінився, ви можете підтвердити і оновити книгу або скасувати оновлення.
+ Як керувати історією?
+ Перейдіть в
+ Історію
+
+ Якщо натиснути кнопку видалення праворуч від елемента історії, його буде видалено.
+ Історія впливає на порядок книг в Бібліотеці,
+ видалення елемента історії призведе до зміни порядку книг.
+ Остання відкрита книга завжди є першою в Бібліотеці та Історії.
+
+
+
+
+ Вітаємо в Історія Книги!
+
+ У світ, де слова чарують, вчать та ведуть у неймовірні пригоди!
+ Давайте дещо налаштуємо!
+
+ Налаштування мови
+ Налаштування теми
+ Дозволи
+ Надано
+ Дозвіл на сховище
+ Потрібний, щоб просканувати ваш пристрій та знайти книги
+ Дозвіл на сповіщення
+ Потрібний, щоб перевірити застосунок на нову версію
+ Все готово!
+
+ Ви успішно закінчили базове налаштовування застосунку.
+ Бажаєте дізнатися, як користуватися застосунком, прочитавши екран Допомоги?
+
За замовчуванням
@@ -406,6 +461,9 @@
Додавайте книги
Стрілка
Анімація стрілки
+ Необхідно
+ Скинути початковий посібник
+ Вийти з редагування назви
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 46fb4808..02bef385 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,9 +1,23 @@
- 0.9.7
+ 0.9.8
Book\'s Story
+
+ New updates
+ New update of Book\'s Story!
+
+ Update %1$s just released!
+ By clicking this notification you will download the latest version of the app.
+
+
+ Details
+
+
+
+ https://github.com/Acclorite/book-story/releases/latest/download/book-story.apk
+
https://github.com/Acclorite/book-story/releases/latest
@@ -13,9 +27,6 @@
https://github.com/Acclorite/book-story
-
- https://github.com/Acclorite
-
https://www.buymeacoffee.com/acclorite
@@ -73,12 +84,19 @@
It will delete the whole history from the database.
+ After deleting the whole history, the order of the books will be reset.
By confirming this book will be updated.
Please remember where you left before updating text.
Following will be updated:
+
+ New version of Book\'s Story was found.
+ Do you want to download it?
+ By clicking the «Download» button you will be transferred
+ to your browser and download the latest update automatically.
+
@@ -101,6 +119,10 @@
The book content is empty. Please, make sure that the book content is not empty and update it.
+
+ Something went wrong.
+ Please check your internet connection and try again.
+
@@ -118,6 +140,7 @@
Search %1$s…
%1$s read
File path: %1$s
+ Update %1$s
Cancel
@@ -140,6 +163,11 @@
Dictionary
Retry
Confirm
+ Next
+ Yes, go to Help
+ No
+ Download
+ Done
files
@@ -195,6 +223,8 @@
Paragraph indentation
Background color
Font color
+ Check for updates
+ Check for newer app version on startup
Red
@@ -242,6 +272,7 @@
This book was successfully moved.
Copied
History was successfully deleted.
+ The latest version of the app is already installed.
History element was successfully deleted.
@@ -272,17 +303,11 @@
App version
- Version %1$s
- All releases
- Check for newer version
- Found a bug?
- Tell me!
+ Version v%1$s
+ Click to check for updates
+ Report a bug
Project\'s page
- General app\'s page
- Like the app?
- Check my profile!
- Want to support my work?
- Buy me a coffee ♥
+ Support my work
Slava Ukraini! 🇺🇦
Heroyam Slava! 🐉
@@ -387,6 +412,38 @@
If book\'s file changed you may confirm and update the book or cancel update.
+ How do I manage history?
+ Go to the
+ History
+
+ By clicking the delete button to the right of the history element, it will be deleted.
+ History affects books order in Library,
+ by deleting history element it will also reorder a book.
+ Last opened book is always the first in Library and History.
+
+
+
+
+ Welcome to Book\'s Story!
+
+ To a world where words charm,
+ teach and lead on incredible adventures!
+ Let\'s set some things up!
+
+ Language preferences
+ Theme preferences
+ Permissions
+ Granted
+ Storage permission
+ Required to scan your device and find books
+ Notifications permission
+ Required to check for newer app version
+ Everything\'s done!
+
+ You\'re successfully done basic setting of the app.
+ Would you like to learn how to use the app by reading Help screen?
+
+
Default
@@ -424,5 +481,8 @@
Add books
Arrow
Arrow animation
+ Required
+ Reset start guide
+ Exit title editing
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 0778eea2..b4e9c200 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,4 +3,5 @@ plugins {
id("org.jetbrains.kotlin.android") version "1.9.21" apply false
id("com.google.devtools.ksp") version "1.9.21-1.0.16" apply false
id("com.google.dagger.hilt.android") version "2.50" apply false
+ id("com.mikepenz.aboutlibraries.plugin") version "11.1.3" apply false
}
\ No newline at end of file