0.9.8 - StartScreen + Check for updates + Bug fixes + Lot more...

This commit is contained in:
acclorite 2024-04-20 20:33:35 +03:00
parent 513b286e07
commit b5c2399034
129 changed files with 3944 additions and 1535 deletions

View file

@ -3,30 +3,39 @@
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
</profile> </profile>
</component> </component>

View file

@ -4,6 +4,7 @@ plugins {
id("com.google.devtools.ksp") id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android") id("com.google.dagger.hilt.android")
id("kotlin-parcelize") id("kotlin-parcelize")
id("com.mikepenz.aboutlibraries.plugin")
} }
android { android {
@ -15,7 +16,7 @@ android {
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "0.9.7" versionName = "0.9.8"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@ -61,16 +62,15 @@ android {
dependencies { dependencies {
// Default // 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.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 // 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.foundation:foundation")
implementation("androidx.compose.animation:animation") implementation("androidx.compose.animation:animation")
implementation("androidx.compose.animation:animation-core") implementation("androidx.compose.animation:animation-android")
implementation("androidx.compose.animation:animation-graphics")
implementation("androidx.compose.foundation:foundation-layout") implementation("androidx.compose.foundation:foundation-layout")
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-graphics")
@ -81,35 +81,13 @@ dependencies {
implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material:material") 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 // All dependencies
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha") implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
// Dagger - Hilt // Dagger - Hilt
implementation("com.google.dagger:hilt-android:2.51.1") 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") ksp("com.google.dagger:hilt-android-compiler:2.51")
implementation("com.google.dagger:hilt-compiler:latest.release") implementation("com.google.dagger:hilt-compiler:latest.release")
ksp("androidx.hilt:hilt-compiler:latest.release") ksp("androidx.hilt:hilt-compiler:latest.release")
@ -148,4 +126,16 @@ dependencies {
// Coil for loading bitmaps // Coil for loading bitmaps
implementation("io.coil-kt:coil-compose:2.6.0") 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")
} }

View file

@ -2,11 +2,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<!-- Check device for books -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission <uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" /> tools:ignore="ScopedStorage" />
<!-- Send notification about new app version -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<queries> <queries>
<!-- Translator --> <!-- Translator -->

View file

@ -7,30 +7,15 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.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.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.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.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager 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.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import dagger.hilt.android.AndroidEntryPoint 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.MainViewModel
import ua.acclorite.book_story.presentation.data.NavigationHost import ua.acclorite.book_story.presentation.data.NavigationHost
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.about.AboutScreen import ua.acclorite.book_story.presentation.screens.about.AboutScreenRoot
import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreen 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.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.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.help.HelpScreen import ua.acclorite.book_story.presentation.screens.help.HelpScreenRoot
import ua.acclorite.book_story.presentation.screens.history.HistoryScreen 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.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.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.reader.data.ReaderViewModel
import ua.acclorite.book_story.presentation.screens.settings.SettingsScreen import ua.acclorite.book_story.presentation.screens.settings.SettingsScreenRoot
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettings import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettingsRoot
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettings 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.BookStoryTheme
import ua.acclorite.book_story.presentation.ui.Transitions import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.presentation.ui.isDark 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 import java.lang.reflect.Field
@Suppress("unused")
@SuppressLint("DiscouragedPrivateApi") @SuppressLint("DiscouragedPrivateApi")
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@AndroidEntryPoint @AndroidEntryPoint
class Activity : AppCompatActivity() { class Activity : AppCompatActivity() {
// Initializing all viewModels on app startup
private val mainViewModel: MainViewModel by viewModels() private val mainViewModel: MainViewModel by viewModels()
private val libraryViewModel: LibraryViewModel by viewModels() private val libraryViewModel: LibraryViewModel by viewModels()
private val historyViewModel: HistoryViewModel by viewModels() private val historyViewModel: HistoryViewModel by viewModels()
private val browseViewModel: BrowseViewModel by viewModels() private val browseViewModel: BrowseViewModel by viewModels()
private val bookInfoViewModel: BookInfoViewModel by viewModels() private val bookInfoViewModel: BookInfoViewModel by viewModels()
private val readerViewModel: ReaderViewModel 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -103,19 +98,6 @@ class Activity : AppCompatActivity() {
val isLoaded by mainViewModel.isReady.collectAsState() val isLoaded by mainViewModel.isReady.collectAsState()
val state by mainViewModel.state.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 density = LocalDensity.current
val imeInsets = WindowInsets.ime val imeInsets = WindowInsets.ime
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
@ -124,6 +106,7 @@ class Activity : AppCompatActivity() {
imeInsets.getBottom(density) > 0 imeInsets.getBottom(density) > 0
} }
} }
LaunchedEffect(isKeyboardVisible) { LaunchedEffect(isKeyboardVisible) {
if (!isKeyboardVisible) { if (!isKeyboardVisible) {
focusManager.clearFocus() focusManager.clearFocus()
@ -137,175 +120,90 @@ class Activity : AppCompatActivity() {
isPureDark = state.pureDark!!.isPureDark(this), isPureDark = state.pureDark!!.isPureDark(this),
themeContrast = state.themeContrast!! themeContrast = state.themeContrast!!
) { ) {
NavigationHost(startScreen = Screen.LIBRARY) { NavigationHost(
val currentScreen by this.getCurrentScreen().collectAsState() startScreen = if (state.showStartScreen!!) Screen.START
else Screen.LIBRARY
AnimatedVisibility(
visible = currentScreen == Screen.LIBRARY ||
currentScreen == Screen.HISTORY ||
currentScreen == Screen.BROWSE,
enter = Transitions.BackSlidingTransitionIn,
exit = Transitions.SlidingTransitionOut
) { ) {
Scaffold( navigation(
screens = arrayOf(Screen.LIBRARY, Screen.HISTORY, Screen.BROWSE),
bottomBar = { bottomBar = {
if (!tabletUI) { BottomNavigationBar()
BottomNavigationBar(navigator = this@NavigationHost)
}
}, },
containerColor = MaterialTheme.colorScheme.surface navigationRail = {
CustomNavigationRail()
}
) { ) {
Box( // Library
modifier = Modifier composable(
.fillMaxSize() screen = Screen.LIBRARY,
enterAnim = Transitions.FadeTransitionIn,
exitAnim = Transitions.FadeTransitionOut
) { ) {
Box( LibraryScreenRoot()
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
)
} }
composable(screen = Screen.HISTORY) { // History
HistoryScreen( composable(
viewModel = historyViewModel, screen = Screen.HISTORY,
libraryViewModel = libraryViewModel, enterAnim = Transitions.FadeTransitionIn,
navigator = this@NavigationHost exitAnim = Transitions.FadeTransitionOut
) ) {
HistoryScreenRoot()
} }
composable(screen = Screen.BROWSE) { // Browse
BrowseScreen( composable(
viewModel = browseViewModel, screen = Screen.BROWSE,
libraryViewModel = libraryViewModel, enterAnim = Transitions.FadeTransitionIn,
navigator = this@NavigationHost exitAnim = Transitions.FadeTransitionOut
) ) {
} BrowseScreenRoot()
}
if (tabletUI) {
CustomNavigationRail(
navigator = this@NavigationHost,
layoutDirection = layoutDirection
)
}
}
} }
} }
// Book Info // Book Info
composable( composable(screen = Screen.BOOK_INFO) {
screen = Screen.BOOK_INFO, BookInfoScreenRoot()
enterAnim = Transitions.SlidingTransitionIn,
exitAnim = Transitions.SlidingTransitionOut
) {
BookInfoScreen(
viewModel = bookInfoViewModel,
libraryViewModel = libraryViewModel,
browseViewModel = browseViewModel,
historyViewModel = historyViewModel,
navigator = this@NavigationHost
)
} }
composable(
screen = Screen.READER, // Reader
enterAnim = Transitions.SlidingTransitionIn, composable(screen = Screen.READER) {
exitAnim = Transitions.SlidingTransitionOut ReaderScreenRoot()
) {
ReaderScreen(
viewModel = readerViewModel,
mainViewModel = mainViewModel,
libraryViewModel = libraryViewModel,
historyViewModel = historyViewModel,
navigator = this@NavigationHost
)
} }
// Settings // Settings
composable( composable(screen = Screen.SETTINGS) {
screen = Screen.SETTINGS, SettingsScreenRoot()
enterAnim = Transitions.SlidingTransitionIn,
exitAnim = Transitions.SlidingTransitionOut
) {
SettingsScreen(
navigator = this@NavigationHost
)
} }
// Nested categories // Nested settings categories
composable( composable(screen = Screen.GENERAL_SETTINGS) {
screen = Screen.GENERAL_SETTINGS, GeneralSettingsRoot()
enterAnim = Transitions.SlidingTransitionIn,
exitAnim = Transitions.SlidingTransitionOut
) {
GeneralSettings(
mainViewModel = mainViewModel,
navigator = this@NavigationHost
)
} }
composable( composable(screen = Screen.APPEARANCE_SETTINGS) {
screen = Screen.APPEARANCE_SETTINGS, AppearanceSettingsRoot()
enterAnim = Transitions.SlidingTransitionIn,
exitAnim = Transitions.SlidingTransitionOut
) {
AppearanceSettings(
mainViewModel = mainViewModel,
navigator = this@NavigationHost
)
} }
composable( composable(screen = Screen.READER_SETTINGS) {
screen = Screen.READER_SETTINGS, ReaderSettingsRoot()
enterAnim = Transitions.SlidingTransitionIn,
exitAnim = Transitions.SlidingTransitionOut
) {
ReaderSettings(
mainViewModel = mainViewModel,
navigator = this@NavigationHost
)
} }
// About screen // About screen
composable( composable(screen = Screen.ABOUT) {
screen = Screen.ABOUT, AboutScreenRoot()
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)
} }
// Start screen (later) // Help screen
// composable(screen = Screen.START) { composable(screen = Screen.HELP) {
// StartScreen() HelpScreenRoot()
// } }
// Start screen
composable(
screen = Screen.START,
enterAnim = Transitions.FadeTransitionIn
) {
StartScreenRoot()
}
} }
} }
} }

View file

@ -1,7 +1,36 @@
package ua.acclorite.book_story package ua.acclorite.book_story
import android.app.Application import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import ua.acclorite.book_story.data.local.notification.UpdatesNotificationService
@HiltAndroidApp @HiltAndroidApp
class Application : Application() 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)
}
}

View file

@ -2,18 +2,35 @@ package ua.acclorite.book_story.data.di
import android.app.Application import android.app.Application
import androidx.room.Room import androidx.room.Room
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent 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.BookDao
import ua.acclorite.book_story.data.local.room.BookDatabase import ua.acclorite.book_story.data.local.room.BookDatabase
import ua.acclorite.book_story.data.remote.GithubAPI
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object AppModule { 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 @Provides
@Singleton @Singleton

View file

@ -6,6 +6,8 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import ua.acclorite.book_story.data.local.data_store.DataStore import ua.acclorite.book_story.data.local.data_store.DataStore
import ua.acclorite.book_story.data.local.data_store.DataStoreImpl import ua.acclorite.book_story.data.local.data_store.DataStoreImpl
import ua.acclorite.book_story.data.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.BookMapper
import ua.acclorite.book_story.data.mapper.book.BookMapperImpl import ua.acclorite.book_story.data.mapper.book.BookMapperImpl
import ua.acclorite.book_story.data.mapper.history.HistoryMapper import ua.acclorite.book_story.data.mapper.history.HistoryMapper
@ -56,4 +58,10 @@ abstract class RepositoryModule {
abstract fun bindTextParser( abstract fun bindTextParser(
textParserImpl: TextParserImpl textParserImpl: TextParserImpl
): TextParser ): TextParser
@Binds
@Singleton
abstract fun bindNotificationService(
updatesNotificationServiceImpl: UpdatesNotificationServiceImpl
): UpdatesNotificationService
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -17,11 +17,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.local.data_store.DataStore import ua.acclorite.book_story.data.local.data_store.DataStore
import ua.acclorite.book_story.data.local.notification.UpdatesNotificationService
import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.mapper.book.BookMapper import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.mapper.history.HistoryMapper import ua.acclorite.book_story.data.mapper.history.HistoryMapper
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.data.parser.TextParser 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.Book
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
@ -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.CoverImage
import ua.acclorite.book_story.domain.util.Resource import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText 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.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.UUID import java.util.UUID
@ -44,6 +47,9 @@ class BookRepositoryImpl @Inject constructor(
private val database: BookDao, private val database: BookDao,
private val dataStore: DataStore, private val dataStore: DataStore,
private val githubAPI: GithubAPI,
private val updatesNotificationService: UpdatesNotificationService,
private val bookMapper: BookMapper, private val bookMapper: BookMapper,
private val historyMapper: HistoryMapper, private val historyMapper: HistoryMapper,
@ -385,8 +391,8 @@ class BookRepositoryImpl @Inject constructor(
dataStore.putData(key, value) dataStore.putData(key, value)
} }
override suspend fun getAllSettings(scope: CoroutineScope): MainState { override suspend fun getAllSettings(scope: CoroutineScope): MainSettingsState {
val result = CompletableDeferred<MainState>() val result = CompletableDeferred<MainSettingsState>()
scope.launch { scope.launch {
val keys = dataStore.getAllData() val keys = dataStore.getAllData()
@ -405,7 +411,7 @@ class BookRepositoryImpl @Inject constructor(
} }
jobs?.awaitAll() jobs?.awaitAll()
result.complete(MainState.initialize(data)) result.complete(MainSettingsState.initialize(data))
} }
return result.await() return result.await()
@ -601,6 +607,28 @@ class BookRepositoryImpl @Inject constructor(
override suspend fun deleteHistory(history: List<History>) { override suspend fun deleteHistory(history: List<History>) {
database.deleteHistory(history.map { historyMapper.toHistoryEntity(it) }) 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
}
}
}
} }

View file

@ -3,13 +3,14 @@ package ua.acclorite.book_story.domain.repository
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow 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.Book
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.Resource 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 import java.io.File
interface BookRepository { interface BookRepository {
@ -57,7 +58,7 @@ interface BookRepository {
value: T value: T
) )
suspend fun getAllSettings(scope: CoroutineScope): MainState suspend fun getAllSettings(scope: CoroutineScope): MainSettingsState
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>> suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
@ -76,4 +77,6 @@ interface BookRepository {
suspend fun deleteHistory( suspend fun deleteHistory(
history: List<History> history: List<History>
) )
suspend fun checkForUpdates(postNotification: Boolean): ReleaseResponse?
} }

View file

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

View file

@ -2,14 +2,14 @@ package ua.acclorite.book_story.domain.use_case
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import ua.acclorite.book_story.domain.repository.BookRepository 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 import javax.inject.Inject
class GetAllSettings @Inject constructor( class GetAllSettings @Inject constructor(
private val repository: BookRepository private val repository: BookRepository
) { ) {
suspend fun execute(scope: CoroutineScope): MainState { suspend fun execute(scope: CoroutineScope): MainSettingsState {
return repository.getAllSettings(scope) return repository.getAllSettings(scope)
} }
} }

View file

@ -20,4 +20,5 @@ object DataStoreConstants {
val LINE_HEIGHT = intPreferencesKey("line_height") val LINE_HEIGHT = intPreferencesKey("line_height")
val PARAGRAPH_HEIGHT = intPreferencesKey("paragraph_height") val PARAGRAPH_HEIGHT = intPreferencesKey("paragraph_height")
val PARAGRAPH_INDENTATION = booleanPreferencesKey("paragraph_indentation") val PARAGRAPH_INDENTATION = booleanPreferencesKey("paragraph_indentation")
val CHECK_FOR_UPDATES = booleanPreferencesKey("check_for_updates")
} }

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.components package ua.acclorite.book_story.presentation.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
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) label = stringResource(id = R.string.top_app_bar_anim_content_desc)
) )
AnimatedVisibility( CustomAnimatedVisibility(
visible = content1Visibility ?: true, visible = content1Visibility ?: true,
enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)), enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)),
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)) exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
@ -115,7 +114,7 @@ fun AnimatedTopAppBar(
} }
if (content2Visibility != null) { if (content2Visibility != null) {
AnimatedVisibility( CustomAnimatedVisibility(
visible = content2Visibility, visible = content2Visibility,
enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)), enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)),
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)) exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
@ -136,7 +135,7 @@ fun AnimatedTopAppBar(
} }
if (content3Visibility != null) { if (content3Visibility != null) {
AnimatedVisibility( CustomAnimatedVisibility(
visible = content3Visibility, visible = content3Visibility,
enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)), enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)),
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)) exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))

View file

@ -14,9 +14,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
/**
* Collapsible Scroll Behavior with Lazy List State.
*/
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState( fun TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState(
): Pair<TopAppBarScrollBehavior, LazyListState> { ): Pair<TopAppBarScrollBehavior, LazyListState> {
val listState = rememberLazyListState() val listState = rememberLazyListState()

View file

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

View file

@ -10,6 +10,9 @@ import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
/**
* Custom Cover Image. Has smooth appearing.
*/
@Composable @Composable
fun CustomCoverImage( fun CustomCoverImage(
uri: Uri, uri: Uri,

View file

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

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.components package ua.acclorite.book_story.presentation.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -82,7 +81,7 @@ fun CustomSnackbar(modifier: Modifier = Modifier, snackbarState: SnackbarHostSta
showSnackbar = true showSnackbar = true
} }
AnimatedVisibility( CustomAnimatedVisibility(
visible = showSnackbar, visible = showSnackbar,
enter = slideInVertically(tween(350)) { it } + fadeIn(tween(350)), enter = slideInVertically(tween(350)) { it } + fadeIn(tween(350)),
exit = slideOutVertically(tween(200)) { it } + fadeOut(tween(100)) exit = slideOutVertically(tween(200)) { it } + fadeOut(tween(100))

View file

@ -10,6 +10,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
/**
* Custom Tooltip.
*/
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun CustomTooltip(text: String, padding: Dp = 14.dp, content: @Composable () -> Unit) { fun CustomTooltip(text: String, padding: Dp = 14.dp, content: @Composable () -> Unit) {

View file

@ -17,14 +17,15 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.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.Screen
/** /**
* More drop down. Navigates to Settings, About and Help screens. * More drop down. Navigates to Settings, About and Help screens.
*/ */
@Composable @Composable
fun MoreDropDown(navigator: Navigator) { fun MoreDropDown() {
val navigator = LocalNavigator.current
var showDropDown by remember { mutableStateOf(false) } var showDropDown by remember { mutableStateOf(false) }
val startPadding = remember { 12.dp } val startPadding = remember { 12.dp }

View file

@ -11,19 +11,17 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NavigationItem 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 import ua.acclorite.book_story.presentation.data.Screen
/** /**
* Bottom navigation bar, uses default [NavigationBar]. * Bottom navigation bar, uses default [NavigationBar].
* *
* @param navigator Navigator.
*/ */
@Composable @Composable
fun BottomNavigationBar( fun BottomNavigationBar() {
navigator: Navigator
) {
var currentScreen: Screen? by remember { mutableStateOf(null) } var currentScreen: Screen? by remember { mutableStateOf(null) }
val navigator = LocalNavigator.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
navigator.getCurrentScreen().collect { navigator.getCurrentScreen().collect {

View file

@ -47,7 +47,8 @@ import ua.acclorite.book_story.R
* @param title The title of the dialog. * @param title The title of the dialog.
* @param description The description of the dialog. * @param description The description of the dialog.
* @param actionText The action text 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 onDismiss Code to execute when the dialog is dismissed.
* @param onAction Code to execute when action is clicked. * @param onAction Code to execute when action is clicked.
* @param withDivider Should divider be shown or not. * @param withDivider Should divider be shown or not.
@ -63,6 +64,7 @@ fun CustomDialogWithContent(
title: String, title: String,
description: String?, description: String?,
actionText: String?, actionText: String?,
disableOnClick: Boolean = true,
isActionEnabled: Boolean?, isActionEnabled: Boolean?,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onAction: () -> Unit, onAction: () -> Unit,
@ -158,7 +160,9 @@ fun CustomDialogWithContent(
) { ) {
TextButton( TextButton(
onClick = { onClick = {
if (disableOnClick) {
actionClicked = true actionClicked = true
}
onDismiss() onDismiss()
}, },
enabled = !actionClicked enabled = !actionClicked
@ -173,7 +177,9 @@ fun CustomDialogWithContent(
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
TextButton( TextButton(
onClick = { onClick = {
if (disableOnClick) {
actionClicked = true actionClicked = true
}
onAction() onAction()
}, },
enabled = isActionEnabled == true && !actionClicked enabled = isActionEnabled == true && !actionClicked

View file

@ -49,7 +49,8 @@ import ua.acclorite.book_story.R
* @param title The title of the dialog. * @param title The title of the dialog.
* @param description The description of the dialog. * @param description The description of the dialog.
* @param actionText The action text 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 onDismiss Code to execute when the dialog is dismissed.
* @param onAction Code to execute when action is clicked. * @param onAction Code to execute when action is clicked.
* @param withDivider Should divider be shown or not. * @param withDivider Should divider be shown or not.
@ -65,6 +66,7 @@ fun CustomDialogWithLazyColumn(
title: String, title: String,
description: String?, description: String?,
actionText: String?, actionText: String?,
disableOnClick: Boolean = true,
isActionEnabled: Boolean?, isActionEnabled: Boolean?,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onAction: () -> Unit, onAction: () -> Unit,
@ -162,7 +164,9 @@ fun CustomDialogWithLazyColumn(
) { ) {
TextButton( TextButton(
onClick = { onClick = {
if (disableOnClick) {
actionClicked = true actionClicked = true
}
onDismiss() onDismiss()
}, },
enabled = !actionClicked enabled = !actionClicked
@ -177,7 +181,9 @@ fun CustomDialogWithLazyColumn(
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
TextButton( TextButton(
onClick = { onClick = {
if (disableOnClick) {
actionClicked = true actionClicked = true
}
onAction() onAction()
}, },
enabled = isActionEnabled == true && !actionClicked enabled = isActionEnabled == true && !actionClicked

View file

@ -15,18 +15,24 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
/** /**
* Selectable dialog item. Perfectly combines with [CustomDialogWithLazyColumn]. * Selectable dialog item. Perfectly combines with [CustomDialogWithLazyColumn].
*/ */
@Composable @Composable
fun SelectableDialogItem(selected: Boolean, title: String, onClick: () -> Unit) { fun SelectableDialogItem(
selected: Boolean,
title: String,
horizontalPadding: Dp = 24.dp,
onClick: () -> Unit
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(enabled = !selected) { onClick() } .clickable(enabled = !selected) { onClick() }
.padding(horizontal = 24.dp, vertical = 12.dp), .padding(horizontal = horizontalPadding, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
RadioButton( RadioButton(

View file

@ -21,24 +21,23 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NavigationItem 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 import ua.acclorite.book_story.presentation.data.Screen
/** /**
* Custom Navigation Rail. It is used to be shown on Tablets. * Custom Navigation Rail. It is used to be shown on Tablets.
*/ */
@Composable @Composable
fun BoxScope.CustomNavigationRail( fun BoxScope.CustomNavigationRail() {
navigator: Navigator,
layoutDirection: LayoutDirection
) {
var currentScreen: Screen? by remember { mutableStateOf(null) } var currentScreen: Screen? by remember { mutableStateOf(null) }
val navigator = LocalNavigator.current
val layoutDirection = LocalLayoutDirection.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
navigator.getCurrentScreen().collect { navigator.getCurrentScreen().collect {

View file

@ -15,4 +15,5 @@ sealed class MainEvent {
data class OnChangeBackgroundColor(val color: Long) : MainEvent() data class OnChangeBackgroundColor(val color: Long) : MainEvent()
data class OnChangeFontColor(val color: Long) : MainEvent() data class OnChangeFontColor(val color: Long) : MainEvent()
data class OnChangeShowStartScreen(val bool: Boolean) : MainEvent() data class OnChangeShowStartScreen(val bool: Boolean) : MainEvent()
data class OnChangeCheckForUpdates(val bool: Boolean) : MainEvent()
} }

View file

@ -21,7 +21,7 @@ import java.util.Locale
* Main State. All app's settings are here. Wrapped in SavedStateHandle, so it won't reset. * Main State. All app's settings are here. Wrapped in SavedStateHandle, so it won't reset.
*/ */
@Immutable @Immutable
data class MainState( data class MainSettingsState(
val language: String? = null, val language: String? = null,
val theme: Theme? = null, val theme: Theme? = null,
val darkTheme: DarkTheme? = null, val darkTheme: DarkTheme? = null,
@ -36,6 +36,7 @@ data class MainState(
val backgroundColor: Long? = null, val backgroundColor: Long? = null,
val fontColor: Long? = null, val fontColor: Long? = null,
val showStartScreen: Boolean? = null, val showStartScreen: Boolean? = null,
val checkForUpdates: Boolean? = null,
) : Parcelable { ) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
// String // String
@ -68,6 +69,9 @@ data class MainState(
// Boolean // Boolean
showStartScreen = if (parcel.readByte().toInt() != 0) parcel.readByte() showStartScreen = if (parcel.readByte().toInt() != 0) parcel.readByte()
.toInt() == 1 else null, .toInt() == 1 else null,
// Boolean
checkForUpdates = if (parcel.readByte().toInt() != 0) parcel.readByte()
.toInt() == 1 else null,
) )
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
@ -107,25 +111,28 @@ data class MainState(
// Show Start Screen // Show Start Screen
parcel.writeByte((if (showStartScreen != null) 1 else 0).toByte()) parcel.writeByte((if (showStartScreen != null) 1 else 0).toByte())
showStartScreen?.let { parcel.writeByte(if (it) 1 else 0) } 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 { override fun describeContents(): Int {
return 0 return 0
} }
companion object CREATOR : Parcelable.Creator<MainState> { companion object CREATOR : Parcelable.Creator<MainSettingsState> {
override fun createFromParcel(parcel: Parcel): MainState { override fun createFromParcel(parcel: Parcel): MainSettingsState {
return MainState(parcel) return MainSettingsState(parcel)
} }
override fun newArray(size: Int): Array<MainState?> { override fun newArray(size: Int): Array<MainSettingsState?> {
return arrayOfNulls(size) return arrayOfNulls(size)
} }
/** /**
* Initializes [MainState] by given [Map]. * Initializes [MainSettingsState] by given [Map].
*/ */
fun initialize(data: Map<String, Any>): MainState { fun initialize(data: Map<String, Any>): MainSettingsState {
DataStoreConstants.apply { DataStoreConstants.apply {
val language: String = data[LANGUAGE.name] as? String ?: if ( 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 val paragraphIndentation: Boolean = data[PARAGRAPH_INDENTATION.name] as? Boolean
?: false ?: false
val checkForUpdates: Boolean = data[CHECK_FOR_UPDATES.name] as? Boolean
?: false
return MainState( return MainSettingsState(
language = language, language = language,
theme = theme.toTheme(), theme = theme.toTheme(),
darkTheme = darkTheme.toDarkTheme(), darkTheme = darkTheme.toDarkTheme(),
@ -191,7 +200,8 @@ data class MainState(
fontSize = fontSize, fontSize = fontSize,
lineHeight = lineHeight, lineHeight = lineHeight,
paragraphHeight = paragraphHeight, paragraphHeight = paragraphHeight,
paragraphIndentation = paragraphIndentation paragraphIndentation = paragraphIndentation,
checkForUpdates = checkForUpdates
) )
} }
} }

View file

@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ua.acclorite.book_story.domain.use_case.ChangeLanguage import ua.acclorite.book_story.domain.use_case.ChangeLanguage
import ua.acclorite.book_story.domain.use_case.CheckForUpdates
import ua.acclorite.book_story.domain.use_case.GetAllSettings import ua.acclorite.book_story.domain.use_case.GetAllSettings
import ua.acclorite.book_story.domain.use_case.SetDatastore import ua.acclorite.book_story.domain.use_case.SetDatastore
import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.Constants
@ -33,6 +34,7 @@ class MainViewModel @Inject constructor(
private val setDatastore: SetDatastore, private val setDatastore: SetDatastore,
private val changeLanguage: ChangeLanguage, private val changeLanguage: ChangeLanguage,
private val checkForUpdates: CheckForUpdates,
private val getAllSettings: GetAllSettings private val getAllSettings: GetAllSettings
) : ViewModel() { ) : ViewModel() {
@ -42,8 +44,8 @@ class MainViewModel @Inject constructor(
private val isSettingsReady = MutableStateFlow(false) private val isSettingsReady = MutableStateFlow(false)
private val isViewModelReady = MutableStateFlow(false) private val isViewModelReady = MutableStateFlow(false)
private val _state: MutableStateFlow<MainState> = MutableStateFlow( private val _state: MutableStateFlow<MainSettingsState> = MutableStateFlow(
stateHandle[Constants.MAIN_STATE] ?: MainState() stateHandle[Constants.MAIN_STATE] ?: MainSettingsState()
) )
val state = _state.asStateFlow() 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( fun init(
libraryViewModel: LibraryViewModel, libraryViewModel: LibraryViewModel
) { ) {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
val settings = getAllSettings.execute(viewModelScope) val settings = getAllSettings.execute(viewModelScope)
@ -215,6 +228,14 @@ class MainViewModel @Inject constructor(
// All additional execution // All additional execution
changeLanguage.execute(settings.language!!) changeLanguage.execute(settings.language!!)
if (settings.checkForUpdates == true) {
viewModelScope.launch(Dispatchers.IO) {
checkForUpdates.execute(
postNotification = true
)
}
}
updateStateWithSavedHandle { updateStateWithSavedHandle {
settings settings
} }
@ -249,10 +270,10 @@ class MainViewModel @Inject constructor(
} }
/** /**
* Updates [MainState] along with [SavedStateHandle]. * Updates [MainSettingsState] along with [SavedStateHandle].
*/ */
private fun updateStateWithSavedHandle( private fun updateStateWithSavedHandle(
function: (MainState) -> MainState function: (MainSettingsState) -> MainSettingsState
) { ) {
_state.update { _state.update {
stateHandle[Constants.MAIN_STATE] = function(it) stateHandle[Constants.MAIN_STATE] = function(it)

View file

@ -1,3 +1,5 @@
@file:Suppress("MemberVisibilityCanBePrivate")
package ua.acclorite.book_story.presentation.data package ua.acclorite.book_story.presentation.data
import android.annotation.SuppressLint import android.annotation.SuppressLint
@ -5,19 +7,33 @@ import android.os.Parcelable
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState 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.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext 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.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -30,6 +46,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.ui.Transitions import ua.acclorite.book_story.presentation.ui.Transitions
import java.io.Serializable 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 USE_BACK_ANIM = "use_back_animation"
private const val ARGUMENTS = "arguments" private const val ARGUMENTS = "arguments"
val LocalNavigator = compositionLocalOf<Navigator> {
error("Cannot initialize Navigator.")
}
/** /**
* All screens are listed here, later each screen will be passed as a param for [Navigator.composable] function. * All screens are listed here, later each screen will be passed as a param for [Navigator.composable] function.
*/ */
@ -71,7 +92,7 @@ data class Argument<out T : Serializable>(
) : Parcelable ) : Parcelable
/** /**
* Navigator. Using for navigation between screens. * Navigator. Using to navigate between screens.
* *
* [Navigator.currentScreen] param represents current [Screen]. * [Navigator.currentScreen] param represents current [Screen].
* [Navigator.navigate] navigates to [Screen] passed as param. * [Navigator.navigate] navigates to [Screen] passed as param.
@ -134,7 +155,7 @@ class Navigator @AssistedInject constructor(
useBackAnimation: Boolean, useBackAnimation: Boolean,
vararg args: Argument<Serializable> vararg args: Argument<Serializable>
) = viewModelScope.launch(Dispatchers.Default) { ) = viewModelScope.launch(Dispatchers.Default) {
if (backStack.value.last() == screen) { if (backStack.value.lastOrNull() == screen) {
backStack.value.removeLast() backStack.value.removeLast()
} }
@ -155,6 +176,10 @@ class Navigator @AssistedInject constructor(
} }
} }
fun clearBackStack() {
backStack.value.clear()
}
fun canGoBack(): Boolean { fun canGoBack(): Boolean {
return backStack.value.isNotEmpty() return backStack.value.isNotEmpty()
} }
@ -163,6 +188,10 @@ class Navigator @AssistedInject constructor(
return currentScreen return currentScreen
} }
fun getUseBackAnim(): StateFlow<Boolean> {
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. * 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 @Composable
fun composable( fun composable(
screen: Screen, screen: Screen,
enterAnim: EnterTransition = Transitions.FadeTransitionIn, enterAnim: EnterTransition = Transitions.SlidingTransitionIn,
backEnterAnim: EnterTransition = Transitions.BackSlidingTransitionIn, backEnterAnim: EnterTransition = Transitions.BackSlidingTransitionIn,
exitAnim: ExitTransition = Transitions.FadeTransitionOut, exitAnim: ExitTransition = Transitions.SlidingTransitionOut,
backExitAnim: ExitTransition = Transitions.BackSlidingTransitionOut, backExitAnim: ExitTransition = Transitions.BackSlidingTransitionOut,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
AnimatedVisibility( CustomAnimatedVisibility(
visible = getCurrentScreen().collectAsState().value == screen, visible = getCurrentScreen().collectAsState().value == screen,
enter = if (!useBackAnimation.collectAsState().value) enterAnim else backEnterAnim, enter = if (!useBackAnimation.collectAsState().value) enterAnim else backEnterAnim,
exit = if (!useBackAnimation.collectAsState().value) exitAnim else backExitAnim, exit = if (!useBackAnimation.collectAsState().value) exitAnim else backExitAnim
label = ""
) { ) {
content() 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 @AssistedFactory
interface Factory { interface Factory {
fun create(startScreen: Screen): Navigator fun create(startScreen: Screen): Navigator
@ -214,7 +315,6 @@ fun NavigationHost(
content: @Composable Navigator.() -> Unit content: @Composable Navigator.() -> Unit
) { ) {
val activity = LocalContext.current as ComponentActivity val activity = LocalContext.current as ComponentActivity
val navigator by activity.viewModels<Navigator>( val navigator by activity.viewModels<Navigator>(
extrasProducer = { extrasProducer = {
activity activity
@ -234,10 +334,12 @@ fun NavigationHost(
} }
Box( Box(
modifier = Modifier Modifier
.fillMaxSize() .fillMaxSize()
.background(colorBetweenAnimations) .background(colorBetweenAnimations)
) )
CompositionLocalProvider(LocalNavigator provides navigator) {
content(navigator) content(navigator)
}
} }

View file

@ -21,30 +21,58 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.GoBackButton 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.Navigator
import ua.acclorite.book_story.presentation.screens.about.components.AboutItem 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.AboutEvent
import ua.acclorite.book_story.presentation.screens.about.data.AboutState
import ua.acclorite.book_story.presentation.screens.about.data.AboutViewModel 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AboutScreen( private fun AboutScreen(
viewModel: AboutViewModel = hiltViewModel(), state: State<AboutState>,
navigator: Navigator navigator: Navigator,
onEvent: (AboutEvent) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState() val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
if (state.value.showUpdateDialog) {
AboutUpdateDialog(
state = state,
onEvent = onEvent
)
}
Scaffold( Scaffold(
Modifier Modifier
@ -104,36 +132,32 @@ fun AboutScreen(
item { item {
AboutItem( AboutItem(
title = stringResource(id = R.string.app_version_option), title = stringResource(id = R.string.app_version_option),
description = stringResource( description = buildAnnotatedString {
id = R.string.app_version_option_desc, append(
stringResource(id = R.string.app_version) stringResource(
), id = R.string.app_version_option_desc_1,
isOnClickEnabled = false context.getString(R.string.app_version)
) )
)
append("\n")
append(stringResource(id = R.string.app_version_option_desc_2))
} }
item {
AboutItem(
title = stringResource(id = R.string.all_releases_option),
description = stringResource(
id = R.string.all_releases_option_desc
)
) { ) {
onEvent(
AboutEvent.OnCheckForUpdates(
context = context,
noUpdatesFound = {
Toast.makeText( Toast.makeText(
context, context,
"${context.getString(R.string.app_version_option)}:" + context.getString(R.string.no_updates),
" ${context.getString(R.string.app_version)}",
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
viewModel.onEvent( },
AboutEvent.OnNavigateToBrowserPage( error = {
context.getString(R.string.releases_page),
context,
noAppsFound = {
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.error_no_browser), context.getString(R.string.error_check_internet),
Toast.LENGTH_SHORT Toast.LENGTH_LONG
).show() ).show()
} }
) )
@ -143,12 +167,10 @@ fun AboutScreen(
item { item {
AboutItem( AboutItem(
title = stringResource(id = R.string.issues_option), title = stringResource(id = R.string.report_bug_option),
description = stringResource( description = null
id = R.string.issues_option_desc
)
) { ) {
viewModel.onEvent( onEvent(
AboutEvent.OnNavigateToBrowserPage( AboutEvent.OnNavigateToBrowserPage(
context.getString(R.string.issues_page), context.getString(R.string.issues_page),
context, context,
@ -167,11 +189,9 @@ fun AboutScreen(
item { item {
AboutItem( AboutItem(
title = stringResource(id = R.string.project_page_option), title = stringResource(id = R.string.project_page_option),
description = stringResource( description = null
id = R.string.project_page_option_desc
)
) { ) {
viewModel.onEvent( onEvent(
AboutEvent.OnNavigateToBrowserPage( AboutEvent.OnNavigateToBrowserPage(
context.getString(R.string.project_page), context.getString(R.string.project_page),
context, 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 { item {
AboutItem( AboutItem(
title = stringResource(id = R.string.support_option), title = stringResource(id = R.string.support_option),
description = stringResource( description = null
id = R.string.support_option_desc
)
) { ) {
viewModel.onEvent( onEvent(
AboutEvent.OnNavigateToBrowserPage( AboutEvent.OnNavigateToBrowserPage(
context.getString(R.string.support_page), context.getString(R.string.support_page),
context, context,

View file

@ -3,15 +3,13 @@ package ua.acclorite.book_story.presentation.screens.about.components
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -24,7 +22,7 @@ import androidx.compose.ui.unit.sp
fun AboutItem( fun AboutItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String, title: String,
description: String?, description: AnnotatedString?,
verticalPadding: Dp = 12.dp, verticalPadding: Dp = 12.dp,
isOnClickEnabled: Boolean = true, isOnClickEnabled: Boolean = true,
onClick: () -> Unit = {} onClick: () -> Unit = {}
@ -36,21 +34,19 @@ fun AboutItem(
onClick() onClick()
} }
.padding(horizontal = 18.dp, vertical = verticalPadding), .padding(horizontal = 18.dp, vertical = verticalPadding),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center
horizontalAlignment = Alignment.Start
) { ) {
Text( Text(
title, title,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 18.sp fontSize = 18.sp
) )
Spacer(modifier = Modifier.height(2.dp))
description?.let { description?.let {
Text( Text(
it, it,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }

View file

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

View file

@ -1,11 +1,21 @@
package ua.acclorite.book_story.presentation.screens.about.data package ua.acclorite.book_story.presentation.screens.about.data
import android.content.Context import android.content.Context
import androidx.compose.runtime.Immutable
@Immutable
sealed class AboutEvent { sealed class AboutEvent {
data class OnNavigateToBrowserPage( data class OnNavigateToBrowserPage(
val page: String, val page: String,
val context: Context, val context: Context,
val noAppsFound: () -> Unit val noAppsFound: () -> Unit
) : AboutEvent() ) : AboutEvent()
data class OnCheckForUpdates(
val context: Context,
val noUpdatesFound: () -> Unit,
val error: () -> Unit
) : AboutEvent()
data object OnDismissUpdateDialog : AboutEvent()
} }

View file

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

View file

@ -5,14 +5,22 @@ import android.net.Uri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel 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 kotlinx.coroutines.launch
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.use_case.CheckForUpdates
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AboutViewModel @Inject constructor( class AboutViewModel @Inject constructor(
private val checkForUpdates: CheckForUpdates,
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(AboutState())
val state = _state.asStateFlow()
fun onEvent(event: AboutEvent) { fun onEvent(event: AboutEvent) {
when (event) { when (event) {
is AboutEvent.OnNavigateToBrowserPage -> { is AboutEvent.OnNavigateToBrowserPage -> {
@ -30,6 +38,40 @@ class AboutViewModel @Inject constructor(
event.noAppsFound() 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
)
}
}
} }
} }
} }

View file

@ -2,9 +2,11 @@ package ua.acclorite.book_story.presentation.screens.book_info
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween 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.fadeOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
@ -39,6 +42,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue 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.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar 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.CustomIconButton
import ua.acclorite.book_story.presentation.components.CustomSnackbar import ua.acclorite.book_story.presentation.components.CustomSnackbar
import ua.acclorite.book_story.presentation.components.GoBackButton 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.data.Navigator
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoBackground import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoBackground
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDescriptionSection 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.BookInfoDeleteDialog
import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoMoveDialog import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoMoveDialog
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel 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.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions 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) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable @Composable
fun BookInfoScreen( private fun BookInfoScreen(
viewModel: BookInfoViewModel, state: State<BookInfoState>,
libraryViewModel: LibraryViewModel, navigator: Navigator,
browseViewModel: BrowseViewModel, onEvent: (BookInfoEvent) -> Unit,
historyViewModel: HistoryViewModel, onLibraryEvent: (LibraryEvent) -> Unit,
navigator: Navigator onBrowseEvent: (BrowseEvent) -> Unit,
onHistoryEvent: (HistoryEvent) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val state by viewModel.state.collectAsState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val listState = rememberLazyListState() val listState = rememberLazyListState()
val snackbarState = remember { SnackbarHostState() } val snackbarState = remember { SnackbarHostState() }
val refreshState = rememberPullRefreshState( val refreshState = rememberPullRefreshState(
refreshing = state.isRefreshing || state.isLoadingUpdate, refreshing = state.value.isRefreshing || state.value.isLoadingUpdate,
onRefresh = { onRefresh = {
viewModel.onEvent( onEvent(
BookInfoEvent.OnLoadUpdate( BookInfoEvent.OnLoadUpdate(
snackbarState, snackbarState,
context context
@ -105,45 +140,43 @@ fun BookInfoScreen(
) )
val firstVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } } val firstVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } }
LaunchedEffect(Unit) { if (state.value.showChangeCoverBottomSheet) {
viewModel.init(
navigator
)
}
if (state.showChangeCoverBottomSheet) {
BookInfoChangeCoverBottomSheet( BookInfoChangeCoverBottomSheet(
libraryViewModel = libraryViewModel, state = state,
historyViewModel = historyViewModel, onEvent = onEvent,
viewModel = viewModel onLibraryUpdateEvent = onLibraryEvent,
onHistoryUpdateEvent = onHistoryEvent
) )
} }
if (state.showDetailsBottomSheet) { if (state.value.showDetailsBottomSheet) {
BookInfoDetailsBottomSheet(viewModel = viewModel) BookInfoDetailsBottomSheet(
state = state,
onEvent = onEvent
)
} }
if (state.showDeleteDialog) { if (state.value.showDeleteDialog) {
BookInfoDeleteDialog( BookInfoDeleteDialog(
libraryViewModel = libraryViewModel, onEvent = onEvent,
browseViewModel = browseViewModel, onLibraryLoadEvent = onLibraryEvent,
historyViewModel = historyViewModel, onHistoryLoadEvent = onHistoryEvent,
viewModel = viewModel, onBrowseLoadEvent = onBrowseEvent
navigator = navigator
) )
} }
if (state.showMoveDialog) { if (state.value.showMoveDialog) {
BookInfoMoveDialog( BookInfoMoveDialog(
libraryViewModel = libraryViewModel, state = state,
historyViewModel = historyViewModel, onEvent = onEvent,
viewModel = viewModel, onLibraryEvent = onLibraryEvent,
navigator = navigator onHistoryUpdateEvent = onHistoryEvent
) )
} }
if (state.showConfirmUpdateDialog) { if (state.value.showConfirmUpdateDialog) {
BookInfoConfirmUpdateDialog( BookInfoConfirmUpdateDialog(
libraryViewModel = libraryViewModel, state = state,
historyViewModel = historyViewModel, snackbarHostState = snackbarState,
viewModel = viewModel, onEvent = onEvent,
snackbarHostState = snackbarState onLibraryUpdateEvent = onLibraryEvent,
onHistoryUpdateEvent = onHistoryEvent
) )
} }
@ -161,14 +194,24 @@ fun BookInfoScreen(
isTopBarScrolled = null, isTopBarScrolled = null,
content1NavigationIcon = { content1NavigationIcon = {
GoBackButton(navigator = navigator, enabled = !state.isRefreshing) { if (state.value.editTitle) {
viewModel.onEvent(BookInfoEvent.OnCancelUpdate) 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 = { content1Title = {
DefaultTransition(visible = firstVisibleItemIndex > 0) { DefaultTransition(visible = firstVisibleItemIndex > 0) {
Text( Text(
state.book.title, state.value.book.title,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@ -181,9 +224,9 @@ fun BookInfoScreen(
icon = Icons.Default.Refresh, icon = Icons.Default.Refresh,
contentDescription = R.string.refresh_book_content_desc, contentDescription = R.string.refresh_book_content_desc,
disableOnClick = false, disableOnClick = false,
enabled = !state.isRefreshing && !state.isLoadingUpdate enabled = !state.value.isRefreshing && !state.value.isLoadingUpdate
) { ) {
viewModel.onEvent( onEvent(
BookInfoEvent.OnLoadUpdate( BookInfoEvent.OnLoadUpdate(
snackbarState, snackbarState,
context context
@ -193,15 +236,16 @@ fun BookInfoScreen(
Box { Box {
DefaultTransition( DefaultTransition(
visible = !state.editTitle visible = !state.value.editTitle
) { ) {
BookInfoMoreDropDown( BookInfoMoreDropDown(
viewModel = viewModel, state = state,
onEvent = onEvent,
snackbarState = snackbarState snackbarState = snackbarState
) )
} }
androidx.compose.animation.AnimatedVisibility( CustomAnimatedVisibility(
visible = state.editTitle, visible = state.value.editTitle,
enter = Transitions.DefaultTransitionIn, enter = Transitions.DefaultTransitionIn,
exit = fadeOut(tween(200)) exit = fadeOut(tween(200))
) { ) {
@ -209,19 +253,20 @@ fun BookInfoScreen(
icon = Icons.Default.Done, icon = Icons.Default.Done,
contentDescription = R.string.apply_changes_content_desc, contentDescription = R.string.apply_changes_content_desc,
disableOnClick = true, disableOnClick = true,
enabled = !state.isRefreshing && enabled = !state.value.isRefreshing &&
state.titleValue.isNotBlank() && state.value.titleValue.isNotBlank() &&
state.titleValue.trim() != state.value.titleValue.trim() !=
state.book.title.trim(), state.value.book.title.trim(),
color = if (state.titleValue.isNotBlank() color = if (state.value.titleValue.isNotBlank()
&& state.titleValue != state.book.title && state.value.titleValue != state.value.book.title
) MaterialTheme.colorScheme.onSurface ) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant
) { ) {
viewModel.onEvent(BookInfoEvent.OnUpdateTitle( onEvent(
BookInfoEvent.OnUpdateTitle(
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) onLibraryEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) onHistoryEvent(HistoryEvent.OnUpdateBook(it))
} }
)) ))
Toast.makeText( Toast.makeText(
@ -253,17 +298,17 @@ fun BookInfoScreen(
) { ) {
item { item {
Box(modifier = Modifier.fillMaxWidth()) { Box(modifier = Modifier.fillMaxWidth()) {
if (state.book.coverImage != null) { if (state.value.book.coverImage != null) {
BookInfoBackground( BookInfoBackground(
height = paddingValues.calculateTopPadding() + 12.dp + 195.dp, height = paddingValues.calculateTopPadding() + 12.dp + 195.dp,
image = state.book.coverImage!! image = state.value.book.coverImage!!
) )
} }
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding() + 12.dp)) Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding() + 12.dp))
// Info // Info
BookInfoInfoSection(viewModel = viewModel, book = state.book) BookInfoInfoSection(state = state, onEvent = onEvent)
} }
} }
} }
@ -271,20 +316,20 @@ fun BookInfoScreen(
item { item {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// Statistic // Statistic
BookInfoStatisticSection(book = state.book) BookInfoStatisticSection(state = state)
} }
item { item {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// Description // Description
BookInfoDescriptionSection(book = state.book) BookInfoDescriptionSection(state = state)
} }
} }
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
if (!state.isRefreshing) { if (!state.value.isRefreshing) {
viewModel.onEvent(BookInfoEvent.OnNavigateToReaderScreen(navigator)) onEvent(BookInfoEvent.OnNavigateToReaderScreen(navigator))
} }
}, },
shape = MaterialTheme.shapes.large, shape = MaterialTheme.shapes.large,
@ -302,12 +347,14 @@ fun BookInfoScreen(
contentDescription = stringResource(id = R.string.continue_reading_content_desc), contentDescription = stringResource(id = R.string.continue_reading_content_desc),
Modifier.size(24.dp) Modifier.size(24.dp)
) )
AnimatedVisibility( CustomAnimatedVisibility(
visible = !listState.canScrollBackward visible = !listState.canScrollBackward,
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally()
) { ) {
Text( Text(
text = stringResource( text = stringResource(
if (state.book.progress == 0f) R.string.start if (state.value.book.progress == 0f) R.string.start
else R.string.continue_read else R.string.continue_read
), ),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
@ -321,7 +368,7 @@ fun BookInfoScreen(
) )
PullRefreshIndicator( PullRefreshIndicator(
state.isRefreshing || state.isLoadingUpdate, state.value.isRefreshing || state.value.isLoadingUpdate,
refreshState, refreshState,
Modifier Modifier
.align(Alignment.TopCenter) .align(Alignment.TopCenter)
@ -333,8 +380,13 @@ fun BookInfoScreen(
} }
BackHandler { BackHandler {
if (!state.isRefreshing) { if (state.value.editTitle) {
viewModel.onEvent(BookInfoEvent.OnCancelUpdate) onEvent(BookInfoEvent.OnShowHideEditTitle)
return@BackHandler
}
if (!state.value.isRefreshing) {
onEvent(BookInfoEvent.OnCancelUpdate)
navigator.navigateBack() navigator.navigateBack()
} }
} }

View file

@ -7,21 +7,22 @@ import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
/** /**
* Description section. * Description section.
*/ */
@Composable @Composable
fun BookInfoDescriptionSection(book: Book) { fun BookInfoDescriptionSection(state: State<BookInfoState>) {
SelectionContainer { SelectionContainer {
Text( 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), else stringResource(id = R.string.error_no_description),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,

View file

@ -24,8 +24,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -35,23 +34,23 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R 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.components.CustomCoverImage
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
/** /**
* Info section. * Info section.
*/ */
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) { fun BookInfoInfoSection(
val state by viewModel.state.collectAsState() state: State<BookInfoState>,
onEvent: (BookInfoEvent) -> Unit
) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
Row( Row(
@ -70,13 +69,13 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
indication = null, indication = null,
onClick = {}, onClick = {},
onLongClick = { onLongClick = {
viewModel.onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet) onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
} }
) )
) { ) {
if (book.coverImage != null) { if (state.value.book.coverImage != null) {
CustomCoverImage( CustomCoverImage(
uri = book.coverImage, uri = state.value.book.coverImage!!,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
@ -97,9 +96,9 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Column(verticalArrangement = Arrangement.Center) { Column(verticalArrangement = Arrangement.Center) {
if (!state.editTitle) { if (!state.value.editTitle) {
Text( Text(
book.title, state.value.book.title,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
modifier = Modifier modifier = Modifier
@ -109,7 +108,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
indication = null, indication = null,
onClick = {}, onClick = {},
onLongClick = { onLongClick = {
viewModel.onEvent(BookInfoEvent.OnShowHideEditTitle) onEvent(BookInfoEvent.OnShowHideEditTitle)
} }
), ),
maxLines = 4, maxLines = 4,
@ -118,22 +117,19 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
} else { } else {
BasicTextField( BasicTextField(
maxLines = 4, maxLines = 4,
value = state.titleValue, value = state.value.titleValue,
textStyle = TextStyle( textStyle = MaterialTheme.typography.headlineSmall.copy(
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface
fontSize = MaterialTheme.typography.headlineSmall.fontSize,
lineHeight = MaterialTheme.typography.headlineSmall.lineHeight,
fontFamily = MaterialTheme.typography.headlineSmall.fontFamily
), ),
onValueChange = { onValueChange = {
if (it.length < 100 || it.length < state.titleValue.length) { if (it.length < 100 || it.length < state.value.titleValue.length) {
viewModel.onEvent(BookInfoEvent.OnTitleValueChange(it)) onEvent(BookInfoEvent.OnTitleValueChange(it))
} }
}, },
modifier = Modifier modifier = Modifier
.focusRequester(focusRequester) .focusRequester(focusRequester)
.onGloballyPositioned { .onGloballyPositioned {
viewModel.onEvent(BookInfoEvent.OnRequestFocus(focusRequester)) onEvent(BookInfoEvent.OnRequestFocus(focusRequester))
}, },
keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words), keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words),
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant)
@ -149,7 +145,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
SelectionContainer { SelectionContainer {
Text( Text(
book.author.asString(), state.value.book.author.asString(),
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
modifier = Modifier modifier = Modifier

View file

@ -9,7 +9,7 @@ import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.CustomDropDownMenuItem
import ua.acclorite.book_story.presentation.components.CustomIconButton 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.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. * Book info more dropdown. Has three options: Delete, Move and Details.
*/ */
@Composable @Composable
fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHostState) { fun BookInfoMoreDropDown(
val state by viewModel.state.collectAsState() state: State<BookInfoState>,
onEvent: (BookInfoEvent) -> Unit,
snackbarState: SnackbarHostState
) {
var showDropDown by remember { mutableStateOf(false) } var showDropDown by remember { mutableStateOf(false) }
Box { Box {
@ -37,7 +40,7 @@ fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHo
contentDescription = R.string.show_dropdown_content_desc, contentDescription = R.string.show_dropdown_content_desc,
disableOnClick = false, disableOnClick = false,
enabled = !showDropDown && enabled = !showDropDown &&
!state.isRefreshing !state.value.isRefreshing
) { ) {
showDropDown = true showDropDown = true
} }
@ -53,14 +56,14 @@ fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHo
leadingIcon = Icons.AutoMirrored.Outlined.DriveFileMove, leadingIcon = Icons.AutoMirrored.Outlined.DriveFileMove,
text = stringResource(id = R.string.move_this_book) text = stringResource(id = R.string.move_this_book)
) { ) {
viewModel.onEvent(BookInfoEvent.OnShowHideMoveDialog) onEvent(BookInfoEvent.OnShowHideMoveDialog)
showDropDown = false showDropDown = false
} }
CustomDropDownMenuItem( CustomDropDownMenuItem(
leadingIcon = Icons.Outlined.Delete, leadingIcon = Icons.Outlined.Delete,
text = stringResource(id = R.string.delete_this_book) text = stringResource(id = R.string.delete_this_book)
) { ) {
viewModel.onEvent(BookInfoEvent.OnShowHideDeleteDialog) onEvent(BookInfoEvent.OnShowHideDeleteDialog)
showDropDown = false showDropDown = false
} }
CustomDropDownMenuItem( CustomDropDownMenuItem(
@ -68,7 +71,7 @@ fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHo
text = stringResource(id = R.string.details_book) text = stringResource(id = R.string.details_book)
) { ) {
snackbarState.currentSnackbarData?.dismiss() snackbarState.currentSnackbarData?.dismiss()
viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet) onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
showDropDown = false showDropDown = false
} }
} }

View file

@ -9,6 +9,7 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero import ua.acclorite.book_story.presentation.data.removeTrailingZero
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
/** /**
* Statistic section. * Statistic section.
*/ */
@Composable @Composable
fun BookInfoStatisticSection(book: Book) { fun BookInfoStatisticSection(state: State<BookInfoState>) {
val progress = remember(book) { val progress = remember(state.value.book) {
"${ "${
(book.progress * 100) (state.value.book.progress * 100)
.toDouble() .toDouble()
.removeDigits(1) .removeDigits(1)
.removeTrailingZero() .removeTrailingZero()
}%" }%"
} }
val description = stringResource( val description = stringResource(
if (book.progress == 1f) R.string.read_done if (state.value.book.progress == 1f) R.string.read_done
else if (book.progress > 0.2f) R.string.read_keep else if (state.value.book.progress > 0.2f) R.string.read_keep
else R.string.read_more else R.string.read_more
) )
@ -53,7 +54,7 @@ fun BookInfoStatisticSection(book: Book) {
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
LinearProgressIndicator( LinearProgressIndicator(
progress = { book.progress.coerceAtLeast(0.01f) }, progress = { state.value.book.progress.coerceAtLeast(0.01f) },
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,

View file

@ -18,19 +18,16 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R 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.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.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.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. * 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BookInfoChangeCoverBottomSheet( fun BookInfoChangeCoverBottomSheet(
libraryViewModel: LibraryViewModel, state: State<BookInfoState>,
historyViewModel: HistoryViewModel, onEvent: (BookInfoEvent) -> Unit,
viewModel: BookInfoViewModel onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit
) { ) {
val state by viewModel.state.collectAsState()
val context = LocalContext.current val context = LocalContext.current
val navigationBarPadding = val navigationBarPadding =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
val photoPicker = rememberLauncherForActivityResult( val photoPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(), contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri -> onResult = { uri ->
if (uri != null) { if (uri != null) {
viewModel.onEvent( onEvent(
BookInfoEvent.OnChangeCover( BookInfoEvent.OnChangeCover(
uri, uri,
context, context,
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
} }
) )
) )
@ -73,7 +69,7 @@ fun BookInfoChangeCoverBottomSheet(
ModalBottomSheet( ModalBottomSheet(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onDismissRequest = { onDismissRequest = {
viewModel.onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet) onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
}, },
sheetState = rememberModalBottomSheetState(true), sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
@ -90,17 +86,17 @@ fun BookInfoChangeCoverBottomSheet(
) )
} }
if (state.book.coverImage != null) { if (state.value.book.coverImage != null) {
BookInfoChangeCoverBottomSheetItem( BookInfoChangeCoverBottomSheetItem(
icon = Icons.Default.HideImage, icon = Icons.Default.HideImage,
text = stringResource(id = R.string.delete_cover), text = stringResource(id = R.string.delete_cover),
description = stringResource(id = R.string.delete_cover_desc) description = stringResource(id = R.string.delete_cover_desc)
) { ) {
viewModel.onEvent( onEvent(
BookInfoEvent.OnDeleteCover( BookInfoEvent.OnDeleteCover(
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
} }
) )
) )

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@ -58,7 +57,6 @@ fun BookInfoChangeCoverBottomSheetItem(
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
fontSize = 18.sp fontSize = 18.sp
) )
Spacer(modifier = Modifier.height(2.dp))
Text( Text(
description, description,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,

View file

@ -4,28 +4,28 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Update import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn 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.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.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.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
/**
* Confirm update dialog. Updates book if action clicked.
*/
@Composable @Composable
fun BookInfoConfirmUpdateDialog( fun BookInfoConfirmUpdateDialog(
libraryViewModel: LibraryViewModel, state: State<BookInfoState>,
historyViewModel: HistoryViewModel, snackbarHostState: SnackbarHostState,
viewModel: BookInfoViewModel, onEvent: (BookInfoEvent) -> Unit,
snackbarHostState: SnackbarHostState onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val state by viewModel.state.collectAsState()
CustomDialogWithLazyColumn( CustomDialogWithLazyColumn(
title = stringResource(id = R.string.confirm_update), title = stringResource(id = R.string.confirm_update),
@ -35,32 +35,32 @@ fun BookInfoConfirmUpdateDialog(
), ),
actionText = stringResource(id = R.string.confirm), actionText = stringResource(id = R.string.confirm),
isActionEnabled = true, isActionEnabled = true,
onDismiss = { viewModel.onEvent(BookInfoEvent.OnDismissConfirmUpdateDialog) }, onDismiss = { onEvent(BookInfoEvent.OnDismissConfirmUpdateDialog) },
onAction = { onAction = {
viewModel.onEvent( onEvent(
BookInfoEvent.OnConfirmUpdate( BookInfoEvent.OnConfirmUpdate(
snackbarState = snackbarHostState, snackbarState = snackbarHostState,
context = context, context = context,
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
} }
) )
) )
}, },
withDivider = false, withDivider = false,
items = { items = {
if (state.authorChanged) { if (state.value.authorChanged) {
item { item {
BookInfoConfirmUpdateDialogItem(title = stringResource(id = R.string.author)) BookInfoConfirmUpdateDialogItem(title = stringResource(id = R.string.author))
} }
} }
if (state.descriptionChanged) { if (state.value.descriptionChanged) {
item { item {
BookInfoConfirmUpdateDialogItem(title = stringResource(id = R.string.description)) BookInfoConfirmUpdateDialogItem(title = stringResource(id = R.string.description))
} }
} }
if (state.textChanged) { if (state.value.textChanged) {
item { item {
BookInfoConfirmUpdateDialogItem(title = stringResource(id = R.string.text)) BookInfoConfirmUpdateDialogItem(title = stringResource(id = R.string.text))
} }

View file

@ -7,6 +7,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
/**
* Confirm update dialog item.
*/
@Composable @Composable
fun BookInfoConfirmUpdateDialogItem(title: String) { fun BookInfoConfirmUpdateDialogItem(title: String) {
Text( Text(

View file

@ -12,8 +12,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -21,7 +20,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -32,9 +31,9 @@ import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BookInfoDetailsBottomSheet( fun BookInfoDetailsBottomSheet(
viewModel: BookInfoViewModel state: State<BookInfoState>,
onEvent: (BookInfoEvent) -> Unit
) { ) {
val state by viewModel.state.collectAsState()
val context = LocalContext.current val context = LocalContext.current
val navigationBarPadding = val navigationBarPadding =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
@ -43,11 +42,11 @@ fun BookInfoDetailsBottomSheet(
SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault())
} }
val lastOpened = remember { val lastOpened = remember {
pattern.format(Date(state.book.lastOpened ?: 0)) pattern.format(Date(state.value.book.lastOpened ?: 0))
} }
val sizeBytes = remember { val sizeBytes = remember {
state.book.file?.length() ?: 0 state.value.book.file?.length() ?: 0
} }
val fileSizeKB = remember { val fileSizeKB = remember {
@ -66,7 +65,7 @@ fun BookInfoDetailsBottomSheet(
ModalBottomSheet( ModalBottomSheet(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onDismissRequest = { onDismissRequest = {
viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet) onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
}, },
sheetState = rememberModalBottomSheetState(true), sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
@ -74,11 +73,12 @@ fun BookInfoDetailsBottomSheet(
) { ) {
BookInfoDetailsBottomSheetItem( BookInfoDetailsBottomSheetItem(
title = stringResource(id = R.string.file_name), title = stringResource(id = R.string.file_name),
description = state.book.filePath.substringAfterLast("/").trim() description = state.value.book.filePath.substringAfterLast("/").trim()
) { ) {
viewModel.onEvent(BookInfoEvent.OnCopyToClipboard( onEvent(
BookInfoEvent.OnCopyToClipboard(
context, context,
state.book.filePath.substringAfterLast("/").trim(), state.value.book.filePath.substringAfterLast("/").trim(),
success = { success = {
Toast.makeText( Toast.makeText(
context, context,
@ -90,11 +90,12 @@ fun BookInfoDetailsBottomSheet(
} }
BookInfoDetailsBottomSheetItem( BookInfoDetailsBottomSheetItem(
title = stringResource(id = R.string.file_path), title = stringResource(id = R.string.file_path),
description = state.book.filePath.trim() description = state.value.book.filePath.trim()
) { ) {
viewModel.onEvent(BookInfoEvent.OnCopyToClipboard( onEvent(
BookInfoEvent.OnCopyToClipboard(
context, context,
state.book.filePath.trim(), state.value.book.filePath.trim(),
success = { success = {
Toast.makeText( Toast.makeText(
context, context,
@ -106,11 +107,12 @@ fun BookInfoDetailsBottomSheet(
} }
BookInfoDetailsBottomSheetItem( BookInfoDetailsBottomSheetItem(
title = stringResource(id = R.string.file_last_opened), title = stringResource(id = R.string.file_last_opened),
description = if (state.book.lastOpened != null) lastOpened description = if (state.value.book.lastOpened != null) lastOpened
else stringResource(id = R.string.never) else stringResource(id = R.string.never)
) { ) {
if (state.book.lastOpened != null) { if (state.value.book.lastOpened != null) {
viewModel.onEvent(BookInfoEvent.OnCopyToClipboard( onEvent(
BookInfoEvent.OnCopyToClipboard(
context, context,
lastOpened, lastOpened,
success = { success = {
@ -128,7 +130,8 @@ fun BookInfoDetailsBottomSheet(
description = fileSize.ifBlank { stringResource(id = R.string.unknown) } description = fileSize.ifBlank { stringResource(id = R.string.unknown) }
) { ) {
if (fileSize.isNotBlank()) { if (fileSize.isNotBlank()) {
viewModel.onEvent(BookInfoEvent.OnCopyToClipboard( onEvent(
BookInfoEvent.OnCopyToClipboard(
context, context,
fileSize, fileSize,
success = { success = {

View file

@ -8,27 +8,23 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent 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. * Delete dialog. Deletes current book.
*/ */
@Composable @Composable
fun BookInfoDeleteDialog( fun BookInfoDeleteDialog(
libraryViewModel: LibraryViewModel, onEvent: (BookInfoEvent) -> Unit,
browseViewModel: BrowseViewModel, onLibraryLoadEvent: (LibraryEvent.OnLoadList) -> Unit,
historyViewModel: HistoryViewModel, onHistoryLoadEvent: (HistoryEvent.OnLoadList) -> Unit,
viewModel: BookInfoViewModel, onBrowseLoadEvent: (BrowseEvent.OnLoadList) -> Unit
navigator: Navigator
) { ) {
val navigator = LocalNavigator.current
val context = LocalContext.current val context = LocalContext.current
CustomDialogWithContent( CustomDialogWithContent(
@ -38,17 +34,17 @@ fun BookInfoDeleteDialog(
id = R.string.delete_book_description id = R.string.delete_book_description
), ),
actionText = stringResource(id = R.string.delete), actionText = stringResource(id = R.string.delete),
onDismiss = { viewModel.onEvent(BookInfoEvent.OnShowHideDeleteDialog) }, onDismiss = { onEvent(BookInfoEvent.OnShowHideDeleteDialog) },
withDivider = false, withDivider = false,
isActionEnabled = true, isActionEnabled = true,
onAction = { onAction = {
viewModel.onEvent( onEvent(
BookInfoEvent.OnDeleteBook( BookInfoEvent.OnDeleteBook(
navigator = navigator, navigator = navigator,
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnLoadList) onLibraryLoadEvent(LibraryEvent.OnLoadList)
browseViewModel.onEvent(BrowseEvent.OnLoadList) onBrowseLoadEvent(BrowseEvent.OnLoadList)
historyViewModel.onEvent(HistoryEvent.OnLoadList) onHistoryLoadEvent(HistoryEvent.OnLoadList)
} }
) )
) )

View file

@ -6,8 +6,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.domain.model.Category
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn 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.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.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.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.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
/** /**
* Move dialog. Moves current book to the selected category. * Move dialog. Moves current book to the selected category.
*/ */
@Composable @Composable
fun BookInfoMoveDialog( fun BookInfoMoveDialog(
libraryViewModel: LibraryViewModel, state: State<BookInfoState>,
historyViewModel: HistoryViewModel, onEvent: (BookInfoEvent) -> Unit,
viewModel: BookInfoViewModel, onLibraryEvent: (LibraryEvent) -> Unit,
navigator: Navigator onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit
) { ) {
val state by viewModel.state.collectAsState() val navigator = LocalNavigator.current
val context = LocalContext.current val context = LocalContext.current
val categories = remember { val categories = remember {
Category.entries.filter { state.book.category != it } Category.entries.filter { state.value.book.category != it }
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.onEvent(BookInfoEvent.OnSelectCategory(categories[0])) onEvent(BookInfoEvent.OnSelectCategory(categories[0]))
} }
CustomDialogWithLazyColumn( CustomDialogWithLazyColumn(
@ -52,16 +49,16 @@ fun BookInfoMoveDialog(
), ),
actionText = stringResource(id = R.string.move), actionText = stringResource(id = R.string.move),
isActionEnabled = true, isActionEnabled = true,
onDismiss = { viewModel.onEvent(BookInfoEvent.OnShowHideMoveDialog) }, onDismiss = { onEvent(BookInfoEvent.OnShowHideMoveDialog) },
onAction = { onAction = {
viewModel.onEvent( onEvent(
BookInfoEvent.OnMoveBook( BookInfoEvent.OnMoveBook(
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) onLibraryEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
}, },
updatePage = { updatePage = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it)) onLibraryEvent(LibraryEvent.OnUpdateCurrentPage(it))
}, },
navigator = navigator navigator = navigator
) )
@ -82,8 +79,11 @@ fun BookInfoMoveDialog(
Category.DROPPED -> stringResource(id = R.string.dropped_tab) Category.DROPPED -> stringResource(id = R.string.dropped_tab)
} }
SelectableDialogItem(selected = it == state.selectedCategory, title = category) { SelectableDialogItem(
viewModel.onEvent(BookInfoEvent.OnSelectCategory(it)) selected = it == state.value.selectedCategory,
title = category
) {
onEvent(BookInfoEvent.OnSelectCategory(it))
} }
} }
} }

View file

@ -3,11 +3,13 @@ package ua.acclorite.book_story.presentation.screens.book_info.data
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
@Immutable
sealed class BookInfoEvent { sealed class BookInfoEvent {
data object OnShowHideChangeCoverBottomSheet : BookInfoEvent() data object OnShowHideChangeCoverBottomSheet : BookInfoEvent()
data object OnShowHideDetailsBottomSheet : BookInfoEvent() data object OnShowHideDetailsBottomSheet : BookInfoEvent()

View file

@ -3,18 +3,14 @@ package ua.acclorite.book_story.presentation.screens.browse
import android.Manifest import android.Manifest
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
@ -30,6 +26,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -39,62 +36,81 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar 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.CustomIconButton
import ua.acclorite.book_story.presentation.components.CustomSearchTextField
import ua.acclorite.book_story.presentation.components.MoreDropDown import ua.acclorite.book_story.presentation.components.MoreDropDown
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
import ua.acclorite.book_story.presentation.components.is_messages.IsError 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.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.browse.components.BrowseFileItem import ua.acclorite.book_story.presentation.screens.browse.components.BrowseFileItem
import ua.acclorite.book_story.presentation.screens.browse.components.BrowseStoragePermissionDialog import ua.acclorite.book_story.presentation.screens.browse.components.BrowseStoragePermissionDialog
import ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog.BrowseAddingDialog import ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog.BrowseAddingDialog
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseState
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel 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.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions 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( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalPermissionsApi::class, ExperimentalPermissionsApi::class,
ExperimentalMaterialApi::class ExperimentalMaterialApi::class
) )
@Composable @Composable
fun BrowseScreen( private fun BrowseScreen(
viewModel: BrowseViewModel, state: State<BrowseState>,
libraryViewModel: LibraryViewModel, navigator: Navigator,
navigator: Navigator onEvent: (BrowseEvent) -> Unit,
onLibraryEvent: (LibraryEvent) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val permissionState = rememberPermissionState( val permissionState = rememberPermissionState(
permission = Manifest.permission.READ_EXTERNAL_STORAGE permission = Manifest.permission.READ_EXTERNAL_STORAGE
) )
val state by viewModel.state.collectAsState()
val refreshState = rememberPullRefreshState( val refreshState = rememberPullRefreshState(
refreshing = state.isRefreshing, refreshing = state.value.isRefreshing,
onRefresh = { onRefresh = {
viewModel.onEvent(BrowseEvent.OnRefreshList) onEvent(BrowseEvent.OnRefreshList)
} }
) )
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
var showErrorMessage by remember { mutableStateOf(false) } var showErrorMessage by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.onEvent(BrowseEvent.OnUpdateScrollOffset) onEvent(BrowseEvent.OnUpdateScrollOffset)
viewModel.onEvent( onEvent(
BrowseEvent.OnPermissionCheck( BrowseEvent.OnPermissionCheck(
permissionState, permissionState,
hideErrorMessage = { showErrorMessage = false } hideErrorMessage = { showErrorMessage = false }
@ -102,16 +118,16 @@ fun BrowseScreen(
) )
} }
if (state.requestPermissionDialog) { if (state.value.requestPermissionDialog) {
BrowseStoragePermissionDialog(viewModel, permissionState) { BrowseStoragePermissionDialog(onEvent, permissionState) {
showErrorMessage = it showErrorMessage = it
} }
} }
if (state.showAddingDialog) { if (state.value.showAddingDialog) {
BrowseAddingDialog( BrowseAddingDialog(
viewModel = viewModel, state = state,
navigator = navigator, onEvent = onEvent,
libraryViewModel = libraryViewModel onLibraryEvent = onLibraryEvent
) )
} }
@ -123,9 +139,9 @@ fun BrowseScreen(
topBar = { topBar = {
AnimatedTopAppBar( AnimatedTopAppBar(
scrollBehavior = null, 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 = {}, content1NavigationIcon = {},
content1Title = { content1Title = {
Text(stringResource(id = R.string.browse_screen)) Text(stringResource(id = R.string.browse_screen))
@ -136,26 +152,26 @@ fun BrowseScreen(
contentDescription = R.string.search_content_desc, contentDescription = R.string.search_content_desc,
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent(BrowseEvent.OnSearchShowHide) onEvent(BrowseEvent.OnSearchShowHide)
} }
MoreDropDown(navigator = navigator) MoreDropDown()
}, },
content2Visibility = state.hasSelectedItems, content2Visibility = state.value.hasSelectedItems,
content2NavigationIcon = { content2NavigationIcon = {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Clear, icon = Icons.Default.Clear,
contentDescription = R.string.clear_selected_items_content_desc, contentDescription = R.string.clear_selected_items_content_desc,
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent(BrowseEvent.OnClearSelectedFiles) onEvent(BrowseEvent.OnClearSelectedFiles)
} }
}, },
content2Title = { content2Title = {
Text( Text(
stringResource( stringResource(
id = R.string.selected_items_count_query, id = R.string.selected_items_count_query,
state.selectedItemsCount.coerceAtLeast(1) state.value.selectedItemsCount.coerceAtLeast(1)
), ),
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1 maxLines = 1
@ -166,65 +182,44 @@ fun BrowseScreen(
icon = Icons.Default.Check, icon = Icons.Default.Check,
contentDescription = R.string.add_files_content_desc, contentDescription = R.string.add_files_content_desc,
disableOnClick = false, 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 = { content3NavigationIcon = {
CustomIconButton( CustomIconButton(
icon = Icons.AutoMirrored.Default.ArrowBack, icon = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = R.string.exit_search_content_desc, contentDescription = R.string.exit_search_content_desc,
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent(BrowseEvent.OnSearchShowHide) onEvent(BrowseEvent.OnSearchShowHide)
} }
}, },
content3Title = { content3Title = {
BasicTextField( CustomSearchTextField(
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
),
modifier = Modifier modifier = Modifier
.focusRequester(focusRequester) .focusRequester(focusRequester)
.onGloballyPositioned { .onGloballyPositioned {
viewModel.onEvent(BrowseEvent.OnRequestFocus(focusRequester)) onEvent(BrowseEvent.OnRequestFocus(focusRequester))
}, },
onValueChange = { query = state.value.searchQuery,
viewModel.onEvent(BrowseEvent.OnSearchQueryChange(it)) onQueryChange = {
onEvent(BrowseEvent.OnSearchQueryChange(it))
}, },
keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words), onSearch = {
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) onEvent(BrowseEvent.OnSearch)
) { innerText -> },
Box( placeholder = stringResource(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.CenterStart
) {
if (state.searchQuery.isEmpty()) {
Text(
stringResource(
id = R.string.search_query, id = R.string.search_query,
stringResource(id = R.string.files) stringResource(id = R.string.files)
),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} )
innerText()
}
}
}, },
content3Actions = { content3Actions = {
MoreDropDown(navigator = navigator) MoreDropDown()
} }
) )
} }
@ -234,21 +229,21 @@ fun BrowseScreen(
.fillMaxSize() .fillMaxSize()
.padding(top = padding.calculateTopPadding()) .padding(top = padding.calculateTopPadding())
) { ) {
DefaultTransition(visible = !state.isLoading) { DefaultTransition(visible = !state.value.isLoading) {
LazyColumn( LazyColumn(
state = state.listState, state = state.value.listState,
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
contentPadding = PaddingValues(vertical = 8.dp) contentPadding = PaddingValues(vertical = 8.dp)
) { ) {
items( items(
state.selectableFiles, state.value.selectableFiles,
key = { it.first.path } key = { it.first.path }
) { selectableFile -> ) { selectableFile ->
BrowseFileItem( BrowseFileItem(
file = selectableFile, file = selectableFile,
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
hasSelectedFiles = state.selectableFiles.any { it.second }, hasSelectedFiles = state.value.selectableFiles.any { it.second },
onLongClick = { onLongClick = {
Toast.makeText( Toast.makeText(
context, context,
@ -260,14 +255,14 @@ fun BrowseScreen(
).show() ).show()
}, },
onClick = { onClick = {
viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile)) onEvent(BrowseEvent.OnSelectFile(selectableFile))
} }
) )
} }
} }
} }
AnimatedVisibility( CustomAnimatedVisibility(
visible = showErrorMessage, visible = showErrorMessage,
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn, enter = Transitions.DefaultTransitionIn,
@ -279,7 +274,7 @@ fun BrowseScreen(
icon = painterResource(id = R.drawable.error), icon = painterResource(id = R.drawable.error),
actionTitle = stringResource(id = R.string.grant_permission) actionTitle = stringResource(id = R.string.grant_permission)
) { ) {
viewModel.onEvent( onEvent(
BrowseEvent.OnPermissionCheck( BrowseEvent.OnPermissionCheck(
permissionState, permissionState,
hideErrorMessage = { showErrorMessage = false } hideErrorMessage = { showErrorMessage = false }
@ -288,10 +283,10 @@ fun BrowseScreen(
} }
} }
AnimatedVisibility( CustomAnimatedVisibility(
visible = !state.isLoading && state.selectableFiles.isEmpty() visible = !state.value.isLoading && state.value.selectableFiles.isEmpty()
&& !showErrorMessage && !state.requestPermissionDialog && !showErrorMessage && !state.value.requestPermissionDialog
&& !state.isRefreshing, && !state.value.isRefreshing,
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn, enter = Transitions.DefaultTransitionIn,
exit = fadeOut(tween(0)) exit = fadeOut(tween(0))
@ -305,7 +300,7 @@ fun BrowseScreen(
} }
PullRefreshIndicator( PullRefreshIndicator(
state.isRefreshing, state.value.isRefreshing,
refreshState, refreshState,
Modifier.align(Alignment.TopCenter), Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.inverseSurface, backgroundColor = MaterialTheme.colorScheme.inverseSurface,
@ -315,13 +310,13 @@ fun BrowseScreen(
} }
BackHandler { BackHandler {
if (state.hasSelectedItems) { if (state.value.hasSelectedItems) {
viewModel.onEvent(BrowseEvent.OnClearSelectedFiles) onEvent(BrowseEvent.OnClearSelectedFiles)
return@BackHandler return@BackHandler
} }
if (state.showSearch) { if (state.value.showSearch) {
viewModel.onEvent(BrowseEvent.OnSearchShowHide) onEvent(BrowseEvent.OnSearchShowHide)
return@BackHandler return@BackHandler
} }

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.screens.browse.components package ua.acclorite.book_story.presentation.screens.browse.components
import android.os.Build
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SdStorage 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.R
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
/** /**
* Storage permission dialog. * Storage permission dialog.
@ -20,7 +18,7 @@ import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun BrowseStoragePermissionDialog( fun BrowseStoragePermissionDialog(
viewModel: BrowseViewModel, onEvent: (BrowseEvent) -> Unit,
permissionState: PermissionState, permissionState: PermissionState,
showErrorMessage: (Boolean) -> Unit showErrorMessage: (Boolean) -> Unit
) { ) {
@ -32,7 +30,7 @@ fun BrowseStoragePermissionDialog(
actionText = stringResource(id = R.string.grant), actionText = stringResource(id = R.string.grant),
imageVectorIcon = Icons.Default.SdStorage, imageVectorIcon = Icons.Default.SdStorage,
onDismiss = { onDismiss = {
viewModel.onEvent( onEvent(
BrowseEvent.OnStoragePermissionDismiss( BrowseEvent.OnStoragePermissionDismiss(
permissionState, permissionState,
showErrorMessage = { showErrorMessage(true) } showErrorMessage = { showErrorMessage(true) }
@ -41,19 +39,14 @@ fun BrowseStoragePermissionDialog(
}, },
isActionEnabled = true, isActionEnabled = true,
withDivider = false, withDivider = false,
disableOnClick = false,
onAction = { onAction = {
viewModel.onEvent( onEvent(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
BrowseEvent.OnStoragePermissionRequest( BrowseEvent.OnStoragePermissionRequest(
activity, activity,
storagePermissionState = permissionState,
hideErrorMessage = { showErrorMessage(false) } hideErrorMessage = { showErrorMessage(false) }
) )
} else {
BrowseEvent.OnLegacyStoragePermissionRequest(
permissionState,
hideErrorMessage = { showErrorMessage(false) }
)
}
) )
} }
) )

View file

@ -11,8 +11,7 @@ import androidx.compose.material.icons.filled.AddChart
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap 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.R
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel import ua.acclorite.book_story.presentation.screens.browse.data.BrowseState
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
/** /**
* Adding dialog. Adds all selected books to the Library. * Adding dialog. Adds all selected books to the Library.
*/ */
@Composable @Composable
fun BrowseAddingDialog( fun BrowseAddingDialog(
libraryViewModel: LibraryViewModel, state: State<BrowseState>,
viewModel: BrowseViewModel, onEvent: (BrowseEvent) -> Unit,
navigator: Navigator onLibraryEvent: (LibraryEvent) -> Unit
) { ) {
val state by viewModel.state.collectAsState()
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.current
CustomDialogWithLazyColumn( CustomDialogWithLazyColumn(
title = stringResource(id = R.string.add_books), title = stringResource(id = R.string.add_books),
imageVectorIcon = Icons.Default.AddChart, imageVectorIcon = Icons.Default.AddChart,
description = stringResource(id = R.string.add_books_description), description = stringResource(id = R.string.add_books_description),
actionText = stringResource(id = R.string.add), actionText = stringResource(id = R.string.add),
isActionEnabled = !state.isBooksLoading && isActionEnabled = !state.value.isBooksLoading &&
state.selectedBooks.any { it.first is NullableBook.NotNull }, state.value.selectedBooks.any { it.first is NullableBook.NotNull },
onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) }, onDismiss = { onEvent(BrowseEvent.OnAddingDialogDismiss) },
onAction = { onAction = {
viewModel.onEvent( onEvent(
BrowseEvent.OnAddBooks( BrowseEvent.OnAddBooks(
navigator, navigator,
resetScroll = { resetScroll = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(0)) onLibraryEvent(LibraryEvent.OnUpdateCurrentPage(0))
libraryViewModel.onEvent(LibraryEvent.OnLoadList) onLibraryEvent(LibraryEvent.OnLoadList)
}, },
onFailed = { onFailed = {
Toast.makeText( Toast.makeText(
@ -75,7 +73,7 @@ fun BrowseAddingDialog(
}, },
withDivider = true, withDivider = true,
items = { items = {
if (state.isBooksLoading) { if (state.value.isBooksLoading) {
item { item {
Box( Box(
modifier = Modifier modifier = Modifier
@ -92,12 +90,12 @@ fun BrowseAddingDialog(
} }
} }
} else { } else {
items(state.selectedBooks) { book -> items(state.value.selectedBooks) { book ->
BrowseAddingDialogItem( BrowseAddingDialogItem(
result = book result = book
) { ) {
if (it) { if (it) {
viewModel.onEvent(BrowseEvent.OnSelectBook(book)) onEvent(BrowseEvent.OnSelectBook(book))
} else { } else {
Toast.makeText( Toast.makeText(
context, context,

View file

@ -3,6 +3,7 @@
package ua.acclorite.book_story.presentation.screens.browse.data package ua.acclorite.book_story.presentation.screens.browse.data
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.Immutable
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.PermissionState
@ -11,13 +12,12 @@ import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import java.io.File import java.io.File
@Immutable
sealed class BrowseEvent { sealed class BrowseEvent {
data class OnStoragePermissionRequest( data class OnStoragePermissionRequest(
val activity: ComponentActivity, val hideErrorMessage: () -> Unit val activity: ComponentActivity,
) : BrowseEvent() val storagePermissionState: PermissionState,
val hideErrorMessage: () -> Unit
data class OnLegacyStoragePermissionRequest(
val permissionState: PermissionState, val hideErrorMessage: () -> Unit
) : BrowseEvent() ) : BrowseEvent()
data class OnStoragePermissionDismiss( data class OnStoragePermissionDismiss(
@ -36,6 +36,7 @@ sealed class BrowseEvent {
data class OnRequestFocus(val focusRequester: FocusRequester) : BrowseEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : BrowseEvent()
data object OnClearSelectedFiles : BrowseEvent() data object OnClearSelectedFiles : BrowseEvent()
data class OnSearchQueryChange(val query: String) : BrowseEvent() data class OnSearchQueryChange(val query: String) : BrowseEvent()
data object OnSearch : BrowseEvent()
data object OnAddingDialogRequest : BrowseEvent() data object OnAddingDialogRequest : BrowseEvent()
data object OnAddingDialogDismiss : BrowseEvent() data object OnAddingDialogDismiss : BrowseEvent()
data object OnGetBooksFromFiles : BrowseEvent() data object OnGetBooksFromFiles : BrowseEvent()

View file

@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.shouldShowRationale
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -41,6 +42,7 @@ class BrowseViewModel @Inject constructor(
private var job: Job? = null private var job: Job? = null
private var job2: Job? = null private var job2: Job? = null
private var job3: Job? = null private var job3: Job? = null
private var storagePermissionJob: Job? = null
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -55,15 +57,16 @@ class BrowseViewModel @Inject constructor(
fun onEvent(event: BrowseEvent) { fun onEvent(event: BrowseEvent) {
when (event) { when (event) {
is BrowseEvent.OnLegacyStoragePermissionRequest -> { is BrowseEvent.OnStoragePermissionRequest -> {
event.permissionState.launchPermissionRequest() val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
viewModelScope.launch { val isPermissionGranted = if (legacyStoragePermission) {
for (i in 0 until 100) { event.storagePermissionState.status.isGranted
if (!event.permissionState.status.isGranted) { } else {
delay(100) Environment.isExternalStorageManager()
continue
} }
if (isPermissionGranted) {
_state.update { _state.update {
it.copy( it.copy(
requestPermissionDialog = false requestPermissionDialog = false
@ -71,27 +74,55 @@ class BrowseViewModel @Inject constructor(
} }
event.hideErrorMessage() event.hideErrorMessage()
onEvent(BrowseEvent.OnRefreshList) onEvent(BrowseEvent.OnRefreshList)
break 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
} }
} }
} }
is BrowseEvent.OnStoragePermissionRequest -> { if (!legacyStoragePermission) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val uri = Uri.parse("package:${event.activity.packageName}") val uri = Uri.parse("package:${event.activity.packageName}")
val intent = Intent( val intent = Intent(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
uri uri
) )
event.activity.startActivity(intent)
viewModelScope.launch { if (intent.resolveActivity(event.activity.packageManager) != null) {
for (i in 0 until 20) { event.activity.startActivity(intent)
if (!Environment.isExternalStorageManager()) { } else {
return
}
}
storagePermissionJob?.cancel()
storagePermissionJob = viewModelScope.launch {
while (true) {
val granted = if (legacyStoragePermission) {
event.storagePermissionState.status.isGranted
} else {
Environment.isExternalStorageManager()
}
if (!granted) {
delay(1000) delay(1000)
yield()
continue continue
} }
yield()
_state.update { _state.update {
it.copy( it.copy(
requestPermissionDialog = false requestPermissionDialog = false
@ -103,7 +134,6 @@ class BrowseViewModel @Inject constructor(
} }
} }
} }
}
is BrowseEvent.OnStoragePermissionDismiss -> { is BrowseEvent.OnStoragePermissionDismiss -> {
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
@ -111,6 +141,7 @@ class BrowseViewModel @Inject constructor(
if (!legacyPermission) Environment.isExternalStorageManager() if (!legacyPermission) Environment.isExternalStorageManager()
else event.permissionState.status.isGranted else event.permissionState.status.isGranted
storagePermissionJob?.cancel()
_state.update { _state.update {
it.copy( it.copy(
requestPermissionDialog = false requestPermissionDialog = false
@ -270,6 +301,12 @@ class BrowseViewModel @Inject constructor(
job = viewModelScope.launch(Dispatchers.IO) { job = viewModelScope.launch(Dispatchers.IO) {
delay(500) delay(500)
yield() yield()
onEvent(BrowseEvent.OnSearch)
}
}
is BrowseEvent.OnSearch -> {
viewModelScope.launch(Dispatchers.IO) {
getFilesFromDownloads() getFilesFromDownloads()
} }
} }

View file

@ -1,14 +1,22 @@
package ua.acclorite.book_story.presentation.screens.help 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.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.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.ExperimentalMaterial3Api
import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -16,35 +24,73 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R 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.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.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.HelpAddBooksItem
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpClickMeNoteItem 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.HelpCustomizeApp
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpCustomizeReader 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.HelpEditBook
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpFindBooksItem 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.HelpMoveOrDeleteBooks
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpReadBook 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.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.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( @OptIn(
ExperimentalMaterial3Api::class ExperimentalMaterial3Api::class
) )
@Composable @Composable
fun HelpScreen( private fun HelpScreen(
viewModel: HelpViewModel = hiltViewModel(), state: State<HelpState>,
navigator: Navigator navigator: Navigator,
onEvent: (HelpEvent) -> Unit,
onMainEvent: (MainEvent) -> Unit,
onStartEvent: (StartEvent) -> Unit
) { ) {
val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState() val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
Scaffold( Scaffold(
Modifier Modifier
@ -58,7 +104,23 @@ fun HelpScreen(
Text(stringResource(id = R.string.help_screen)) Text(stringResource(id = R.string.help_screen))
}, },
navigationIcon = { navigationIcon = {
if (!state.value.fromStart) {
GoBackButton(navigator = navigator) 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, scrollBehavior = scrollState.first,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
@ -66,14 +128,37 @@ fun HelpScreen(
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer 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 -> ) { paddingValues ->
LazyColumn( LazyColumn(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(top = paddingValues.calculateTopPadding(), start = 18.dp, end = 18.dp) .padding(paddingValues)
.imePadding(), .imePadding(),
state = scrollState.second state = scrollState.second,
contentPadding = PaddingValues(horizontal = 18.dp)
) { ) {
item { item {
Spacer( Spacer(
@ -84,7 +169,10 @@ fun HelpScreen(
} }
item { item {
HelpClickMeNoteItem(viewModel = viewModel) HelpClickMeNoteItem(
state = state,
onEvent = onEvent
)
} }
item { item {
@ -95,54 +183,66 @@ fun HelpScreen(
) )
} }
item { item {
HelpFindBooksItem(viewModel = viewModel) HelpFindBooksItem(
state = state,
onEvent = onEvent
)
} }
item { item {
HelpAddBooksItem(navigator = navigator, viewModel = viewModel) HelpAddBooksItem(
state = state,
onEvent = onEvent
)
} }
item { item {
HelpCustomizeApp( HelpCustomizeApp(
navigator = navigator, state = state,
viewModel = viewModel onEvent = onEvent
) )
} }
item { item {
HelpMoveOrDeleteBooks( HelpMoveOrDeleteBooks(
navigator = navigator, state = state,
viewModel = viewModel onEvent = onEvent
) )
} }
item { item {
HelpEditBook( HelpEditBook(
navigator = navigator, state = state,
viewModel = viewModel onEvent = onEvent
) )
} }
item { item {
HelpReadBook( HelpReadBook(
navigator = navigator, state = state,
viewModel = viewModel onEvent = onEvent
) )
} }
item { item {
HelpCustomizeReader( HelpCustomizeReader(
navigator = navigator, state = state,
viewModel = viewModel onEvent = onEvent
) )
} }
item { item {
HelpUpdateBook( HelpUpdateBook(
navigator = navigator, state = state,
viewModel = viewModel onEvent = onEvent
)
}
item {
HelpManageHistory(
state = state,
onEvent = onEvent
) )
} }

View file

@ -7,6 +7,9 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
/**
* Help clickable annotation. Mainly used for highlighting screens.
*/
@Composable @Composable
fun AnnotatedString.Builder.HelpAnnotation( fun AnnotatedString.Builder.HelpAnnotation(
tag: String, tag: String,

View file

@ -29,6 +29,9 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.ui.SlidingTransition import ua.acclorite.book_story.presentation.ui.SlidingTransition
/**
* Help Item. Can be Expanded or Collapsed.
*/
@Composable @Composable
fun LazyItemScope.HelpItem( fun LazyItemScope.HelpItem(
title: String, title: String,

View file

@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R 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.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation 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.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 @Composable
fun LazyItemScope.HelpAddBooksItem(navigator: Navigator, viewModel: HelpViewModel) { fun LazyItemScope.HelpAddBooksItem(
val state by viewModel.state.collectAsState() state: State<HelpState>,
onEvent: (HelpEvent) -> Unit
) {
val navigator = LocalNavigator.current
HelpItem( HelpItem(
title = stringResource(id = R.string.help_title_how_to_add_books), title = stringResource(id = R.string.help_title_how_to_add_books),
@ -35,13 +38,15 @@ fun LazyItemScope.HelpAddBooksItem(navigator: Navigator, viewModel: HelpViewMode
append(".") append(".")
}, },
tags = listOf("browse", "library"), tags = listOf("browse", "library"),
shouldShowDescription = state.showHelpItem2, shouldShowDescription = state.value.showHelpItem2,
onTitleClick = { onTitleClick = {
viewModel.onUpdate { onEvent(
HelpEvent.OnUpdateState {
it.copy( it.copy(
showHelpItem2 = !it.showHelpItem2 showHelpItem2 = !it.showHelpItem2
) )
} }
)
}, },
onTagClick = { tag -> onTagClick = { tag ->
when (tag) { when (tag) {

View file

@ -3,19 +3,21 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R 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.HelpAnnotation
import ua.acclorite.book_story.presentation.screens.help.components.HelpClickableNote 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 @Composable
fun LazyItemScope.HelpClickMeNoteItem(viewModel: HelpViewModel) { fun LazyItemScope.HelpClickMeNoteItem(
val state by viewModel.state.collectAsState() state: State<HelpState>,
onEvent: (HelpEvent) -> Unit
) {
val context = LocalContext.current val context = LocalContext.current
HelpClickableNote( HelpClickableNote(
@ -29,13 +31,15 @@ fun LazyItemScope.HelpClickMeNoteItem(viewModel: HelpViewModel) {
append(" " + stringResource(id = R.string.help_clickable_note_3)) append(" " + stringResource(id = R.string.help_clickable_note_3))
}, },
tags = listOf("note"), tags = listOf("note"),
isNoteFullyShown = state.showNote1, isNoteFullyShown = state.value.showNote1,
onIconClick = { onIconClick = {
viewModel.onUpdate { onEvent(
HelpEvent.OnUpdateState {
it.copy( it.copy(
showNote1 = !it.showNote1 showNote1 = !it.showNote1
) )
} }
)
}, },
onTagClick = { tag -> onTagClick = { tag ->
when (tag) { when (tag) {

View file

@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R 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.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation 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.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 @Composable
fun LazyItemScope.HelpCustomizeApp(viewModel: HelpViewModel, navigator: Navigator) { fun LazyItemScope.HelpCustomizeApp(
val state by viewModel.state.collectAsState() state: State<HelpState>,
onEvent: (HelpEvent) -> Unit
) {
val navigator = LocalNavigator.current
HelpItem( HelpItem(
title = stringResource(id = R.string.help_title_how_to_customize_app), 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)) append(stringResource(id = R.string.help_desc_how_to_customize_app_3))
}, },
tags = listOf("settings"), tags = listOf("settings"),
shouldShowDescription = state.showHelpItem3, shouldShowDescription = state.value.showHelpItem3,
onTitleClick = { onTitleClick = {
viewModel.onUpdate { onEvent(
HelpEvent.OnUpdateState {
it.copy( it.copy(
showHelpItem3 = !it.showHelpItem3 showHelpItem3 = !it.showHelpItem3
) )
} }
)
}, },
onTagClick = { tag -> onTagClick = { tag ->
when (tag) { when (tag) {

View file

@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R 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.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation 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.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 @Composable
fun LazyItemScope.HelpCustomizeReader(viewModel: HelpViewModel, navigator: Navigator) { fun LazyItemScope.HelpCustomizeReader(
val state by viewModel.state.collectAsState() state: State<HelpState>,
onEvent: (HelpEvent) -> Unit
) {
val navigator = LocalNavigator.current
HelpItem( HelpItem(
title = stringResource(id = R.string.help_title_how_to_customize_reader), 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)) append(stringResource(id = R.string.help_desc_how_to_customize_reader_3))
}, },
tags = listOf("settings"), tags = listOf("settings"),
shouldShowDescription = state.showHelpItem7, shouldShowDescription = state.value.showHelpItem7,
onTitleClick = { onTitleClick = {
viewModel.onUpdate { onEvent(
HelpEvent.OnUpdateState {
it.copy( it.copy(
showHelpItem7 = !it.showHelpItem7 showHelpItem7 = !it.showHelpItem7
) )
} }
)
}, },
onTagClick = { tag -> onTagClick = { tag ->
when (tag) { when (tag) {

View file

@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R 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.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation 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.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 @Composable
fun LazyItemScope.HelpEditBook(viewModel: HelpViewModel, navigator: Navigator) { fun LazyItemScope.HelpEditBook(
val state by viewModel.state.collectAsState() state: State<HelpState>,
onEvent: (HelpEvent) -> Unit
) {
val navigator = LocalNavigator.current
HelpItem( HelpItem(
title = stringResource(id = R.string.help_title_how_to_edit_book), 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)) append(stringResource(id = R.string.help_desc_how_to_edit_book_3))
}, },
tags = listOf("library"), tags = listOf("library"),
shouldShowDescription = state.showHelpItem5, shouldShowDescription = state.value.showHelpItem5,
onTitleClick = { onTitleClick = {
viewModel.onUpdate { onEvent(
HelpEvent.OnUpdateState {
it.copy( it.copy(
showHelpItem5 = !it.showHelpItem5 showHelpItem5 = !it.showHelpItem5
) )
} }
)
}, },
onTagClick = { tag -> onTagClick = { tag ->
when (tag) { when (tag) {

View file

@ -16,8 +16,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource 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.HelpAnnotation
import ua.acclorite.book_story.presentation.screens.help.components.HelpItem 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.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 import ua.acclorite.book_story.presentation.ui.SlidingTransition
@Composable @Composable
fun LazyItemScope.HelpFindBooksItem(viewModel: HelpViewModel) { fun LazyItemScope.HelpFindBooksItem(
val state by viewModel.state.collectAsState() state: State<HelpState>,
onEvent: (HelpEvent) -> Unit
) {
val context = LocalContext.current val context = LocalContext.current
val sites = stringArrayResource(id = R.array.book_sites) 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)) append(stringResource(id = R.string.help_desc_how_to_find_books))
}, },
tags = emptyList(), tags = emptyList(),
shouldShowDescription = state.showHelpItem1, shouldShowDescription = state.value.showHelpItem1,
onTitleClick = { onTitleClick = {
viewModel.onUpdate { onEvent(
HelpEvent.OnUpdateState {
it.copy( it.copy(
showHelpItem1 = !it.showHelpItem1 showHelpItem1 = !it.showHelpItem1
) )
} }
)
}, },
customContent = { customContent = {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
OutlinedTextField( OutlinedTextField(
value = state.textFieldValue, value = state.value.textFieldValue,
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
onValueChange = { value -> onValueChange = { value ->
viewModel.onUpdate { onEvent(
HelpEvent.OnUpdateState {
it.copy( it.copy(
textFieldValue = value, textFieldValue = value,
showError = false showError = false
) )
} }
)
}, },
label = { label = {
Text(text = stringResource(id = R.string.help_field_placeholder)) Text(text = stringResource(id = R.string.help_field_placeholder))
}, },
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onSearch = { onSearch = {
viewModel.onEvent( onEvent(
HelpEvent.OnSearchInWeb( HelpEvent.OnSearchInWeb(
context = context, context = context,
noAppsFound = { noAppsFound = {
@ -110,14 +115,14 @@ fun LazyItemScope.HelpFindBooksItem(viewModel: HelpViewModel) {
icon = Icons.Default.Search, icon = Icons.Default.Search,
contentDescription = R.string.search_content_desc, contentDescription = R.string.search_content_desc,
disableOnClick = false, disableOnClick = false,
enabled = !state.showError, enabled = !state.value.showError,
color = if (state.showError) { color = if (state.value.showError) {
MaterialTheme.colorScheme.outline MaterialTheme.colorScheme.outline
} else { } else {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
} }
) { ) {
viewModel.onEvent( onEvent(
HelpEvent.OnSearchInWeb( HelpEvent.OnSearchInWeb(
context = context, context = context,
noAppsFound = { noAppsFound = {
@ -135,14 +140,14 @@ fun LazyItemScope.HelpFindBooksItem(viewModel: HelpViewModel) {
Column { Column {
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
Box { Box {
SlidingTransition(visible = state.showError) { SlidingTransition(visible = state.value.showError) {
Text( Text(
text = stringResource(id = R.string.help_field_error), text = stringResource(id = R.string.help_field_error),
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
} }
SlidingTransition(visible = !state.showError) { SlidingTransition(visible = !state.value.showError) {
ClickableText( ClickableText(
text = text, text = text,
style = MaterialTheme.typography.bodySmall.copy( style = MaterialTheme.typography.bodySmall.copy(
@ -155,7 +160,7 @@ fun LazyItemScope.HelpFindBooksItem(viewModel: HelpViewModel) {
start = offset, start = offset,
end = offset end = offset
).firstOrNull()?.let { ).firstOrNull()?.let {
viewModel.onEvent( onEvent(
HelpEvent.OnNavigateToBrowserPage( HelpEvent.OnNavigateToBrowserPage(
page = tag.substringAfterLast("|"), page = tag.substringAfterLast("|"),
context = context, context = context,

View file

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

View file

@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R 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.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation 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.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 @Composable
fun LazyItemScope.HelpMoveOrDeleteBooks(viewModel: HelpViewModel, navigator: Navigator) { fun LazyItemScope.HelpMoveOrDeleteBooks(
val state by viewModel.state.collectAsState() state: State<HelpState>,
onEvent: (HelpEvent) -> Unit
) {
val navigator = LocalNavigator.current
HelpItem( HelpItem(
title = stringResource(id = R.string.help_title_how_to_move_or_delete_books), 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)) append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_3))
}, },
tags = listOf("library"), tags = listOf("library"),
shouldShowDescription = state.showHelpItem4, shouldShowDescription = state.value.showHelpItem4,
onTitleClick = { onTitleClick = {
viewModel.onUpdate { onEvent(
HelpEvent.OnUpdateState {
it.copy( it.copy(
showHelpItem4 = !it.showHelpItem4 showHelpItem4 = !it.showHelpItem4
) )
} }
)
}, },
onTagClick = { tag -> onTagClick = { tag ->
when (tag) { when (tag) {

View file

@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R 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.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation 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.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 @Composable
fun LazyItemScope.HelpReadBook(viewModel: HelpViewModel, navigator: Navigator) { fun LazyItemScope.HelpReadBook(
val state by viewModel.state.collectAsState() state: State<HelpState>,
onEvent: (HelpEvent) -> Unit
) {
val navigator = LocalNavigator.current
HelpItem( HelpItem(
title = stringResource(id = R.string.help_title_how_to_read_book), 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)) append(stringResource(id = R.string.help_desc_how_to_read_book_3))
}, },
tags = listOf("library"), tags = listOf("library"),
shouldShowDescription = state.showHelpItem6, shouldShowDescription = state.value.showHelpItem6,
onTitleClick = { onTitleClick = {
viewModel.onUpdate { onEvent(
HelpEvent.OnUpdateState {
it.copy( it.copy(
showHelpItem6 = !it.showHelpItem6 showHelpItem6 = !it.showHelpItem6
) )
} }
)
}, },
onTagClick = { tag -> onTagClick = { tag ->
when (tag) { when (tag) {

View file

@ -2,20 +2,23 @@ package ua.acclorite.book_story.presentation.screens.help.components.items
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import ua.acclorite.book_story.R 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.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation 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.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 @Composable
fun LazyItemScope.HelpUpdateBook(viewModel: HelpViewModel, navigator: Navigator) { fun LazyItemScope.HelpUpdateBook(
val state by viewModel.state.collectAsState() state: State<HelpState>,
onEvent: (HelpEvent) -> Unit
) {
val navigator = LocalNavigator.current
HelpItem( HelpItem(
title = stringResource(id = R.string.help_title_how_to_update_book), 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)) append(stringResource(id = R.string.help_desc_how_to_update_book_3))
}, },
tags = listOf("library"), tags = listOf("library"),
shouldShowDescription = state.showHelpItem8, shouldShowDescription = state.value.showHelpItem8,
onTitleClick = { onTitleClick = {
viewModel.onUpdate { onEvent(
HelpEvent.OnUpdateState {
it.copy( it.copy(
showHelpItem8 = !it.showHelpItem8 showHelpItem8 = !it.showHelpItem8
) )
} }
)
}, },
onTagClick = { tag -> onTagClick = { tag ->
when (tag) { when (tag) {

View file

@ -1,8 +1,13 @@
package ua.acclorite.book_story.presentation.screens.help.data package ua.acclorite.book_story.presentation.screens.help.data
import android.content.Context import android.content.Context
import androidx.compose.runtime.Immutable
@Immutable
sealed class HelpEvent { sealed class HelpEvent {
data class OnUpdateState(val block: (HelpState) -> HelpState) : HelpEvent()
data class OnSearchInWeb( data class OnSearchInWeb(
val context: Context, val context: Context,
val noAppsFound: () -> Unit val noAppsFound: () -> Unit

View file

@ -1,6 +1,11 @@
package ua.acclorite.book_story.presentation.screens.help.data package ua.acclorite.book_story.presentation.screens.help.data
import androidx.compose.runtime.Immutable
@Immutable
data class HelpState( data class HelpState(
val fromStart: Boolean = false,
val showNote1: Boolean = true, val showNote1: Boolean = true,
val showHelpItem1: Boolean = false, val showHelpItem1: Boolean = false,
@ -14,4 +19,5 @@ data class HelpState(
val showHelpItem6: Boolean = false, val showHelpItem6: Boolean = false,
val showHelpItem7: Boolean = false, val showHelpItem7: Boolean = false,
val showHelpItem8: Boolean = false, val showHelpItem8: Boolean = false,
val showHelpItem9: Boolean = false,
) )

View file

@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ua.acclorite.book_story.presentation.data.Navigator
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -65,12 +66,24 @@ class HelpViewModel @Inject constructor(
event.noAppsFound() event.noAppsFound()
} }
} }
is HelpEvent.OnUpdateState -> {
_state.update {
event.block(it)
}
}
} }
} }
fun onUpdate(calculation: (HelpState) -> HelpState) { fun init(navigator: Navigator) {
viewModelScope.launch {
val isFromStart = navigator.retrieveArgument("from_start") as? Boolean ?: false
_state.update { _state.update {
calculation(it) it.copy(
fromStart = isFromStart
)
}
} }
} }
} }

View file

@ -1,20 +1,16 @@
package ua.acclorite.book_story.presentation.screens.history package ua.acclorite.book_story.presentation.screens.history
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
@ -30,68 +26,88 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import 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.dp
import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.CustomIconButton 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.CustomSnackbar
import ua.acclorite.book_story.presentation.components.MoreDropDown import ua.acclorite.book_story.presentation.components.MoreDropDown
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
import ua.acclorite.book_story.presentation.data.Argument 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.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.history.components.HistoryDeleteWholeHistoryDialog import ua.acclorite.book_story.presentation.screens.history.components.HistoryDeleteWholeHistoryDialog
import ua.acclorite.book_story.presentation.screens.history.components.HistoryItem import ua.acclorite.book_story.presentation.screens.history.components.HistoryItem
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryState
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DefaultTransition import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions 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( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalMaterialApi::class ExperimentalMaterialApi::class
) )
@Composable @Composable
fun HistoryScreen( private fun HistoryScreen(
viewModel: HistoryViewModel, state: State<HistoryState>,
libraryViewModel: LibraryViewModel, navigator: Navigator,
navigator: Navigator onEvent: (HistoryEvent) -> Unit,
onLibraryEvent: (LibraryEvent) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val state by viewModel.state.collectAsState()
val refreshState = rememberPullRefreshState( val refreshState = rememberPullRefreshState(
refreshing = state.isRefreshing, refreshing = state.value.isRefreshing,
onRefresh = { onRefresh = {
viewModel.onEvent(HistoryEvent.OnRefreshList) onEvent(HistoryEvent.OnRefreshList)
} }
) )
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val snackbarState = remember { SnackbarHostState() } val snackbarState = remember { SnackbarHostState() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.onEvent(HistoryEvent.OnUpdateScrollOffset) onEvent(HistoryEvent.OnUpdateScrollOffset)
} }
if (state.showDeleteWholeHistoryDialog) { if (state.value.showDeleteWholeHistoryDialog) {
HistoryDeleteWholeHistoryDialog(viewModel = viewModel, libraryViewModel = libraryViewModel) HistoryDeleteWholeHistoryDialog(
onEvent = onEvent,
onLibraryLoadEvent = onLibraryEvent
)
} }
Scaffold( Scaffold(
@ -102,9 +118,9 @@ fun HistoryScreen(
topBar = { topBar = {
AnimatedTopAppBar( AnimatedTopAppBar(
scrollBehavior = null, scrollBehavior = null,
isTopBarScrolled = state.listState.canScrollBackward, isTopBarScrolled = state.value.listState.canScrollBackward,
content1Visibility = !state.showSearch, content1Visibility = !state.value.showSearch,
content1NavigationIcon = {}, content1NavigationIcon = {},
content1Title = { content1Title = {
Text( Text(
@ -117,75 +133,54 @@ fun HistoryScreen(
contentDescription = R.string.search_content_desc, contentDescription = R.string.search_content_desc,
disableOnClick = true, disableOnClick = true,
) { ) {
viewModel.onEvent(HistoryEvent.OnSearchShowHide) onEvent(HistoryEvent.OnSearchShowHide)
} }
CustomIconButton( CustomIconButton(
icon = Icons.Outlined.DeleteSweep, icon = Icons.Outlined.DeleteSweep,
contentDescription = R.string.delete_whole_history_content_desc, contentDescription = R.string.delete_whole_history_content_desc,
disableOnClick = false, disableOnClick = false,
enabled = !state.isLoading enabled = !state.value.isLoading
&& !state.isRefreshing && !state.value.isRefreshing
&& state.history.isNotEmpty() && state.value.history.isNotEmpty()
&& !state.showDeleteWholeHistoryDialog && !state.value.showDeleteWholeHistoryDialog
) { ) {
viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog) onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog)
} }
MoreDropDown(navigator = navigator) MoreDropDown()
}, },
content2Visibility = state.showSearch, content2Visibility = state.value.showSearch,
content2NavigationIcon = { content2NavigationIcon = {
CustomIconButton( CustomIconButton(
icon = Icons.AutoMirrored.Default.ArrowBack, icon = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = R.string.exit_search_content_desc, contentDescription = R.string.exit_search_content_desc,
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent(HistoryEvent.OnSearchShowHide) onEvent(HistoryEvent.OnSearchShowHide)
} }
}, },
content2Title = { content2Title = {
BasicTextField( CustomSearchTextField(
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
),
modifier = Modifier modifier = Modifier
.focusRequester(focusRequester) .focusRequester(focusRequester)
.onGloballyPositioned { .onGloballyPositioned {
viewModel.onEvent(HistoryEvent.OnRequestFocus(focusRequester)) onEvent(HistoryEvent.OnRequestFocus(focusRequester))
}, },
onValueChange = { query = state.value.searchQuery,
viewModel.onEvent(HistoryEvent.OnSearchQueryChange(it)) onQueryChange = {
onEvent(HistoryEvent.OnSearchQueryChange(it))
}, },
keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words), onSearch = {
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) onEvent(HistoryEvent.OnSearch)
) { innerText -> },
Box( placeholder = stringResource(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.CenterStart
) {
if (state.searchQuery.isEmpty()) {
Text(
stringResource(
id = R.string.search_query, id = R.string.search_query,
stringResource(id = R.string.history) stringResource(id = R.string.history)
),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} )
innerText()
}
}
}, },
content2Actions = { content2Actions = {
MoreDropDown(navigator = navigator) MoreDropDown()
}, },
content3Visibility = false content3Visibility = false
) )
@ -199,14 +194,14 @@ fun HistoryScreen(
.fillMaxSize() .fillMaxSize()
.padding(top = paddingValues.calculateTopPadding()) .padding(top = paddingValues.calculateTopPadding())
) { ) {
DefaultTransition(visible = !state.isLoading) { DefaultTransition(visible = !state.value.isLoading) {
LazyColumn( LazyColumn(
Modifier Modifier
.fillMaxSize(), .fillMaxSize(),
state = state.listState, state = state.value.listState,
contentPadding = PaddingValues(vertical = 12.dp) contentPadding = PaddingValues(vertical = 12.dp)
) { ) {
state.history.forEachIndexed { index, groupedHistory -> state.value.history.forEachIndexed { index, groupedHistory ->
item(key = groupedHistory.title) { item(key = groupedHistory.title) {
if (index > 0) { if (index > 0) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@ -231,7 +226,7 @@ fun HistoryScreen(
HistoryItem( HistoryItem(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
history = it, history = it,
isOnClickEnabled = !state.isRefreshing, isOnClickEnabled = !state.value.isRefreshing,
onBodyClick = { onBodyClick = {
navigator.navigate( navigator.navigate(
Screen.BOOK_INFO, Screen.BOOK_INFO,
@ -240,22 +235,22 @@ fun HistoryScreen(
) )
}, },
onTitleClick = { onTitleClick = {
viewModel.onEvent( onEvent(
HistoryEvent.OnNavigateToReaderScreen( HistoryEvent.OnNavigateToReaderScreen(
navigator, navigator,
it.book!! it.book!!
) )
) )
}, },
isDeleteEnabled = !state.isRefreshing, isDeleteEnabled = !state.value.isRefreshing,
onDeleteClick = { onDeleteClick = {
viewModel.onEvent( onEvent(
HistoryEvent.OnDeleteHistoryElement( HistoryEvent.OnDeleteHistoryElement(
historyToDelete = it, historyToDelete = it,
snackbarState = snackbarState, snackbarState = snackbarState,
context = context, context = context,
refreshList = { refreshList = {
libraryViewModel.onEvent( onLibraryEvent(
LibraryEvent.OnLoadList LibraryEvent.OnLoadList
) )
} }
@ -268,10 +263,10 @@ fun HistoryScreen(
} }
} }
AnimatedVisibility( CustomAnimatedVisibility(
visible = !state.isLoading visible = !state.value.isLoading
&& state.history.isEmpty() && state.value.history.isEmpty()
&& !state.isRefreshing, && !state.value.isRefreshing,
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn, enter = Transitions.DefaultTransitionIn,
exit = fadeOut(tween(0)) exit = fadeOut(tween(0))
@ -284,7 +279,7 @@ fun HistoryScreen(
} }
PullRefreshIndicator( PullRefreshIndicator(
state.isRefreshing, state.value.isRefreshing,
refreshState, refreshState,
Modifier.align(Alignment.TopCenter), Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.inverseSurface, backgroundColor = MaterialTheme.colorScheme.inverseSurface,
@ -294,8 +289,8 @@ fun HistoryScreen(
} }
BackHandler { BackHandler {
if (state.showSearch) { if (state.value.showSearch) {
viewModel.onEvent(HistoryEvent.OnSearchShowHide) onEvent(HistoryEvent.OnSearchShowHide)
return@BackHandler return@BackHandler
} }

View file

@ -9,17 +9,15 @@ import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
/** /**
* Delete whole history dialog. * Delete whole history dialog.
*/ */
@Composable @Composable
fun HistoryDeleteWholeHistoryDialog( fun HistoryDeleteWholeHistoryDialog(
viewModel: HistoryViewModel, onEvent: (HistoryEvent) -> Unit,
libraryViewModel: LibraryViewModel onLibraryLoadEvent: (LibraryEvent.OnLoadList) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -30,13 +28,13 @@ fun HistoryDeleteWholeHistoryDialog(
id = R.string.delete_history_description id = R.string.delete_history_description
), ),
actionText = stringResource(id = R.string.delete), actionText = stringResource(id = R.string.delete),
onDismiss = { viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog) }, onDismiss = { onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog) },
withDivider = false, withDivider = false,
isActionEnabled = true, isActionEnabled = true,
onAction = { onAction = {
viewModel.onEvent( onEvent(
HistoryEvent.OnDeleteWholeHistory { HistoryEvent.OnDeleteWholeHistory {
libraryViewModel.onEvent(LibraryEvent.OnLoadList) onLibraryLoadEvent(LibraryEvent.OnLoadList)
} }
) )
Toast.makeText( Toast.makeText(

View file

@ -2,11 +2,13 @@ package ua.acclorite.book_story.presentation.screens.history.data
import android.content.Context import android.content.Context
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
@Immutable
sealed class HistoryEvent { sealed class HistoryEvent {
data object OnRefreshList : HistoryEvent() data object OnRefreshList : HistoryEvent()
data object OnLoadList : HistoryEvent() data object OnLoadList : HistoryEvent()
@ -20,6 +22,7 @@ sealed class HistoryEvent {
) : HistoryEvent() ) : HistoryEvent()
data class OnSearchQueryChange(val query: String) : HistoryEvent() data class OnSearchQueryChange(val query: String) : HistoryEvent()
data object OnSearch : HistoryEvent()
data object OnSearchShowHide : HistoryEvent() data object OnSearchShowHide : HistoryEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : HistoryEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : HistoryEvent()
data class OnUpdateBook(val book: Book) : HistoryEvent() data class OnUpdateBook(val book: Book) : HistoryEvent()

View file

@ -1,8 +1,10 @@
package ua.acclorite.book_story.presentation.screens.history.data package ua.acclorite.book_story.presentation.screens.history.data
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.GroupedHistory import ua.acclorite.book_story.domain.model.GroupedHistory
@Immutable
data class HistoryState( data class HistoryState(
val history: List<GroupedHistory> = emptyList(), val history: List<GroupedHistory> = emptyList(),
val listState: LazyListState = LazyListState(0, 0), val listState: LazyListState = LazyListState(0, 0),

View file

@ -208,6 +208,12 @@ class HistoryViewModel @Inject constructor(
job = viewModelScope.launch(Dispatchers.IO) { job = viewModelScope.launch(Dispatchers.IO) {
delay(500) delay(500)
yield() yield()
onEvent(HistoryEvent.OnSearch)
}
}
is HistoryEvent.OnSearch -> {
viewModelScope.launch(Dispatchers.IO) {
getHistoryFromDatabase() getHistoryFromDatabase()
} }
} }

View file

@ -3,17 +3,14 @@ package ua.acclorite.book_story.presentation.screens.library
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape 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.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
@ -43,6 +38,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -54,16 +50,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -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.model.Category
import ua.acclorite.book_story.domain.util.Selected import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar 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.CustomIconButton
import ua.acclorite.book_story.presentation.components.CustomSearchTextField
import ua.acclorite.book_story.presentation.components.MoreDropDown import ua.acclorite.book_story.presentation.components.MoreDropDown
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
import ua.acclorite.book_story.presentation.data.Argument 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.Navigator
import ua.acclorite.book_story.presentation.data.Screen 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.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.components.LibraryBookItem 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.LibraryTabRow
import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryDeleteDialog 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.components.dialog.LibraryMoveDialog
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryState
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DefaultTransition import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions 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( @OptIn(
ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalMaterialApi::class ExperimentalMaterialApi::class
) )
@Composable @Composable
fun LibraryScreen( private fun LibraryScreen(
viewModel: LibraryViewModel, state: State<LibraryState>,
browseViewModel: BrowseViewModel, navigator: Navigator,
historyViewModel: HistoryViewModel, onEvent: (LibraryEvent) -> Unit,
navigator: Navigator onHistoryEvent: (HistoryEvent) -> Unit,
onBrowseEvent: (BrowseEvent) -> Unit
) { ) {
val state by viewModel.state.collectAsState()
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
initialPage = state.currentPage, initialPage = state.value.currentPage,
pageCount = { Category.entries.size } pageCount = { Category.entries.size }
) )
var isScrollInProgress by remember { mutableStateOf(false) } var isScrollInProgress by remember { mutableStateOf(false) }
val refreshState = rememberPullRefreshState( val refreshState = rememberPullRefreshState(
refreshing = state.isRefreshing, refreshing = state.value.isRefreshing,
onRefresh = { onRefresh = {
viewModel.onEvent(LibraryEvent.OnRefreshList) onEvent(LibraryEvent.OnRefreshList)
} }
) )
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.onEvent(LibraryEvent.OnScrollToPage(state.currentPage, pagerState)) onEvent(LibraryEvent.OnScrollToPage(state.value.currentPage, pagerState))
} }
LaunchedEffect(pagerState.currentPage) { LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage != state.currentPage) { if (pagerState.currentPage != state.value.currentPage) {
viewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage)) onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage))
} }
} }
if (state.showMoveDialog) { if (state.value.showMoveDialog) {
LibraryMoveDialog( LibraryMoveDialog(
viewModel = viewModel, state = state,
historyViewModel = historyViewModel, onEvent = onEvent,
onHistoryLoadEvent = onHistoryEvent,
pagerState = pagerState pagerState = pagerState
) )
} }
if (state.showDeleteDialog) { if (state.value.showDeleteDialog) {
LibraryDeleteDialog( LibraryDeleteDialog(
viewModel = viewModel, state = state,
browseViewModel = browseViewModel, onEvent = onEvent,
historyViewModel = historyViewModel onBrowseLoadEvent = onBrowseEvent,
onHistoryLoadEvent = onHistoryEvent
) )
} }
@ -146,16 +166,16 @@ fun LibraryScreen(
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
AnimatedTopAppBar( AnimatedTopAppBar(
scrollBehavior = null, scrollBehavior = null,
isTopBarScrolled = state.hasSelectedItems, isTopBarScrolled = state.value.hasSelectedItems,
content1Visibility = !state.hasSelectedItems && !state.showSearch, content1Visibility = !state.value.hasSelectedItems && !state.value.showSearch,
content1NavigationIcon = {}, content1NavigationIcon = {},
content1Title = { content1Title = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(id = R.string.library_screen)) Text(stringResource(id = R.string.library_screen))
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
Text( Text(
text = state.books.size.toString(), text = state.value.books.size.toString(),
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(14.dp)) .clip(RoundedCornerShape(14.dp))
.background(MaterialTheme.colorScheme.surfaceContainer) .background(MaterialTheme.colorScheme.surfaceContainer)
@ -171,26 +191,26 @@ fun LibraryScreen(
contentDescription = R.string.search_content_desc, contentDescription = R.string.search_content_desc,
disableOnClick = true, disableOnClick = true,
) { ) {
viewModel.onEvent(LibraryEvent.OnSearchShowHide) onEvent(LibraryEvent.OnSearchShowHide)
} }
MoreDropDown(navigator = navigator) MoreDropDown()
}, },
content2Visibility = state.hasSelectedItems, content2Visibility = state.value.hasSelectedItems,
content2NavigationIcon = { content2NavigationIcon = {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Clear, icon = Icons.Default.Clear,
contentDescription = R.string.clear_selected_items_content_desc, contentDescription = R.string.clear_selected_items_content_desc,
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent(LibraryEvent.OnClearSelectedBooks) onEvent(LibraryEvent.OnClearSelectedBooks)
} }
}, },
content2Title = { content2Title = {
Text( Text(
stringResource( stringResource(
id = R.string.selected_items_count_query, id = R.string.selected_items_count_query,
state.selectedItemsCount.coerceAtLeast(1) state.value.selectedItemsCount.coerceAtLeast(1)
), ),
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1 maxLines = 1
@ -200,86 +220,65 @@ fun LibraryScreen(
CustomIconButton( CustomIconButton(
icon = Icons.AutoMirrored.Outlined.DriveFileMove, icon = Icons.AutoMirrored.Outlined.DriveFileMove,
contentDescription = R.string.move_books_content_desc, contentDescription = R.string.move_books_content_desc,
enabled = !state.isLoading enabled = !state.value.isLoading
&& !state.isRefreshing && !state.value.isRefreshing
&& !state.showMoveDialog, && !state.value.showMoveDialog,
disableOnClick = false, disableOnClick = false,
) { ) {
viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog) onEvent(LibraryEvent.OnShowHideMoveDialog)
} }
CustomIconButton( CustomIconButton(
icon = Icons.Outlined.Delete, icon = Icons.Outlined.Delete,
contentDescription = R.string.delete_books_content_desc, contentDescription = R.string.delete_books_content_desc,
enabled = !state.isLoading enabled = !state.value.isLoading
&& !state.isRefreshing && !state.value.isRefreshing
&& !state.showDeleteDialog, && !state.value.showDeleteDialog,
disableOnClick = false disableOnClick = false
) { ) {
viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog) onEvent(LibraryEvent.OnShowHideDeleteDialog)
} }
}, },
content3Visibility = state.showSearch && !state.hasSelectedItems, content3Visibility = state.value.showSearch && !state.value.hasSelectedItems,
content3NavigationIcon = { content3NavigationIcon = {
CustomIconButton( CustomIconButton(
icon = Icons.AutoMirrored.Default.ArrowBack, icon = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = R.string.exit_search_content_desc, contentDescription = R.string.exit_search_content_desc,
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent( onEvent(
LibraryEvent.OnSearchShowHide LibraryEvent.OnSearchShowHide
) )
} }
}, },
content3Title = { content3Title = {
BasicTextField( CustomSearchTextField(
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
),
modifier = Modifier modifier = Modifier
.focusRequester(focusRequester) .focusRequester(focusRequester)
.onGloballyPositioned { .onGloballyPositioned {
viewModel.onEvent(LibraryEvent.OnRequestFocus(focusRequester)) onEvent(LibraryEvent.OnRequestFocus(focusRequester))
}, },
onValueChange = { query = state.value.searchQuery,
viewModel.onEvent(LibraryEvent.OnSearchQueryChange(it)) onQueryChange = {
onEvent(LibraryEvent.OnSearchQueryChange(it))
}, },
keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words), onSearch = {
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) onEvent(LibraryEvent.OnSearch)
) { innerText -> },
Box( placeholder = stringResource(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.CenterStart
) {
if (state.searchQuery.isEmpty()) {
Text(
stringResource(
id = R.string.search_query, id = R.string.search_query,
stringResource(id = R.string.books) stringResource(id = R.string.books)
),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} )
innerText()
}
}
}, },
content3Actions = { content3Actions = {
MoreDropDown(navigator = navigator) MoreDropDown()
} }
) )
LibraryTabRow( LibraryTabRow(
viewModel = viewModel, onEvent = onEvent,
books = state.categorizedBooks, books = state.value.categorizedBooks,
pagerState = pagerState pagerState = pagerState
) )
} }
@ -295,8 +294,8 @@ fun LibraryScreen(
userScrollEnabled = !isScrollInProgress userScrollEnabled = !isScrollInProgress
) { index -> ) { index ->
val category = remember { Category.entries[index] } val category = remember { Category.entries[index] }
val books = remember(state.categorizedBooks) { val books = remember(state.value.categorizedBooks) {
state.categorizedBooks.find { state.value.categorizedBooks.find {
it.category == category it.category == category
}?.books?.sortedWith( }?.books?.sortedWith(
compareByDescending<Pair<Book, Selected>> { it.first.lastOpened } compareByDescending<Pair<Book, Selected>> { it.first.lastOpened }
@ -307,14 +306,14 @@ fun LibraryScreen(
LaunchedEffect(listState.isScrollInProgress, pagerState.currentPage) { LaunchedEffect(listState.isScrollInProgress, pagerState.currentPage) {
if (listState.isScrollInProgress != isScrollInProgress if (listState.isScrollInProgress != isScrollInProgress
&& pagerState.currentPage == state.currentPage && pagerState.currentPage == state.value.currentPage
) { ) {
isScrollInProgress = listState.isScrollInProgress isScrollInProgress = listState.isScrollInProgress
} }
} }
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
DefaultTransition(visible = !state.isLoading) { DefaultTransition(visible = !state.value.isLoading) {
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Adaptive(120.dp), columns = GridCells.Adaptive(120.dp),
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -327,10 +326,13 @@ fun LibraryScreen(
) { ) {
LibraryBookItem( LibraryBookItem(
book = it, book = it,
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(
fadeInSpec = null,
fadeOutSpec = null
),
onCoverImageClick = { onCoverImageClick = {
if (state.hasSelectedItems) { if (state.value.hasSelectedItems) {
viewModel.onEvent(LibraryEvent.OnSelectBook(it)) onEvent(LibraryEvent.OnSelectBook(it))
} else { } else {
navigator.navigate( navigator.navigate(
Screen.BOOK_INFO, Screen.BOOK_INFO,
@ -341,11 +343,11 @@ fun LibraryScreen(
}, },
onLongClick = { onLongClick = {
if (!it.second) { if (!it.second) {
viewModel.onEvent(LibraryEvent.OnSelectBook(it, true)) onEvent(LibraryEvent.OnSelectBook(it, true))
} }
}, },
onButtonClick = { onButtonClick = {
viewModel.onEvent( onEvent(
LibraryEvent.OnNavigateToReaderScreen( LibraryEvent.OnNavigateToReaderScreen(
navigator, navigator,
it.first it.first
@ -357,9 +359,9 @@ fun LibraryScreen(
} }
} }
AnimatedVisibility( CustomAnimatedVisibility(
visible = !state.isLoading visible = !state.value.isLoading
&& !state.isRefreshing && !state.value.isRefreshing
&& books.isEmpty(), && books.isEmpty(),
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn, enter = Transitions.DefaultTransitionIn,
@ -378,7 +380,7 @@ fun LibraryScreen(
} }
PullRefreshIndicator( PullRefreshIndicator(
state.isRefreshing, state.value.isRefreshing,
refreshState, refreshState,
Modifier.align(Alignment.TopCenter), Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.inverseSurface, backgroundColor = MaterialTheme.colorScheme.inverseSurface,
@ -391,18 +393,18 @@ fun LibraryScreen(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var shouldExit = remember { false } var shouldExit = remember { false }
BackHandler { BackHandler {
if (state.hasSelectedItems) { if (state.value.hasSelectedItems) {
viewModel.onEvent(LibraryEvent.OnClearSelectedBooks) onEvent(LibraryEvent.OnClearSelectedBooks)
return@BackHandler return@BackHandler
} }
if (state.showSearch) { if (state.value.showSearch) {
viewModel.onEvent(LibraryEvent.OnSearchShowHide) onEvent(LibraryEvent.OnSearchShowHide)
return@BackHandler return@BackHandler
} }
if (state.currentPage > 0) { if (state.value.currentPage > 0) {
viewModel.onEvent(LibraryEvent.OnScrollToPage(0, pagerState)) onEvent(LibraryEvent.OnScrollToPage(0, pagerState))
return@BackHandler return@BackHandler
} }

View file

@ -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.CategorizedBooks
import ua.acclorite.book_story.domain.model.Category 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.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
/** /**
* Tab row, either scrollable or static, depends on screen width. * 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LibraryTabRow( fun LibraryTabRow(
viewModel: LibraryViewModel, onEvent: (LibraryEvent) -> Unit,
books: List<CategorizedBooks>, books: List<CategorizedBooks>,
pagerState: PagerState pagerState: PagerState
) { ) {
@ -99,7 +98,7 @@ fun LibraryTabRow(
modifier = Modifier, modifier = Modifier,
selected = pagerState.currentPage == index, selected = pagerState.currentPage == index,
onClick = { onClick = {
viewModel.onEvent(LibraryEvent.OnScrollToPage(index, pagerState)) onEvent(LibraryEvent.OnScrollToPage(index, pagerState))
}, },
selectedContentColor = MaterialTheme.colorScheme.primary, selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,

View file

@ -4,29 +4,26 @@ import android.widget.Toast
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.DeleteOutline
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryState
/** /**
* Delete dialog. Deletes all selected books. * Delete dialog. Deletes all selected books.
*/ */
@Composable @Composable
fun LibraryDeleteDialog( fun LibraryDeleteDialog(
viewModel: LibraryViewModel, state: State<LibraryState>,
browseViewModel: BrowseViewModel, onEvent: (LibraryEvent) -> Unit,
historyViewModel: HistoryViewModel onBrowseLoadEvent: (BrowseEvent.OnLoadList) -> Unit,
onHistoryLoadEvent: (HistoryEvent.OnLoadList) -> Unit
) { ) {
val state by viewModel.state.collectAsState()
val context = LocalContext.current val context = LocalContext.current
CustomDialogWithContent( CustomDialogWithContent(
@ -34,17 +31,17 @@ fun LibraryDeleteDialog(
imageVectorIcon = Icons.Outlined.DeleteOutline, imageVectorIcon = Icons.Outlined.DeleteOutline,
description = stringResource( description = stringResource(
id = R.string.delete_books_description, 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), actionText = stringResource(id = R.string.delete),
onDismiss = { viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog) }, onDismiss = { onEvent(LibraryEvent.OnShowHideDeleteDialog) },
withDivider = false, withDivider = false,
isActionEnabled = true, isActionEnabled = true,
onAction = { onAction = {
viewModel.onEvent( onEvent(
LibraryEvent.OnDeleteBooks { LibraryEvent.OnDeleteBooks {
browseViewModel.onEvent(BrowseEvent.OnLoadList) onBrowseLoadEvent(BrowseEvent.OnLoadList)
historyViewModel.onEvent(HistoryEvent.OnLoadList) onHistoryLoadEvent(HistoryEvent.OnLoadList)
} }
) )
Toast.makeText( Toast.makeText(

View file

@ -6,8 +6,7 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -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.CustomDialogWithLazyColumn
import ua.acclorite.book_story.presentation.components.custom_dialog.SelectableDialogItem 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.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.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. * Move dialog. Moves all selected books to the selected category.
*/ */
@Composable @Composable
fun LibraryMoveDialog( fun LibraryMoveDialog(
viewModel: LibraryViewModel, state: State<LibraryState>,
historyViewModel: HistoryViewModel, onEvent: (LibraryEvent) -> Unit,
onHistoryLoadEvent: (HistoryEvent.OnLoadList) -> Unit,
pagerState: PagerState pagerState: PagerState
) { ) {
val state by viewModel.state.collectAsState()
val context = LocalContext.current val context = LocalContext.current
CustomDialogWithLazyColumn( CustomDialogWithLazyColumn(
@ -36,17 +34,17 @@ fun LibraryMoveDialog(
imageVectorIcon = Icons.AutoMirrored.Outlined.DriveFileMove, imageVectorIcon = Icons.AutoMirrored.Outlined.DriveFileMove,
description = stringResource( description = stringResource(
id = R.string.move_books_description, 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), actionText = stringResource(id = R.string.move),
isActionEnabled = true, isActionEnabled = true,
onDismiss = { viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog) }, onDismiss = { onEvent(LibraryEvent.OnShowHideMoveDialog) },
onAction = { onAction = {
viewModel.onEvent( onEvent(
LibraryEvent.OnMoveBooks( LibraryEvent.OnMoveBooks(
pagerState, pagerState,
refreshList = { refreshList = {
historyViewModel.onEvent(HistoryEvent.OnLoadList) onHistoryLoadEvent(HistoryEvent.OnLoadList)
} }
) )
) )
@ -58,7 +56,7 @@ fun LibraryMoveDialog(
}, },
withDivider = false, withDivider = false,
items = { items = {
items(state.categories) { items(state.value.categories) {
val category = when (it) { val category = when (it) {
Category.READING -> stringResource(id = R.string.reading_tab) Category.READING -> stringResource(id = R.string.reading_tab)
Category.ALREADY_READ -> stringResource(id = R.string.already_read_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) Category.DROPPED -> stringResource(id = R.string.dropped_tab)
} }
SelectableDialogItem(selected = it == state.selectedCategory, title = category) { SelectableDialogItem(
viewModel.onEvent(LibraryEvent.OnSelectCategory(it)) selected = it == state.value.selectedCategory,
title = category
) {
onEvent(LibraryEvent.OnSelectCategory(it))
} }
} }
} }

View file

@ -1,12 +1,14 @@
package ua.acclorite.book_story.presentation.screens.library.data package ua.acclorite.book_story.presentation.screens.library.data
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.runtime.Immutable
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.Selected import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
@Immutable
sealed class LibraryEvent { sealed class LibraryEvent {
data object OnRefreshList : LibraryEvent() data object OnRefreshList : LibraryEvent()
data object OnLoadList : LibraryEvent() data object OnLoadList : LibraryEvent()
@ -16,6 +18,7 @@ sealed class LibraryEvent {
data object OnSearchShowHide : LibraryEvent() data object OnSearchShowHide : LibraryEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : LibraryEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : LibraryEvent()
data class OnSearchQueryChange(val query: String) : LibraryEvent() data class OnSearchQueryChange(val query: String) : LibraryEvent()
data object OnSearch : LibraryEvent()
data class OnSelectBook(val book: Pair<Book, Selected>, val select: Boolean? = null) : data class OnSelectBook(val book: Pair<Book, Selected>, val select: Boolean? = null) :
LibraryEvent() LibraryEvent()

View file

@ -154,6 +154,12 @@ class LibraryViewModel @Inject constructor(
job = viewModelScope.launch(Dispatchers.IO) { job = viewModelScope.launch(Dispatchers.IO) {
delay(500) delay(500)
yield() yield()
onEvent(LibraryEvent.OnSearch)
}
}
is LibraryEvent.OnSearch -> {
viewModelScope.launch(Dispatchers.IO) {
getBooksFromDatabase() getBooksFromDatabase()
} }
} }
@ -237,7 +243,20 @@ class LibraryViewModel @Inject constructor(
}.map { it.first.copy(category = _state.value.selectedCategory) } }.map { it.first.copy(category = _state.value.selectedCategory) }
updateBooks.execute(books) 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() event.refreshList()
onEvent(LibraryEvent.OnClearSelectedBooks) onEvent(LibraryEvent.OnClearSelectedBooks)
@ -264,8 +283,7 @@ class LibraryViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
showDeleteDialog = false, showDeleteDialog = false
isLoading = true
) )
} }
@ -274,7 +292,12 @@ class LibraryViewModel @Inject constructor(
}.map { it.first } }.map { it.first }
deleteBooks.execute(books) deleteBooks.execute(books)
getBooksFromDatabase() _state.update {
it.copy(
books = it.books.filter { book -> !book.second }
)
}
onEvent(LibraryEvent.OnRecalculateCategories(_state.value.books))
event.refreshList() event.refreshList()
onEvent(LibraryEvent.OnClearSelectedBooks) onEvent(LibraryEvent.OnClearSelectedBooks)

View file

@ -5,7 +5,6 @@ import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.DisableSelection
@ -27,12 +27,12 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.Constants 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.CustomSelectionContainer
import ua.acclorite.book_story.presentation.components.is_messages.IsError 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.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
@ -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.app_bar.ReaderTopBar
import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
@OptIn(FlowPreview::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable @Composable
fun ReaderScreen( fun ReaderScreenRoot() {
viewModel: ReaderViewModel, val navigator = LocalNavigator.current
mainViewModel: MainViewModel,
libraryViewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
navigator: Navigator
) {
val context = LocalContext.current as ComponentActivity 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 listState = rememberLazyListState()
val loading = remember { mutableStateOf(true) }
val state by viewModel.state.collectAsState() val state = viewModel.state.collectAsState()
val canScroll by remember { val mainState = mainViewModel.state.collectAsState()
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]
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.init( viewModel.init(
@ -129,13 +104,73 @@ fun ReaderScreen(
}, },
listState = listState, listState = listState,
onLoaded = { 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<ReaderState>,
mainState: State<MainSettingsState>,
listState: LazyListState,
loading: State<Boolean>,
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) { LaunchedEffect(canScroll) {
if (!canScroll && !state.showMenu && !loading) { if (!canScroll && !state.value.showMenu && !loading.value) {
viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context)) onEvent(ReaderEvent.OnShowHideMenu(context = context))
} }
} }
@ -143,28 +178,28 @@ fun ReaderScreen(
snapshotFlow { snapshotFlow {
listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
}.debounce(500).collectLatest { items -> }.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 lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index
val progress = if (items.first > 0) { val progress = if (items.first > 0) {
if (lastVisibleItemIndex >= (listState.layoutInfo.totalItemsCount - 1)) { if (lastVisibleItemIndex >= (listState.layoutInfo.totalItemsCount - 1)) {
1f 1f
} else { } else {
(items.first.toFloat() / (state.book.text.lastIndex).toFloat()) (items.first.toFloat() / (state.value.book.text.lastIndex).toFloat())
} }
} else { } else {
0f 0f
} }
viewModel.onEvent( onEvent(
ReaderEvent.OnChangeProgress( ReaderEvent.OnChangeProgress(
progress = progress, progress = progress,
navigator = navigator, navigator = navigator,
firstVisibleItemIndex = items.first, firstVisibleItemIndex = items.first,
firstVisibleItemOffset = items.second, firstVisibleItemOffset = items.second,
refreshList = { book -> refreshList = { book ->
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(book)) onLibraryEvent(LibraryEvent.OnUpdateBook(book))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(book)) onHistoryEvent(HistoryEvent.OnUpdateBook(book))
} }
) )
) )
@ -172,8 +207,12 @@ fun ReaderScreen(
} }
} }
if (state.showSettingsBottomSheet) { if (state.value.showSettingsBottomSheet) {
ReaderSettingsBottomSheet(viewModel = viewModel, mainViewModel = mainViewModel) ReaderSettingsBottomSheet(
mainState = mainState,
onEvent = onEvent,
onMainEvent = onMainEvent
)
} }
Scaffold( Scaffold(
@ -182,34 +221,34 @@ fun ReaderScreen(
.nestedScroll(nestedScrollConnection), .nestedScroll(nestedScrollConnection),
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
topBar = { topBar = {
AnimatedVisibility( CustomAnimatedVisibility(
visible = state.showMenu, visible = state.value.showMenu,
enter = slideInVertically { -it }, enter = slideInVertically { -it },
exit = slideOutVertically { -it } exit = slideOutVertically { -it }
) { ) {
ReaderTopBar( ReaderTopBar(
viewModel = viewModel, state = state,
libraryViewModel = libraryViewModel, onEvent = onEvent,
historyViewModel = historyViewModel, onLibraryUpdateEvent = onLibraryEvent,
navigator = navigator, onHistoryUpdateEvent = onHistoryEvent,
listState = listState, listState = listState,
containerColor = systemBarsColor containerColor = systemBarsColor
) )
} }
}, },
bottomBar = { bottomBar = {
AnimatedVisibility( CustomAnimatedVisibility(
visible = state.showMenu, visible = state.value.showMenu,
enter = slideInVertically { it }, enter = slideInVertically { it },
exit = slideOutVertically { it } exit = slideOutVertically { it }
) { ) {
ReaderBottomBar( ReaderBottomBar(
viewModel = viewModel, state = state,
libraryViewModel = libraryViewModel, onEvent = onEvent,
historyViewModel = historyViewModel, onLibraryUpdateEvent = onLibraryEvent,
onHistoryUpdateEvent = onHistoryEvent,
listState = listState, listState = listState,
navigator = navigator, containerColor = systemBarsColor
systemBarsColor = systemBarsColor
) )
} }
} }
@ -225,7 +264,7 @@ fun ReaderScreen(
} }
}, },
onTranslateRequested = { textToTranslate -> onTranslateRequested = { textToTranslate ->
viewModel.onEvent( onEvent(
ReaderEvent.OnTranslateText( ReaderEvent.OnTranslateText(
textToTranslate, textToTranslate,
context, context,
@ -240,7 +279,7 @@ fun ReaderScreen(
) )
}, },
onDictionaryRequested = { textToDefine -> onDictionaryRequested = { textToDefine ->
viewModel.onEvent( onEvent(
ReaderEvent.OnOpenDictionary( ReaderEvent.OnOpenDictionary(
textToDefine, textToDefine,
context, context,
@ -260,13 +299,13 @@ fun ReaderScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.then( .then(
if (!loading && toolbarShowed) { if (!loading.value && toolbarShowed) {
Modifier Modifier
.clickable( .clickable(
interactionSource = null, interactionSource = null,
indication = null, indication = null,
onClick = { onClick = {
viewModel.onEvent( onEvent(
ReaderEvent.OnShowHideMenu(context = context) ReaderEvent.OnShowHideMenu(context = context)
) )
} }
@ -276,28 +315,29 @@ fun ReaderScreen(
} }
) )
) { ) {
if (state.book.text.isNotEmpty()) { if (state.value.book.text.isNotEmpty()) {
item { item {
DisableSelection { DisableSelection {
ReaderStartItem(viewModel = viewModel) ReaderStartItem(state = state)
} }
} }
} }
itemsIndexed( itemsIndexed(
state.book.text, key = { _, key -> key.id } state.value.book.text, key = { _, key -> key.id }
) { index, line -> ) { index, line ->
val text = remember(mainState.paragraphIndentation) { val text = remember(mainState.value.paragraphIndentation) {
"${if (mainState.paragraphIndentation!!) " " else ""}${line.line}" "${if (mainState.value.paragraphIndentation!!) " " else ""}${line.line}"
} }
val fontColor = remember(mainState.fontColor) { val fontColor = remember(mainState.value.fontColor) {
Color(mainState.fontColor!!.toULong()) Color(mainState.value.fontColor!!.toULong())
} }
val backgroundColor = remember(mainState.backgroundColor) { val backgroundColor = remember(mainState.value.backgroundColor) {
Color(mainState.backgroundColor!!.toULong()) Color(mainState.value.backgroundColor!!.toULong())
} }
val lineHeight = remember(mainState.fontSize, mainState.lineHeight) { val lineHeight =
(mainState.fontSize!! + mainState.lineHeight!!).sp remember(mainState.value.fontSize, mainState.value.lineHeight) {
(mainState.value.fontSize!! + mainState.value.lineHeight!!).sp
} }
Column( Column(
@ -308,8 +348,8 @@ fun ReaderScreen(
top = if (index == 0) 18.dp else 0.dp, top = if (index == 0) 18.dp else 0.dp,
start = 18.dp, start = 18.dp,
end = 18.dp, end = 18.dp,
bottom = if (index == state.book.text.lastIndex) 18.dp bottom = if (index == state.value.book.text.lastIndex) 18.dp
else (mainState.paragraphHeight!! * 3).dp else (mainState.value.paragraphHeight!! * 3).dp
) )
) { ) {
Text( Text(
@ -319,25 +359,25 @@ fun ReaderScreen(
lineBreak = LineBreak.Paragraph lineBreak = LineBreak.Paragraph
), ),
fontFamily = fontFamily.font, fontFamily = fontFamily.font,
fontStyle = when (mainState.isItalic!!) { fontStyle = when (mainState.value.isItalic!!) {
true -> FontStyle.Italic true -> FontStyle.Italic
false -> FontStyle.Normal false -> FontStyle.Normal
}, },
fontSize = mainState.fontSize!!.sp, fontSize = mainState.value.fontSize!!.sp,
lineHeight = lineHeight lineHeight = lineHeight
) )
} }
} }
if (state.book.text.isNotEmpty()) { if (state.value.book.text.isNotEmpty()) {
item { item {
DisableSelection { DisableSelection {
ReaderEndItem( ReaderEndItem(
libraryViewModel = libraryViewModel, state = state,
historyViewModel = historyViewModel, onEvent = onEvent,
viewModel = viewModel, onLibraryEvent = onLibraryEvent,
listState = listState, onHistoryUpdateEvent = onHistoryEvent,
navigator = navigator listState = listState
) )
} }
} }
@ -346,7 +386,7 @@ fun ReaderScreen(
} }
} }
if (loading || state.errorMessage != null) { if (loading.value || state.value.errorMessage != null) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -354,20 +394,20 @@ fun ReaderScreen(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (!loading && state.errorMessage != null) { if (!loading.value) {
IsError( IsError(
errorMessage = state.errorMessage!!.asString(), errorMessage = state.value.errorMessage!!.asString(),
icon = painterResource(id = R.drawable.error), icon = painterResource(id = R.drawable.error),
actionTitle = stringResource(id = R.string.go_back), actionTitle = stringResource(id = R.string.go_back),
action = { action = {
libraryViewModel.onEvent(LibraryEvent.OnLoadList) onLibraryEvent(LibraryEvent.OnLoadList)
viewModel.onEvent( onEvent(
ReaderEvent.OnGoBack( ReaderEvent.OnGoBack(
context, context,
navigator, navigator,
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) onLibraryEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) onHistoryEvent(HistoryEvent.OnUpdateBook(it))
}, },
listState = listState, listState = listState,
navigate = { navigate = {
@ -396,13 +436,13 @@ fun ReaderScreen(
} }
BackHandler { BackHandler {
viewModel.onEvent( onEvent(
ReaderEvent.OnGoBack( ReaderEvent.OnGoBack(
context, context,
navigator, navigator,
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) onLibraryEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) onHistoryEvent(HistoryEvent.OnUpdateBook(it))
}, },
listState = listState, listState = listState,
navigate = { navigate = {

View file

@ -19,8 +19,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -32,14 +31,12 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.presentation.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.Screen
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
/** /**
* Reader end item. Displays at the end of the book. * 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) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun ReaderEndItem( fun ReaderEndItem(
libraryViewModel: LibraryViewModel, state: State<ReaderState>,
historyViewModel: HistoryViewModel, onEvent: (ReaderEvent) -> Unit,
viewModel: ReaderViewModel, onLibraryEvent: (LibraryEvent) -> Unit,
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
listState: LazyListState, listState: LazyListState,
navigator: Navigator
) { ) {
val state by viewModel.state.collectAsState() val navigator = LocalNavigator.current
val context = LocalContext.current as ComponentActivity val context = LocalContext.current as ComponentActivity
val buttonText = remember { val buttonText = remember {
context.getString( 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 else R.string.back_to_library
) )
} }
@ -76,7 +73,7 @@ fun ReaderEndItem(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( 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, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -86,8 +83,8 @@ fun ReaderEndItem(
Text( Text(
stringResource( stringResource(
id = R.string.letters_and_words, id = R.string.letters_and_words,
state.book.letters, state.value.book.letters,
state.book.words state.value.book.words
), ),
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
@ -96,15 +93,15 @@ fun ReaderEndItem(
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
TextButton( TextButton(
onClick = { onClick = {
if (state.book.category != Category.ALREADY_READ) { if (state.value.book.category != Category.ALREADY_READ) {
viewModel.onEvent( onEvent(
ReaderEvent.OnMoveBookToAlreadyRead( ReaderEvent.OnMoveBookToAlreadyRead(
onUpdateCategories = { onUpdateCategories = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) onLibraryEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
}, },
updatePage = { updatePage = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it)) onLibraryEvent(LibraryEvent.OnUpdateCurrentPage(it))
}, },
navigator = navigator, navigator = navigator,
listState = listState, listState = listState,
@ -118,13 +115,13 @@ fun ReaderEndItem(
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} else { } else {
viewModel.onEvent( onEvent(
ReaderEvent.OnGoBack( ReaderEvent.OnGoBack(
context, context,
navigator, navigator,
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) onLibraryEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
}, },
listState = listState, listState = listState,
navigate = { navigate = {

View file

@ -23,8 +23,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -33,15 +32,14 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CustomCoverImage 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. * Reader start item. Displays at the beginning of the book.
*/ */
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun ReaderStartItem(viewModel: ReaderViewModel) { fun ReaderStartItem(state: State<ReaderState>) {
val state by viewModel.state.collectAsState()
Column( Column(
modifier = Modifier modifier = Modifier
@ -61,9 +59,9 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
.clip(RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colorScheme.surfaceContainerLow) .background(MaterialTheme.colorScheme.surfaceContainerLow)
) { ) {
if (state.book.coverImage != null) { if (state.value.book.coverImage != null) {
CustomCoverImage( CustomCoverImage(
uri = state.book.coverImage!!, uri = state.value.book.coverImage!!,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(10.dp))
@ -85,7 +83,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
Column(verticalArrangement = Arrangement.Center) { Column(verticalArrangement = Arrangement.Center) {
Text( Text(
state.book.title, state.value.book.title,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
modifier = Modifier modifier = Modifier

View file

@ -15,7 +15,7 @@ import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -23,32 +23,30 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.presentation.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.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero 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.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.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.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. * Reader bottom bar. Has a slider to change progress.
*/ */
@Composable @Composable
fun ReaderBottomBar( fun ReaderBottomBar(
viewModel: ReaderViewModel, state: State<ReaderState>,
libraryViewModel: LibraryViewModel, onEvent: (ReaderEvent) -> Unit,
historyViewModel: HistoryViewModel, onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
listState: LazyListState, listState: LazyListState,
navigator: Navigator, containerColor: Color
systemBarsColor: Color
) { ) {
val state by viewModel.state.collectAsState() val navigator = LocalNavigator.current
val progress by remember(state.book.progress) { val progress by remember(state.value.book.progress) {
derivedStateOf { derivedStateOf {
(state.book.progress * 100) (state.value.book.progress * 100)
.toDouble() .toDouble()
.removeDigits(4) .removeDigits(4)
.removeTrailingZero() .removeTrailingZero()
@ -59,7 +57,7 @@ fun ReaderBottomBar(
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.background(systemBarsColor) .background(containerColor)
.clickable( .clickable(
interactionSource = null, interactionSource = null,
indication = null, indication = null,
@ -78,20 +76,20 @@ fun ReaderBottomBar(
) )
Spacer(modifier = Modifier.height(3.dp)) Spacer(modifier = Modifier.height(3.dp))
Slider( Slider(
value = state.book.progress, value = state.value.book.progress,
enabled = !state.lockMenu, enabled = !state.value.lockMenu,
onValueChange = { onValueChange = {
if (listState.layoutInfo.totalItemsCount > 0) { if (listState.layoutInfo.totalItemsCount > 0) {
viewModel.onEvent(ReaderEvent.OnScroll(listState, it)) onEvent(ReaderEvent.OnScroll(listState, it))
viewModel.onEvent( onEvent(
ReaderEvent.OnChangeProgress( ReaderEvent.OnChangeProgress(
progress = it, progress = it,
navigator = navigator, navigator = navigator,
firstVisibleItemIndex = listState.firstVisibleItemIndex, firstVisibleItemIndex = listState.firstVisibleItemIndex,
firstVisibleItemOffset = 0, firstVisibleItemOffset = 0,
refreshList = { book -> refreshList = { book ->
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(book)) onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(book))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(book)) onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(book))
} }
) )
) )

View file

@ -15,7 +15,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -28,16 +28,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CustomIconButton 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.Screen
import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero import ua.acclorite.book_story.presentation.data.removeTrailingZero
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
/** /**
* Reader top bar. Displays title of the book. * 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ReaderTopBar( fun ReaderTopBar(
viewModel: ReaderViewModel, state: State<ReaderState>,
libraryViewModel: LibraryViewModel, onEvent: (ReaderEvent) -> Unit,
historyViewModel: HistoryViewModel, onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
listState: LazyListState, listState: LazyListState,
navigator: Navigator,
containerColor: Color containerColor: Color
) { ) {
val navigator = LocalNavigator.current
val context = LocalContext.current as ComponentActivity 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 { derivedStateOf {
(state.book.progress * 100) (state.value.book.progress * 100)
.toDouble() .toDouble()
.removeDigits(2) .removeDigits(2)
.removeTrailingZero() .removeTrailingZero()
@ -72,13 +70,13 @@ fun ReaderTopBar(
contentDescription = R.string.go_back_content_desc, contentDescription = R.string.go_back_content_desc,
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent( onEvent(
ReaderEvent.OnGoBack( ReaderEvent.OnGoBack(
context, context,
navigator, navigator,
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
}, },
listState = listState, listState = listState,
navigate = { navigate = {
@ -91,7 +89,7 @@ fun ReaderTopBar(
title = { title = {
Column(verticalArrangement = Arrangement.Center) { Column(verticalArrangement = Arrangement.Center) {
Text( Text(
state.book.title, state.value.book.title,
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 20.sp, fontSize = 20.sp,
@ -99,17 +97,17 @@ fun ReaderTopBar(
maxLines = 1, maxLines = 1,
modifier = Modifier modifier = Modifier
.clickable( .clickable(
enabled = !state.lockMenu, enabled = !state.value.lockMenu,
interactionSource = null, interactionSource = null,
indication = null, indication = null,
onClick = { onClick = {
viewModel.onEvent( onEvent(
ReaderEvent.OnGoBack( ReaderEvent.OnGoBack(
context, context,
navigator, navigator,
refreshList = { refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
}, },
listState = listState, listState = listState,
navigate = { navigate = {
@ -141,9 +139,9 @@ fun ReaderTopBar(
icon = Icons.Default.Settings, icon = Icons.Default.Settings,
contentDescription = R.string.open_reader_settings_content_desc, contentDescription = R.string.open_reader_settings_content_desc,
disableOnClick = false, disableOnClick = false,
enabled = !state.lockMenu enabled = !state.value.lockMenu
) { ) {
viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet) onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(

View file

@ -23,7 +23,7 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember 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.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.MainSettingsState
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
import ua.acclorite.book_story.presentation.screens.settings.components.CheckboxWithTitle 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.ChipsWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle 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 ExperimentalMaterial3Api::class
) )
@Composable @Composable
fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderViewModel) { fun ReaderSettingsBottomSheet(
mainState: State<MainSettingsState>,
onEvent: (ReaderEvent) -> Unit,
onMainEvent: (MainEvent) -> Unit
) {
val pagerState = rememberPagerState { 2 } val pagerState = rememberPagerState { 2 }
val currentPage by remember { derivedStateOf { pagerState.currentPage } } val currentPage by remember { derivedStateOf { pagerState.currentPage } }
val context = LocalContext.current val context = LocalContext.current
@ -62,11 +65,9 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
val navigationBarPadding = val navigationBarPadding =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
val state by mainViewModel.state.collectAsState() val fontFamily = remember(mainState.value.fontFamily) {
val fontFamily = remember(state.fontFamily) {
Constants.FONTS.find { Constants.FONTS.find {
it.id == state.fontFamily it.id == mainState.value.fontFamily
} ?: Constants.FONTS[0] } ?: Constants.FONTS[0]
} }
@ -89,7 +90,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
) )
LaunchedEffect(currentPage) { LaunchedEffect(currentPage) {
viewModel.onEvent( onEvent(
ReaderEvent.OnShowHideMenu( ReaderEvent.OnShowHideMenu(
currentPage != 1, currentPage != 1,
context as ComponentActivity context as ComponentActivity
@ -106,13 +107,16 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight(animatedHeight), .fillMaxHeight(animatedHeight),
onDismissRequest = { onDismissRequest = {
viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet) onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
}, },
sheetState = rememberModalBottomSheetState(true), sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
containerColor = MaterialTheme.colorScheme.surfaceContainerLow containerColor = MaterialTheme.colorScheme.surfaceContainerLow
) { ) {
ReaderSettingsBottomSheetTabRow(viewModel = viewModel, pagerState = pagerState) ReaderSettingsBottomSheetTabRow(
onEvent = onEvent,
pagerState = pagerState
)
HorizontalPager(state = pagerState) { page -> HorizontalPager(state = pagerState) { page ->
LazyColumn { LazyColumn {
@ -144,7 +148,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
) )
}, },
onClick = { 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, fontFamily = fontFamily.font,
fontStyle = FontStyle.Normal fontStyle = FontStyle.Normal
), ),
!state.isItalic!! !mainState.value.isItalic!!
), ),
ButtonItem( ButtonItem(
"italic", "italic",
@ -168,11 +172,11 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
fontFamily = fontFamily.font, fontFamily = fontFamily.font,
fontStyle = FontStyle.Italic fontStyle = FontStyle.Italic
), ),
state.isItalic!! mainState.value.isItalic!!
), ),
), ),
onClick = { onClick = {
mainViewModel.onEvent( onMainEvent(
MainEvent.OnChangeFontStyle( MainEvent.OnChangeFontStyle(
when (it.id) { when (it.id) {
"italic" -> true "italic" -> true
@ -185,12 +189,12 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
item { item {
SliderWithTitle( SliderWithTitle(
value = state.fontSize!! to "pt", value = mainState.value.fontSize!! to "pt",
fromValue = 10, fromValue = 10,
toValue = 35, toValue = 35,
title = stringResource(id = R.string.font_size_option), title = stringResource(id = R.string.font_size_option),
onValueChange = { onValueChange = {
mainViewModel.onEvent( onMainEvent(
MainEvent.OnChangeFontSize(it) MainEvent.OnChangeFontSize(it)
) )
} }
@ -210,12 +214,12 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
item { item {
SliderWithTitle( SliderWithTitle(
value = state.lineHeight!! to "pt", value = mainState.value.lineHeight!! to "pt",
fromValue = 1, fromValue = 1,
toValue = 16, toValue = 16,
title = stringResource(id = R.string.line_height_option), title = stringResource(id = R.string.line_height_option),
onValueChange = { onValueChange = {
mainViewModel.onEvent( onMainEvent(
MainEvent.OnChangeLineHeight(it) MainEvent.OnChangeLineHeight(it)
) )
} }
@ -223,12 +227,12 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
item { item {
SliderWithTitle( SliderWithTitle(
value = state.paragraphHeight!! to "pt", value = mainState.value.paragraphHeight!! to "pt",
fromValue = 0, fromValue = 0,
toValue = 24, toValue = 24,
title = stringResource(id = R.string.paragraph_height_option), title = stringResource(id = R.string.paragraph_height_option),
onValueChange = { onValueChange = {
mainViewModel.onEvent( onMainEvent(
MainEvent.OnChangeParagraphHeight(it) MainEvent.OnChangeParagraphHeight(it)
) )
} }
@ -236,11 +240,11 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
item { item {
CheckboxWithTitle( CheckboxWithTitle(
selected = state.paragraphIndentation!!, selected = mainState.value.paragraphIndentation!!,
title = stringResource(id = R.string.paragraph_indentation_option) title = stringResource(id = R.string.paragraph_indentation_option)
) { ) {
mainViewModel.onEvent( onMainEvent(
MainEvent.OnChangeParagraphIndentation(!state.paragraphIndentation!!) MainEvent.OnChangeParagraphIndentation(!mainState.value.paragraphIndentation!!)
) )
} }
} }
@ -252,10 +256,10 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
item { item {
ColorPickerWithTitle( ColorPickerWithTitle(
value = Color(state.backgroundColor!!.toULong()), value = Color(mainState.value.backgroundColor!!.toULong()),
title = stringResource(id = R.string.background_color_option), title = stringResource(id = R.string.background_color_option),
onValueChange = { onValueChange = {
mainViewModel.onEvent( onMainEvent(
MainEvent.OnChangeBackgroundColor( MainEvent.OnChangeBackgroundColor(
it.value.toLong() it.value.toLong()
) )
@ -265,10 +269,10 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
item { item {
ColorPickerWithTitle( ColorPickerWithTitle(
value = Color(state.fontColor!!.toULong()), value = Color(mainState.value.fontColor!!.toULong()),
title = stringResource(id = R.string.font_color_option), title = stringResource(id = R.string.font_color_option),
onValueChange = { onValueChange = {
mainViewModel.onEvent( onMainEvent(
MainEvent.OnChangeFontColor( MainEvent.OnChangeFontColor(
it.value.toLong() it.value.toLong()
) )

View file

@ -13,14 +13,16 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent 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. * Settings bottom sheet tab row. It is used to switch between categories.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: PagerState) { fun ReaderSettingsBottomSheetTabRow(
onEvent: (ReaderEvent) -> Unit,
pagerState: PagerState
) {
val tabItems = listOf( val tabItems = listOf(
stringResource(id = R.string.general_tab), stringResource(id = R.string.general_tab),
stringResource(id = R.string.color_tab) stringResource(id = R.string.color_tab)
@ -37,7 +39,7 @@ fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: Page
modifier = Modifier, modifier = Modifier,
selected = pagerState.currentPage == index, selected = pagerState.currentPage == index,
onClick = { onClick = {
viewModel.onEvent(ReaderEvent.OnScrollToSettingsPage(index, pagerState)) onEvent(ReaderEvent.OnScrollToSettingsPage(index, pagerState))
}, },
selectedContentColor = MaterialTheme.colorScheme.primary, selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,

View file

@ -3,9 +3,11 @@ package ua.acclorite.book_story.presentation.screens.reader.data
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
@Immutable
sealed class ReaderEvent { sealed class ReaderEvent {
data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent() data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent()
data class OnLoadText( data class OnLoadText(

View file

@ -25,17 +25,25 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.GoBackButton 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.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem
@Composable
fun SettingsScreenRoot() {
val navigator = LocalNavigator.current
SettingsScreen(navigator = navigator)
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen( private fun SettingsScreen(
navigator: Navigator navigator: Navigator
) { ) {
val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState() val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
Scaffold( Scaffold(
Modifier Modifier

View file

@ -1,18 +1,21 @@
package ua.acclorite.book_story.presentation.screens.settings.components package ua.acclorite.book_story.presentation.screens.settings.components
import androidx.compose.foundation.clickable 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width 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.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.presentation.components.CategoryTitle
/** /**
* Checkbox with title. * Checkbox with title.
@ -21,24 +24,58 @@ import ua.acclorite.book_story.presentation.components.CategoryTitle
fun CheckboxWithTitle( fun CheckboxWithTitle(
selected: Boolean, selected: Boolean,
title: String, title: String,
description: String? = null,
horizontalPadding: Dp = 18.dp, horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 12.dp, verticalPadding: Dp = 8.dp,
onClick: () -> Unit onClick: () -> Unit
) { ) {
Row( Row(
Modifier modifier = Modifier
.fillMaxWidth() .clickable { onClick() }
.clickable {
onClick()
}
.padding(horizontal = horizontalPadding, vertical = verticalPadding), .padding(horizontal = horizontalPadding, vertical = verticalPadding),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
) { ) {
Checkbox( Column(Modifier.weight(1f), verticalArrangement = Arrangement.Center) {
checked = selected, Row(verticalAlignment = Alignment.CenterVertically) {
onCheckedChange = null, Text(
text = title,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleMedium
) )
Spacer(modifier = Modifier.width(24.dp))
CategoryTitle(title = title, padding = 0.dp)
} }
description?.let {
Text(
text = it,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall
)
}
}
Spacer(modifier = Modifier.width(8.dp))
Switch(
checked = selected,
onCheckedChange = null
)
}
//
// 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)
// }
} }

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@ -61,7 +60,6 @@ fun SettingsCategoryItem(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(2.dp))
Text( Text(
description, description,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,

View file

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

View file

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

View file

@ -16,23 +16,25 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ButtonItem import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.GoBackButton 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.MainEvent
import ua.acclorite.book_story.presentation.data.MainSettingsState
import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle 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.isDark
import ua.acclorite.book_story.presentation.ui.isPureDark 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppearanceSettings( private fun AppearanceSettings(
mainViewModel: MainViewModel, state: State<MainSettingsState>,
navigator: Navigator 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() LaunchedEffect(state.value.theme) {
var themeContrastTheme by remember { mutableStateOf(state.theme!!) } if (themeContrastTheme.value != state.value.theme && state.value.theme != Theme.DYNAMIC) {
themeContrastTheme.value = state.value.theme!!
LaunchedEffect(state.theme) {
if (themeContrastTheme != state.theme && state.theme != Theme.DYNAMIC) {
themeContrastTheme = state.theme!!
} }
} }
@ -128,11 +143,11 @@ fun AppearanceSettings(
DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system) DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system)
}, },
MaterialTheme.typography.labelLarge, MaterialTheme.typography.labelLarge,
it == state.darkTheme it == state.value.darkTheme
) )
} }
) { ) {
mainViewModel.onEvent( onMainEvent(
MainEvent.OnChangeDarkTheme( MainEvent.OnChangeDarkTheme(
it.id it.id
) )
@ -143,27 +158,28 @@ fun AppearanceSettings(
item { item {
AppearanceSettingsThemeSwitcher( AppearanceSettingsThemeSwitcher(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
mainViewModel = mainViewModel state = state,
onMainEvent = onMainEvent
) )
} }
item { item {
BookStoryTheme( BookStoryTheme(
theme = themeContrastTheme, theme = themeContrastTheme.value,
isDark = state.darkTheme!!.isDark(), isDark = state.value.darkTheme!!.isDark(),
isPureDark = state.pureDark!!.isPureDark(context = LocalContext.current), isPureDark = state.value.pureDark!!.isPureDark(context = LocalContext.current),
themeContrast = state.themeContrast!! themeContrast = state.value.themeContrast!!
) { ) {
SlidingTransition( SlidingTransition(
modifier = Modifier.animateItem( modifier = Modifier.animateItem(
fadeInSpec = null, fadeInSpec = null,
fadeOutSpec = null fadeOutSpec = null
), ),
visible = state.theme != Theme.DYNAMIC, visible = state.value.theme != Theme.DYNAMIC,
) { ) {
SegmentedButtonWithTitle( SegmentedButtonWithTitle(
title = stringResource(id = R.string.theme_contrast_option), title = stringResource(id = R.string.theme_contrast_option),
locked = state.theme != Theme.DYNAMIC, locked = state.value.theme != Theme.DYNAMIC,
buttons = ThemeContrast.entries.map { buttons = ThemeContrast.entries.map {
ButtonItem( ButtonItem(
it.toString(), it.toString(),
@ -173,11 +189,11 @@ fun AppearanceSettings(
ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high) ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high)
}, },
MaterialTheme.typography.labelLarge, MaterialTheme.typography.labelLarge,
it == state.themeContrast it == state.value.themeContrast
) )
} }
) { ) {
mainViewModel.onEvent( onMainEvent(
MainEvent.OnChangeThemeContrast( MainEvent.OnChangeThemeContrast(
it.id it.id
) )
@ -193,7 +209,7 @@ fun AppearanceSettings(
fadeInSpec = null, fadeInSpec = null,
fadeOutSpec = null fadeOutSpec = null
), ),
visible = state.darkTheme!!.isDark(), visible = state.value.darkTheme!!.isDark(),
) { ) {
SegmentedButtonWithTitle( SegmentedButtonWithTitle(
title = stringResource(id = R.string.pure_dark_option), title = stringResource(id = R.string.pure_dark_option),
@ -207,11 +223,11 @@ fun AppearanceSettings(
PureDark.SAVER -> stringResource(id = R.string.pure_dark_power_saver) PureDark.SAVER -> stringResource(id = R.string.pure_dark_power_saver)
}, },
MaterialTheme.typography.labelLarge, MaterialTheme.typography.labelLarge,
it == state.pureDark it == state.value.pureDark
) )
} }
) { ) {
mainViewModel.onEvent( onMainEvent(
MainEvent.OnChangePureDark( MainEvent.OnChangePureDark(
it.id it.id
) )
@ -241,10 +257,10 @@ fun AppearanceSettings(
item { item {
ColorPickerWithTitle( ColorPickerWithTitle(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
value = Color(state.backgroundColor!!.toULong()), value = Color(state.value.backgroundColor!!.toULong()),
title = stringResource(id = R.string.background_color_option), title = stringResource(id = R.string.background_color_option),
onValueChange = { onValueChange = {
mainViewModel.onEvent( onMainEvent(
MainEvent.OnChangeBackgroundColor( MainEvent.OnChangeBackgroundColor(
it.value.toLong() it.value.toLong()
) )
@ -255,10 +271,10 @@ fun AppearanceSettings(
item { item {
ColorPickerWithTitle( ColorPickerWithTitle(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
value = Color(state.fontColor!!.toULong()), value = Color(state.value.fontColor!!.toULong()),
title = stringResource(id = R.string.font_color_option), title = stringResource(id = R.string.font_color_option),
onValueChange = { onValueChange = {
mainViewModel.onEvent( onMainEvent(
MainEvent.OnChangeFontColor( MainEvent.OnChangeFontColor(
it.value.toLong() it.value.toLong()
) )

View file

@ -10,8 +10,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.MainSettingsState
import ua.acclorite.book_story.presentation.ui.Theme import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.isDark import ua.acclorite.book_story.presentation.ui.isDark
import ua.acclorite.book_story.presentation.ui.isPureDark import ua.acclorite.book_story.presentation.ui.isPureDark
@ -33,10 +32,10 @@ import ua.acclorite.book_story.presentation.ui.isPureDark
@Composable @Composable
fun AppearanceSettingsThemeSwitcher( fun AppearanceSettingsThemeSwitcher(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
mainViewModel: MainViewModel, state: State<MainSettingsState>,
onMainEvent: (MainEvent) -> Unit,
verticalPadding: Dp = 8.dp verticalPadding: Dp = 8.dp
) { ) {
val state by mainViewModel.state.collectAsState()
val themes = remember { val themes = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES
else Constants.THEMES.dropWhile { it.first == Theme.DYNAMIC } else Constants.THEMES.dropWhile { it.first == Theme.DYNAMIC }
@ -64,12 +63,12 @@ fun AppearanceSettingsThemeSwitcher(
AppearanceSettingsThemeSwitcherItem( AppearanceSettingsThemeSwitcherItem(
theme = themeEntry, theme = themeEntry,
darkTheme = state.darkTheme!!.isDark(), darkTheme = state.value.darkTheme!!.isDark(),
themeContrast = state.themeContrast!!, themeContrast = state.value.themeContrast!!,
isPureDark = state.pureDark!!.isPureDark(context = LocalContext.current), isPureDark = state.value.pureDark!!.isPureDark(context = LocalContext.current),
selected = state.theme == themeEntry.first selected = state.value.theme == themeEntry.first
) { ) {
mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.first.toString())) onMainEvent(MainEvent.OnChangeTheme(themeEntry.first.toString()))
} }
if (index == themes.lastIndex) { if (index == themes.lastIndex) {

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher 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.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -34,6 +33,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.UIText 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.Theme
import ua.acclorite.book_story.presentation.ui.ThemeContrast import ua.acclorite.book_story.presentation.ui.ThemeContrast
import ua.acclorite.book_story.presentation.ui.animatedColorScheme import ua.acclorite.book_story.presentation.ui.animatedColorScheme
@ -94,7 +94,7 @@ fun AppearanceSettingsThemeSwitcherItem(
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
AnimatedVisibility( CustomAnimatedVisibility(
visible = selected, visible = selected,
enter = scaleIn(tween(300), initialScale = 0.5f) + enter = scaleIn(tween(300), initialScale = 0.5f) +
fadeIn(tween(300)), fadeIn(tween(300)),

Some files were not shown because too many files have changed in this diff Show more