0.9.8 - StartScreen + Check for updates + Bug fixes + Lot more...
This commit is contained in:
parent
513b286e07
commit
b5c2399034
129 changed files with 3944 additions and 1535 deletions
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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/"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// }
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package ua.acclorite.book_story.presentation.screens.settings.nested.general
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -15,30 +18,62 @@ import androidx.compose.material3.Scaffold
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.ButtonItem
|
||||
import ua.acclorite.book_story.domain.util.Constants
|
||||
import ua.acclorite.book_story.presentation.components.GoBackButton
|
||||
import ua.acclorite.book_story.presentation.components.collapsibleScrollBehaviorWithLazyListState
|
||||
import ua.acclorite.book_story.presentation.components.collapsibleUntilExitScrollBehaviorWithLazyListState
|
||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainSettingsState
|
||||
import ua.acclorite.book_story.presentation.data.MainViewModel
|
||||
import ua.acclorite.book_story.presentation.data.Navigator
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.CheckboxWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent
|
||||
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun GeneralSettings(
|
||||
mainViewModel: MainViewModel,
|
||||
navigator: Navigator
|
||||
fun GeneralSettingsRoot() {
|
||||
val navigator = LocalNavigator.current
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val mainViewModel: MainViewModel = hiltViewModel()
|
||||
|
||||
val state = mainViewModel.state.collectAsState()
|
||||
|
||||
GeneralSettings(
|
||||
state = state,
|
||||
navigator = navigator,
|
||||
onSettingsEvent = settingsViewModel::onEvent,
|
||||
onMainEvent = mainViewModel::onEvent
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
private fun GeneralSettings(
|
||||
state: State<MainSettingsState>,
|
||||
navigator: Navigator,
|
||||
onSettingsEvent: (SettingsEvent) -> Unit,
|
||||
onMainEvent: (MainEvent) -> Unit
|
||||
) {
|
||||
val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
|
||||
val state by mainViewModel.state.collectAsState()
|
||||
val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
|
||||
val activity = LocalContext.current as ComponentActivity
|
||||
|
||||
val notificationsPermissionState = rememberPermissionState(
|
||||
permission = Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
Modifier
|
||||
|
|
@ -79,11 +114,11 @@ fun GeneralSettings(
|
|||
it.first,
|
||||
it.second,
|
||||
MaterialTheme.typography.labelLarge,
|
||||
it.first == state.language
|
||||
it.first == state.value.language
|
||||
)
|
||||
}.sortedBy { it.title }
|
||||
) {
|
||||
mainViewModel.onEvent(
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeLanguage(
|
||||
it.id
|
||||
)
|
||||
|
|
@ -91,6 +126,29 @@ fun GeneralSettings(
|
|||
}
|
||||
}
|
||||
|
||||
item {
|
||||
CheckboxWithTitle(
|
||||
selected = state.value.checkForUpdates!!,
|
||||
title = stringResource(id = R.string.check_for_updates_option),
|
||||
description = stringResource(id = R.string.check_for_updates_option_desc)
|
||||
) {
|
||||
onSettingsEvent(
|
||||
SettingsEvent.OnGeneralChangeCheckForUpdates(
|
||||
enable = !state.value.checkForUpdates!!,
|
||||
activity = activity,
|
||||
notificationsPermissionState = notificationsPermissionState,
|
||||
onChangeCheckForUpdates = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeCheckForUpdates(
|
||||
it
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(48.dp)) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,39 +15,56 @@ import androidx.compose.material3.Scaffold
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.ButtonItem
|
||||
import ua.acclorite.book_story.domain.util.Constants
|
||||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
||||
import ua.acclorite.book_story.presentation.components.GoBackButton
|
||||
import ua.acclorite.book_story.presentation.components.collapsibleScrollBehaviorWithLazyListState
|
||||
import ua.acclorite.book_story.presentation.components.collapsibleUntilExitScrollBehaviorWithLazyListState
|
||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainSettingsState
|
||||
import ua.acclorite.book_story.presentation.data.MainViewModel
|
||||
import ua.acclorite.book_story.presentation.data.Navigator
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.CheckboxWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle
|
||||
|
||||
@Composable
|
||||
fun ReaderSettingsRoot() {
|
||||
val navigator = LocalNavigator.current
|
||||
val mainViewModel: MainViewModel = hiltViewModel()
|
||||
|
||||
val state = mainViewModel.state.collectAsState()
|
||||
|
||||
ReaderSettings(
|
||||
state = state,
|
||||
navigator = navigator,
|
||||
onMainEvent = mainViewModel::onEvent
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ReaderSettings(
|
||||
mainViewModel: MainViewModel,
|
||||
navigator: Navigator
|
||||
private fun ReaderSettings(
|
||||
state: State<MainSettingsState>,
|
||||
navigator: Navigator,
|
||||
onMainEvent: (MainEvent) -> Unit
|
||||
) {
|
||||
val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
|
||||
val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
|
||||
|
||||
val state by mainViewModel.state.collectAsState()
|
||||
val fontFamily = remember(state.fontFamily) {
|
||||
val fontFamily = remember(state.value.fontFamily) {
|
||||
Constants.FONTS.find {
|
||||
it.id == state.fontFamily
|
||||
it.id == state.value.fontFamily
|
||||
} ?: Constants.FONTS[0]
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +123,7 @@ fun ReaderSettings(
|
|||
)
|
||||
},
|
||||
onClick = {
|
||||
mainViewModel.onEvent(MainEvent.OnChangeFontFamily(it.id))
|
||||
onMainEvent(MainEvent.OnChangeFontFamily(it.id))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -121,7 +138,7 @@ fun ReaderSettings(
|
|||
fontFamily = fontFamily.font,
|
||||
fontStyle = FontStyle.Normal
|
||||
),
|
||||
!state.isItalic!!
|
||||
!state.value.isItalic!!
|
||||
),
|
||||
ButtonItem(
|
||||
"italic",
|
||||
|
|
@ -130,11 +147,11 @@ fun ReaderSettings(
|
|||
fontFamily = fontFamily.font,
|
||||
fontStyle = FontStyle.Italic
|
||||
),
|
||||
state.isItalic!!
|
||||
state.value.isItalic!!
|
||||
),
|
||||
),
|
||||
onClick = {
|
||||
mainViewModel.onEvent(
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeFontStyle(
|
||||
when (it.id) {
|
||||
"italic" -> true
|
||||
|
|
@ -147,12 +164,12 @@ fun ReaderSettings(
|
|||
}
|
||||
item {
|
||||
SliderWithTitle(
|
||||
value = state.fontSize!! to "pt",
|
||||
value = state.value.fontSize!! to "pt",
|
||||
fromValue = 10,
|
||||
toValue = 35,
|
||||
title = stringResource(id = R.string.font_size_option),
|
||||
onValueChange = {
|
||||
mainViewModel.onEvent(
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeFontSize(it)
|
||||
)
|
||||
}
|
||||
|
|
@ -172,12 +189,12 @@ fun ReaderSettings(
|
|||
}
|
||||
item {
|
||||
SliderWithTitle(
|
||||
value = state.lineHeight!! to "pt",
|
||||
value = state.value.lineHeight!! to "pt",
|
||||
fromValue = 1,
|
||||
toValue = 16,
|
||||
title = stringResource(id = R.string.line_height_option),
|
||||
onValueChange = {
|
||||
mainViewModel.onEvent(
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeLineHeight(it)
|
||||
)
|
||||
}
|
||||
|
|
@ -185,12 +202,12 @@ fun ReaderSettings(
|
|||
}
|
||||
item {
|
||||
SliderWithTitle(
|
||||
value = state.paragraphHeight!! to "pt",
|
||||
value = state.value.paragraphHeight!! to "pt",
|
||||
fromValue = 0,
|
||||
toValue = 24,
|
||||
title = stringResource(id = R.string.paragraph_height_option),
|
||||
onValueChange = {
|
||||
mainViewModel.onEvent(
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeParagraphHeight(it)
|
||||
)
|
||||
}
|
||||
|
|
@ -198,11 +215,11 @@ fun ReaderSettings(
|
|||
}
|
||||
item {
|
||||
CheckboxWithTitle(
|
||||
selected = state.paragraphIndentation!!,
|
||||
selected = state.value.paragraphIndentation!!,
|
||||
title = stringResource(id = R.string.paragraph_indentation_option)
|
||||
) {
|
||||
mainViewModel.onEvent(
|
||||
MainEvent.OnChangeParagraphIndentation(!state.paragraphIndentation!!)
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeParagraphIndentation(!state.value.paragraphIndentation!!)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue