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" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

View file

@ -4,6 +4,7 @@ plugins {
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
id("kotlin-parcelize")
id("com.mikepenz.aboutlibraries.plugin")
}
android {
@ -15,7 +16,7 @@ android {
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "0.9.7"
versionName = "0.9.8"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@ -61,16 +62,15 @@ android {
dependencies {
// Default
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.core:core-ktx:1.13.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.activity:activity-compose:1.9.0")
// Compose BOM libraries
implementation(platform("androidx.compose:compose-bom:2024.04.00"))
implementation(platform("androidx.compose:compose-bom:2024.04.01"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.animation:animation")
implementation("androidx.compose.animation:animation-core")
implementation("androidx.compose.animation:animation-graphics")
implementation("androidx.compose.animation:animation-android")
implementation("androidx.compose.foundation:foundation-layout")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
@ -81,35 +81,13 @@ dependencies {
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material:material")
// Unit tests
testImplementation("androidx.test:core:latest.release")
testImplementation("junit:junit:latest.release")
testImplementation("androidx.arch.core:core-testing:latest.release")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:latest.release")
testImplementation("com.google.truth:truth:1.4.2")
testImplementation("androidx.test.ext:truth:1.5.0")
testImplementation("com.squareup.okhttp3:mockwebserver:latest.release")
testImplementation("io.mockk:mockk:latest.release")
debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.5")
// Instrumentation tests
androidTestImplementation("com.google.dagger:hilt-android-testing:latest.release")
androidTestImplementation("junit:junit:latest.release")
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:latest.release")
androidTestImplementation("androidx.arch.core:core-testing:latest.release")
androidTestImplementation("com.google.truth:truth:latest.release")
androidTestImplementation("androidx.test.ext:junit:latest.release")
androidTestImplementation("androidx.test:core-ktx:latest.release")
androidTestImplementation("com.squareup.okhttp3:mockwebserver:latest.release")
androidTestImplementation("io.mockk:mockk-android:latest.release")
androidTestImplementation("androidx.test:runner:latest.release")
// All dependencies
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
// Dagger - Hilt
implementation("com.google.dagger:hilt-android:2.51.1")
debugImplementation("androidx.compose.ui:ui-tooling:1.6.6")
ksp("com.google.dagger:hilt-android-compiler:2.51")
implementation("com.google.dagger:hilt-compiler:latest.release")
ksp("androidx.hilt:hilt-compiler:latest.release")
@ -148,4 +126,16 @@ dependencies {
// Coil for loading bitmaps
implementation("io.coil-kt:coil-compose:2.6.0")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.moshi:moshi:1.15.1")
implementation("com.squareup.moshi:moshi-kotlin:1.15.1")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
// About open source libraries
implementation("com.mikepenz:aboutlibraries-core:latest.release")
implementation("com.mikepenz:aboutlibraries-compose:latest.release")
}

View file

@ -2,11 +2,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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.MANAGE_EXTERNAL_STORAGE"
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>
<!-- Translator -->

View file

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

View file

@ -1,7 +1,36 @@
package ua.acclorite.book_story
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import dagger.hilt.android.HiltAndroidApp
import ua.acclorite.book_story.data.local.notification.UpdatesNotificationService
@HiltAndroidApp
class Application : Application()
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 androidx.room.Room
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.local.room.BookDatabase
import ua.acclorite.book_story.data.remote.GithubAPI
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideGithubApi(): GithubAPI {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
return Retrofit.Builder()
.baseUrl(GithubAPI.BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
.create(GithubAPI::class.java)
}
@Provides
@Singleton

View file

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

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 ua.acclorite.book_story.R
import ua.acclorite.book_story.data.local.data_store.DataStore
import ua.acclorite.book_story.data.local.notification.UpdatesNotificationService
import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.data.remote.GithubAPI
import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook
@ -31,7 +34,7 @@ import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.data.MainSettingsState
import java.io.File
import java.io.FileOutputStream
import java.util.UUID
@ -44,6 +47,9 @@ class BookRepositoryImpl @Inject constructor(
private val database: BookDao,
private val dataStore: DataStore,
private val githubAPI: GithubAPI,
private val updatesNotificationService: UpdatesNotificationService,
private val bookMapper: BookMapper,
private val historyMapper: HistoryMapper,
@ -385,8 +391,8 @@ class BookRepositoryImpl @Inject constructor(
dataStore.putData(key, value)
}
override suspend fun getAllSettings(scope: CoroutineScope): MainState {
val result = CompletableDeferred<MainState>()
override suspend fun getAllSettings(scope: CoroutineScope): MainSettingsState {
val result = CompletableDeferred<MainSettingsState>()
scope.launch {
val keys = dataStore.getAllData()
@ -405,7 +411,7 @@ class BookRepositoryImpl @Inject constructor(
}
jobs?.awaitAll()
result.complete(MainState.initialize(data))
result.complete(MainSettingsState.initialize(data))
}
return result.await()
@ -601,6 +607,28 @@ class BookRepositoryImpl @Inject constructor(
override suspend fun deleteHistory(history: List<History>) {
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.data.MainSettingsState
import java.io.File
interface BookRepository {
@ -57,7 +58,7 @@ interface BookRepository {
value: T
)
suspend fun getAllSettings(scope: CoroutineScope): MainState
suspend fun getAllSettings(scope: CoroutineScope): MainSettingsState
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
@ -76,4 +77,6 @@ interface BookRepository {
suspend fun deleteHistory(
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 ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.data.MainSettingsState
import javax.inject.Inject
class GetAllSettings @Inject constructor(
private val repository: BookRepository
) {
suspend fun execute(scope: CoroutineScope): MainState {
suspend fun execute(scope: CoroutineScope): MainSettingsState {
return repository.getAllSettings(scope)
}
}

View file

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

View file

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

View file

@ -14,9 +14,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.collectLatest
/**
* Collapsible Scroll Behavior with Lazy List State.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState(
fun TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState(
): Pair<TopAppBarScrollBehavior, LazyListState> {
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 ua.acclorite.book_story.R
/**
* Custom Cover Image. Has smooth appearing.
*/
@Composable
fun CustomCoverImage(
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
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@ -82,7 +81,7 @@ fun CustomSnackbar(modifier: Modifier = Modifier, snackbarState: SnackbarHostSta
showSnackbar = true
}
AnimatedVisibility(
CustomAnimatedVisibility(
visible = showSnackbar,
enter = slideInVertically(tween(350)) { it } + fadeIn(tween(350)),
exit = slideOutVertically(tween(200)) { it } + fadeOut(tween(100))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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.
*/
@Immutable
data class MainState(
data class MainSettingsState(
val language: String? = null,
val theme: Theme? = null,
val darkTheme: DarkTheme? = null,
@ -36,6 +36,7 @@ data class MainState(
val backgroundColor: Long? = null,
val fontColor: Long? = null,
val showStartScreen: Boolean? = null,
val checkForUpdates: Boolean? = null,
) : Parcelable {
constructor(parcel: Parcel) : this(
// String
@ -68,6 +69,9 @@ data class MainState(
// Boolean
showStartScreen = if (parcel.readByte().toInt() != 0) parcel.readByte()
.toInt() == 1 else null,
// Boolean
checkForUpdates = if (parcel.readByte().toInt() != 0) parcel.readByte()
.toInt() == 1 else null,
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
@ -107,25 +111,28 @@ data class MainState(
// Show Start Screen
parcel.writeByte((if (showStartScreen != null) 1 else 0).toByte())
showStartScreen?.let { parcel.writeByte(if (it) 1 else 0) }
// Check For Updates
parcel.writeByte((if (checkForUpdates != null) 1 else 0).toByte())
checkForUpdates?.let { parcel.writeByte(if (it) 1 else 0) }
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<MainState> {
override fun createFromParcel(parcel: Parcel): MainState {
return MainState(parcel)
companion object CREATOR : Parcelable.Creator<MainSettingsState> {
override fun createFromParcel(parcel: Parcel): MainSettingsState {
return MainSettingsState(parcel)
}
override fun newArray(size: Int): Array<MainState?> {
override fun newArray(size: Int): Array<MainSettingsState?> {
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 {
val language: String = data[LANGUAGE.name] as? String ?: if (
@ -176,8 +183,10 @@ data class MainState(
val paragraphIndentation: Boolean = data[PARAGRAPH_INDENTATION.name] as? Boolean
?: false
val checkForUpdates: Boolean = data[CHECK_FOR_UPDATES.name] as? Boolean
?: false
return MainState(
return MainSettingsState(
language = language,
theme = theme.toTheme(),
darkTheme = darkTheme.toDarkTheme(),
@ -191,7 +200,8 @@ data class MainState(
fontSize = fontSize,
lineHeight = lineHeight,
paragraphHeight = paragraphHeight,
paragraphIndentation = paragraphIndentation
paragraphIndentation = paragraphIndentation,
checkForUpdates = checkForUpdates
)
}
}

View file

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

View file

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

View file

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

View file

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

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
import android.content.Context
import androidx.compose.runtime.Immutable
@Immutable
sealed class AboutEvent {
data class OnNavigateToBrowserPage(
val page: String,
val context: Context,
val noAppsFound: () -> Unit
) : AboutEvent()
data class OnCheckForUpdates(
val context: Context,
val noUpdatesFound: () -> Unit,
val error: () -> Unit
) : AboutEvent()
data object OnDismissUpdateDialog : AboutEvent()
}

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.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.use_case.CheckForUpdates
import javax.inject.Inject
@HiltViewModel
class AboutViewModel @Inject constructor(
private val checkForUpdates: CheckForUpdates,
) : ViewModel() {
private val _state = MutableStateFlow(AboutState())
val state = _state.asStateFlow()
fun onEvent(event: AboutEvent) {
when (event) {
is AboutEvent.OnNavigateToBrowserPage -> {
@ -30,6 +38,40 @@ class AboutViewModel @Inject constructor(
event.noAppsFound()
}
}
is AboutEvent.OnCheckForUpdates -> {
viewModelScope.launch {
val result = checkForUpdates.execute(false)
if (result == null) {
event.error()
return@launch
}
val version = result.tag_name.substringAfterLast("v")
val currentVersion = event.context.getString(R.string.app_version)
if (version == currentVersion) {
event.noUpdatesFound()
return@launch
}
_state.update {
it.copy(
showUpdateDialog = true,
updateInfo = result
)
}
}
}
is AboutEvent.OnDismissUpdateDialog -> {
_state.update {
it.copy(
showUpdateDialog = false
)
}
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,19 +18,16 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
/**
* Change cover bottom sheet. Lets user select photo from the gallery and replaces old one.
@ -38,26 +35,25 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BookInfoChangeCoverBottomSheet(
libraryViewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
viewModel: BookInfoViewModel
state: State<BookInfoState>,
onEvent: (BookInfoEvent) -> Unit,
onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit
) {
val state by viewModel.state.collectAsState()
val context = LocalContext.current
val navigationBarPadding =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
val photoPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
if (uri != null) {
viewModel.onEvent(
onEvent(
BookInfoEvent.OnChangeCover(
uri,
context,
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
}
)
)
@ -73,7 +69,7 @@ fun BookInfoChangeCoverBottomSheet(
ModalBottomSheet(
modifier = Modifier.fillMaxWidth(),
onDismissRequest = {
viewModel.onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
},
sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0),
@ -90,17 +86,17 @@ fun BookInfoChangeCoverBottomSheet(
)
}
if (state.book.coverImage != null) {
if (state.value.book.coverImage != null) {
BookInfoChangeCoverBottomSheetItem(
icon = Icons.Default.HideImage,
text = stringResource(id = R.string.delete_cover),
description = stringResource(id = R.string.delete_cover_desc)
) {
viewModel.onEvent(
onEvent(
BookInfoEvent.OnDeleteCover(
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
}
)
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,22 @@
package ua.acclorite.book_story.presentation.screens.help
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.RestartAlt
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
@ -16,35 +24,73 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.components.collapsibleScrollBehaviorWithLazyListState
import ua.acclorite.book_story.presentation.components.collapsibleUntilExitScrollBehaviorWithLazyListState
import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpAddBooksItem
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpClickMeNoteItem
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpCustomizeApp
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpCustomizeReader
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpEditBook
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpFindBooksItem
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpManageHistory
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpMoveOrDeleteBooks
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpReadBook
import ua.acclorite.book_story.presentation.screens.help.components.items.HelpUpdateBook
import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent
import ua.acclorite.book_story.presentation.screens.help.data.HelpState
import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
import ua.acclorite.book_story.presentation.screens.start.data.StartEvent
import ua.acclorite.book_story.presentation.screens.start.data.StartViewModel
@Composable
fun HelpScreenRoot() {
val navigator = LocalNavigator.current
val helpViewModel: HelpViewModel = hiltViewModel()
val mainViewModel: MainViewModel = hiltViewModel()
val startViewModel: StartViewModel = hiltViewModel()
val state = helpViewModel.state.collectAsState()
LaunchedEffect(Unit) {
helpViewModel.init(navigator)
}
HelpScreen(
state = state,
navigator = navigator,
onEvent = helpViewModel::onEvent,
onMainEvent = mainViewModel::onEvent,
onStartEvent = startViewModel::onEvent
)
}
@OptIn(
ExperimentalMaterial3Api::class
)
@Composable
fun HelpScreen(
viewModel: HelpViewModel = hiltViewModel(),
navigator: Navigator
private fun HelpScreen(
state: State<HelpState>,
navigator: Navigator,
onEvent: (HelpEvent) -> Unit,
onMainEvent: (MainEvent) -> Unit,
onStartEvent: (StartEvent) -> Unit
) {
val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
Scaffold(
Modifier
@ -58,7 +104,23 @@ fun HelpScreen(
Text(stringResource(id = R.string.help_screen))
},
navigationIcon = {
GoBackButton(navigator = navigator)
if (!state.value.fromStart) {
GoBackButton(navigator = navigator)
}
},
actions = {
if (!state.value.fromStart) {
CustomIconButton(
icon = Icons.Default.RestartAlt,
contentDescription = R.string.reset_start_content_desc,
disableOnClick = false
) {
onStartEvent(StartEvent.OnResetStartScreen)
onMainEvent(MainEvent.OnChangeShowStartScreen(true))
navigator.navigateWithoutBackStack(Screen.START, false)
navigator.clearBackStack()
}
}
},
scrollBehavior = scrollState.first,
colors = TopAppBarDefaults.largeTopAppBarColors(
@ -66,14 +128,37 @@ fun HelpScreen(
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
)
)
},
bottomBar = {
if (state.value.fromStart) {
Column {
Spacer(modifier = Modifier.height(18.dp))
Button(
modifier = Modifier
.navigationBarsPadding()
.padding(bottom = 8.dp)
.padding(horizontal = 18.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(100),
onClick = {
navigator.clearArgument("from_start")
onMainEvent(MainEvent.OnChangeShowStartScreen(false))
navigator.navigateWithoutBackStack(Screen.BROWSE, false)
}
) {
Text(text = stringResource(id = R.string.done))
}
}
}
}
) { paddingValues ->
LazyColumn(
Modifier
.fillMaxSize()
.padding(top = paddingValues.calculateTopPadding(), start = 18.dp, end = 18.dp)
.padding(paddingValues)
.imePadding(),
state = scrollState.second
state = scrollState.second,
contentPadding = PaddingValues(horizontal = 18.dp)
) {
item {
Spacer(
@ -84,7 +169,10 @@ fun HelpScreen(
}
item {
HelpClickMeNoteItem(viewModel = viewModel)
HelpClickMeNoteItem(
state = state,
onEvent = onEvent
)
}
item {
@ -95,54 +183,66 @@ fun HelpScreen(
)
}
item {
HelpFindBooksItem(viewModel = viewModel)
HelpFindBooksItem(
state = state,
onEvent = onEvent
)
}
item {
HelpAddBooksItem(navigator = navigator, viewModel = viewModel)
HelpAddBooksItem(
state = state,
onEvent = onEvent
)
}
item {
HelpCustomizeApp(
navigator = navigator,
viewModel = viewModel
state = state,
onEvent = onEvent
)
}
item {
HelpMoveOrDeleteBooks(
navigator = navigator,
viewModel = viewModel
state = state,
onEvent = onEvent
)
}
item {
HelpEditBook(
navigator = navigator,
viewModel = viewModel
state = state,
onEvent = onEvent
)
}
item {
HelpReadBook(
navigator = navigator,
viewModel = viewModel
state = state,
onEvent = onEvent
)
}
item {
HelpCustomizeReader(
navigator = navigator,
viewModel = viewModel
state = state,
onEvent = onEvent
)
}
item {
HelpUpdateBook(
navigator = navigator,
viewModel = viewModel
state = state,
onEvent = onEvent
)
}
item {
HelpManageHistory(
state = state,
onEvent = onEvent
)
}

View file

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

View file

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

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

View file

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

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

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

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

View file

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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