0.7.0 - Reader Screen

This commit is contained in:
acclorite 2024-03-01 02:11:34 +02:00
parent 03b1ef48c2
commit 0b3a31a183
63 changed files with 2475 additions and 468 deletions

View file

@ -28,34 +28,15 @@ android {
buildTypes {
getByName("release") {
// Enables code shrinking, obfuscation, and optimization for only
// your project's release build type. Make sure to use a build
// variant with `isDebuggable=false`.
isMinifyEnabled = true
// Enables resource shrinking, which is performed by the
// Android Gradle plugin.
isShrinkResources = true
// Includes the default ProGuard rules files that are packaged with
// the Android Gradle plugin. To learn more, go to the section about
// R8 configuration files.
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
// release {
// isMinifyEnabled = false
// proguardFiles(
// getDefaultProguardFile("proguard-android-optimize.txt"),
// "proguard-rules.pro"
// )
// signingConfig = signingConfigs.getByName("debug")
// }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17

View file

@ -22,11 +22,13 @@ import com.acclorite.books_history.domain.model.Book
import com.acclorite.books_history.presentation.NavigationHost
import com.acclorite.books_history.presentation.Screen
import com.acclorite.books_history.presentation.components.BottomNavigationBar
import com.acclorite.books_history.presentation.main.MainEvent
import com.acclorite.books_history.presentation.main.MainViewModel
import com.acclorite.books_history.presentation.data.MainEvent
import com.acclorite.books_history.presentation.data.MainViewModel
import com.acclorite.books_history.presentation.screens.book_info.BookInfoScreen
import com.acclorite.books_history.presentation.screens.browse.BrowseScreen
import com.acclorite.books_history.presentation.screens.history.HistoryScreen
import com.acclorite.books_history.presentation.screens.library.LibraryScreen
import com.acclorite.books_history.presentation.screens.reader.ReaderScreen
import com.acclorite.books_history.presentation.screens.settings.SettingsScreen
import com.acclorite.books_history.presentation.screens.settings.nested.appearance.AppearanceSettings
import com.acclorite.books_history.presentation.screens.settings.nested.general.GeneralSettings
@ -93,6 +95,10 @@ class MainActivity : ComponentActivity() {
)
}
composable(screen = Screen.HISTORY) {
HistoryScreen(navigator = this@NavigationHost)
}
composable(screen = Screen.BROWSE) {
BrowseScreen(navigator = this@NavigationHost)
}
@ -106,11 +112,20 @@ class MainActivity : ComponentActivity() {
enterAnim = Transitions.SlidingTransitionIn,
exitAnim = Transitions.SlidingTransitionOut
) {
BookInfoScreen(
navigator = this@NavigationHost
)
}
composable(
screen = Screen.READER,
enterAnim = Transitions.SlidingTransitionIn,
exitAnim = Transitions.SlidingTransitionOut
) {
ReaderScreen(
navigator = this@NavigationHost,
mainViewModel = mainViewModel
)
}
// Settings
composable(
@ -119,7 +134,6 @@ class MainActivity : ComponentActivity() {
exitAnim = Transitions.SlidingTransitionOut
) {
SettingsScreen(
viewModel = mainViewModel,
navigator = this@NavigationHost
)
}

View file

@ -0,0 +1,12 @@
package com.acclorite.books_history.domain.model
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.TextStyle
@Immutable
data class ChipItem(
val id: String,
val title: String,
val textStyle: TextStyle,
val selected: Boolean
)

View file

@ -2,9 +2,11 @@ package com.acclorite.books_history.domain.model
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.font.FontFamily
import com.acclorite.books_history.util.UIText
@Immutable
data class FontWithName(
val fontName: String,
val id: String,
val fontName: UIText,
val font: FontFamily
)

View file

@ -0,0 +1,15 @@
package com.acclorite.books_history.domain.use_case
import com.acclorite.books_history.domain.model.StringWithId
import com.acclorite.books_history.domain.repository.BookRepository
import com.acclorite.books_history.util.Resource
import kotlinx.coroutines.flow.Flow
import java.io.File
import javax.inject.Inject
class GetText @Inject constructor(private val repository: BookRepository) {
suspend fun execute(file: File): Flow<Resource<List<StringWithId>>> {
return repository.getBookTextFromFile(file)
}
}

View file

@ -75,7 +75,7 @@ class Navigator @AssistedInject constructor(
private val backStack = savedStateHandle.getStateFlow(BACKSTACK, mutableListOf<Screen>())
private val arguments = mutableStateListOf<Argument>()
private fun putArgument(argument: Argument) {
fun putArgument(argument: Argument) {
var found = false
for ((index, arg) in arguments.withIndex()) {
@ -115,6 +115,19 @@ class Navigator @AssistedInject constructor(
savedStateHandle[CURRENT_SCREEN] = screen
}
fun navigateWithoutBackStack(screen: Screen, vararg args: Argument) =
viewModelScope.launch(Dispatchers.Default) {
if (backStack.value.last() == screen) {
backStack.value.removeLast()
}
args.forEach {
putArgument(it)
}
savedStateHandle[CURRENT_SCREEN] = screen
}
fun navigateBack() = viewModelScope.launch(Dispatchers.Default) {
if (canGoBack()) {
savedStateHandle[CURRENT_SCREEN] = backStack.value.last()
@ -126,10 +139,6 @@ class Navigator @AssistedInject constructor(
return backStack.value.isNotEmpty()
}
fun clearBackStack() {
backStack.value.clear()
}
fun getCurrentScreen(): StateFlow<Screen> {
return currentScreen
}

View file

@ -0,0 +1,36 @@
package com.acclorite.books_history.presentation.components
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.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun CategoryTitle(
modifier: Modifier = Modifier,
title: String,
color: Color = MaterialTheme.colorScheme.onSurfaceVariant,
padding: Dp = 18.dp
) {
Text(
title,
color = color,
modifier = modifier.padding(horizontal = padding),
style = TextStyle(
lineBreak = LineBreak.Heading,
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 20.sp
)
)
}

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
@ -60,6 +61,7 @@ fun CustomDialogWithContent(
)
Column(
modifier = modifier
.fillMaxWidth()
.navigationBarsPadding()
.statusBarsPadding()
.clip(MaterialTheme.shapes.extraLarge)

View file

@ -62,6 +62,7 @@ fun CustomDialogWithLazyColumn(
)
Column(
modifier = modifier
.fillMaxWidth()
.navigationBarsPadding()
.statusBarsPadding()
.clip(MaterialTheme.shapes.extraLarge)

View file

@ -1,4 +1,4 @@
package com.acclorite.books_history.util
package com.acclorite.books_history.presentation.data
fun String.removeTrailingZero(): String {
if (!this.contains('.'))

View file

@ -0,0 +1,19 @@
package com.acclorite.books_history.presentation.data
import androidx.activity.ComponentActivity
sealed class MainEvent {
data class OnLocaleUpdate(val activity: ComponentActivity) : MainEvent()
data class OnChangeLanguage(val lang: String, val activity: ComponentActivity) : MainEvent()
data class OnChangeTheme(val theme: String) : MainEvent()
data class OnChangeDarkTheme(val darkTheme: String) : MainEvent()
data class OnChangeFontFamily(val fontFamily: String) : MainEvent()
data class OnChangeFontStyle(val fontStyle: Boolean) : MainEvent()
data class OnChangeFontSize(val fontSize: Int) : MainEvent()
data class OnChangeLineHeight(val lineHeight: Int) : MainEvent()
data class OnChangeParagraphHeight(val paragraphHeight: Int) : MainEvent()
data class OnChangeParagraphIndentation(val bool: Boolean) : MainEvent()
data class OnChangeBackgroundColor(val color: Long) : MainEvent()
data class OnChangeFontColor(val color: Long) : MainEvent()
}

View file

@ -1,4 +1,4 @@
package com.acclorite.books_history.presentation.main
package com.acclorite.books_history.presentation.data
import android.os.Build
import androidx.activity.ComponentActivity
@ -109,7 +109,7 @@ class MainViewModel @Inject constructor(
/* -- Font Family -------------------------------------------------- */
private var _fontFamily: String =
stateHandle[Constants.FONT] ?: Constants.FONTS[0].fontName
stateHandle[Constants.FONT] ?: Constants.FONTS[0].id
set(value) {
field = value
stateHandle[Constants.FONT] = value
@ -204,6 +204,63 @@ class MainViewModel @Inject constructor(
_theme = event.theme.toTheme()
}
}
is MainEvent.OnChangeFontFamily -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT, event.fontFamily)
_fontFamily = Constants.FONTS.find { font -> font.id == event.fontFamily }?.id
?: Constants.FONTS[0].id
}
}
is MainEvent.OnChangeFontStyle -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.IS_ITALIC, event.fontStyle)
_isItalic = event.fontStyle
}
}
is MainEvent.OnChangeFontSize -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT_SIZE, event.fontSize)
_fontSize = event.fontSize
}
}
is MainEvent.OnChangeLineHeight -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.LINE_HEIGHT, event.lineHeight)
_lineHeight = event.lineHeight
}
}
is MainEvent.OnChangeParagraphHeight -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.PARAGRAPH_HEIGHT, event.paragraphHeight)
_paragraphHeight = event.paragraphHeight
}
}
is MainEvent.OnChangeParagraphIndentation -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.PARAGRAPH_INDENTATION, event.bool)
_paragraphIndentation = event.bool
}
}
is MainEvent.OnChangeBackgroundColor -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.BACKGROUND_COLOR, event.color)
_backgroundColor = event.color
}
}
is MainEvent.OnChangeFontColor -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT_COLOR, event.color)
_fontColor = event.color
}
}
}
}
@ -271,9 +328,7 @@ class MainViewModel @Inject constructor(
getDatastore
.execute(DataStoreConstants.BACKGROUND_COLOR, _backgroundColor)
.first {
setDatastore.execute(DataStoreConstants.BACKGROUND_COLOR, it)
_backgroundColor = it
onEvent(MainEvent.OnChangeBackgroundColor(it))
true
}
}
@ -283,9 +338,7 @@ class MainViewModel @Inject constructor(
getDatastore
.execute(DataStoreConstants.FONT_COLOR, _fontColor)
.first {
setDatastore.execute(DataStoreConstants.FONT_COLOR, it)
_fontColor = it
onEvent(MainEvent.OnChangeFontColor(it))
true
}
}
@ -295,10 +348,7 @@ class MainViewModel @Inject constructor(
getDatastore
.execute(DataStoreConstants.FONT, _fontFamily)
.first {
setDatastore.execute(DataStoreConstants.FONT, it)
_fontFamily = Constants.FONTS.find { font -> font.fontName == it }?.fontName
?: Constants.FONTS[0].fontName
onEvent(MainEvent.OnChangeFontFamily(it))
true
}
}
@ -308,9 +358,7 @@ class MainViewModel @Inject constructor(
getDatastore
.execute(DataStoreConstants.IS_ITALIC, _isItalic)
.first {
setDatastore.execute(DataStoreConstants.IS_ITALIC, it)
_isItalic = it
onEvent(MainEvent.OnChangeFontStyle(it))
true
}
}
@ -320,9 +368,7 @@ class MainViewModel @Inject constructor(
getDatastore
.execute(DataStoreConstants.FONT_SIZE, _fontSize)
.first {
setDatastore.execute(DataStoreConstants.FONT_SIZE, it)
_fontSize = it
onEvent(MainEvent.OnChangeFontSize(it))
it > 0
}
}
@ -332,9 +378,7 @@ class MainViewModel @Inject constructor(
getDatastore
.execute(DataStoreConstants.LINE_HEIGHT, _lineHeight)
.first {
setDatastore.execute(DataStoreConstants.LINE_HEIGHT, it)
_lineHeight = it
onEvent(MainEvent.OnChangeLineHeight(it))
true
}
}
@ -344,9 +388,7 @@ class MainViewModel @Inject constructor(
getDatastore
.execute(DataStoreConstants.PARAGRAPH_HEIGHT, _paragraphHeight)
.first {
setDatastore.execute(DataStoreConstants.PARAGRAPH_HEIGHT, it)
_paragraphHeight = it
onEvent(MainEvent.OnChangeParagraphHeight(it))
true
}
}
@ -356,9 +398,7 @@ class MainViewModel @Inject constructor(
getDatastore
.execute(DataStoreConstants.PARAGRAPH_INDENTATION, _paragraphIndentation)
.first {
setDatastore.execute(DataStoreConstants.PARAGRAPH_INDENTATION, it)
_paragraphIndentation = it
onEvent(MainEvent.OnChangeParagraphIndentation(it))
true
}
}

View file

@ -1,11 +0,0 @@
package com.acclorite.books_history.presentation.main
import androidx.activity.ComponentActivity
sealed class MainEvent() {
data class OnChangeLanguage(val lang: String, val activity: ComponentActivity) : MainEvent()
data class OnLocaleUpdate(val activity: ComponentActivity) : MainEvent()
data class OnChangeTheme(val theme: String) : MainEvent()
data class OnChangeDarkTheme(val darkTheme: String) : MainEvent()
}

View file

@ -12,14 +12,15 @@ import androidx.compose.foundation.layout.Row
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.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.Done
@ -37,7 +38,9 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -45,6 +48,7 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.input.nestedscroll.nestedScroll
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 com.acclorite.books_history.R
@ -95,7 +99,9 @@ fun BookInfoScreen(
val state by viewModel.state.collectAsState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val scrollState = rememberScrollState()
val listState = rememberLazyListState()
val firstVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } }
LaunchedEffect(Unit) {
viewModel.init(
@ -154,7 +160,17 @@ fun BookInfoScreen(
)
}
},
title = {},
title = {
DefaultTransition(visible = firstVisibleItemIndex > 0) {
Text(
state.book.title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
},
actions = {
Box {
DefaultTransition(
@ -206,34 +222,44 @@ fun BookInfoScreen(
)
}
) { paddingValues ->
if (state.book.coverImage != null) {
BookInfoBackground(image = state.book.coverImage!!.asImageBitmap())
}
Box(
Modifier
.fillMaxSize()
.padding(top = paddingValues.calculateTopPadding())
) {
Column(
LazyColumn(
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(horizontal = 24.dp)
.fillMaxSize(),
state = listState
) {
item {
Box(modifier = Modifier.fillMaxWidth()) {
if (state.book.coverImage != null) {
BookInfoBackground(
height = paddingValues.calculateTopPadding() + 12.dp + 195.dp,
image = state.book.coverImage!!.asImageBitmap()
)
}
Spacer(modifier = Modifier.height(12.dp))
Column(Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding() + 12.dp))
// Info
BookInfoInfoSection(viewModel = viewModel, book = state.book)
}
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
// Statistic
BookInfoStatisticSection(book = state.book)
}
item {
Spacer(modifier = Modifier.height(24.dp))
// Description
BookInfoDescriptionSection(book = state.book)
}
}
if (state.book.file != null) {
FloatingActionButton(
@ -260,7 +286,7 @@ fun BookInfoScreen(
Modifier.size(24.dp)
)
AnimatedVisibility(
visible = scrollState.value < 5
visible = !listState.canScrollBackward
) {
Text(
text = stringResource(

View file

@ -1,45 +1,41 @@
package com.acclorite.books_history.presentation.screens.book_info.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun BookInfoBackground(image: ImageBitmap) {
fun BookInfoBackground(height: Dp, image: ImageBitmap) {
val background = MaterialTheme.colorScheme.background
Image(
bitmap = image,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.5f)
.blur(1.5.dp),
.height(height)
.drawWithContent {
drawContent()
drawRect(
brush = Brush.verticalGradient(
0f to Color.Transparent,
0.8f to background
)
)
}
.blur(2.dp),
alpha = 0.3f,
contentScale = ContentScale.Crop
)
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.51f)
.background(
Brush.verticalGradient(
listOf(
Color.Transparent,
MaterialTheme.colorScheme.surface
),
startY = 0f,
endY = 750f
)
)
)
}

View file

@ -6,6 +6,7 @@ import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HideImage
@ -13,6 +14,7 @@ import androidx.compose.material.icons.filled.ImageSearch
import androidx.compose.material3.ExperimentalMaterial3Api
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
@ -61,9 +63,11 @@ fun BookInfoChangeCoverBottomSheet(
)
ModalBottomSheet(
modifier = Modifier.fillMaxWidth(),
onDismissRequest = {
viewModel.onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
},
sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0),
containerColor = MaterialTheme.elevation(ElevationDefaults.BottomSheet)
) {

View file

@ -2,6 +2,7 @@ package com.acclorite.books_history.presentation.screens.book_info.components
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -21,7 +22,8 @@ fun BookInfoDescriptionSection(book: Book) {
?: stringResource(id = R.string.error_no_description),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Start
textAlign = TextAlign.Start,
modifier = Modifier.padding(horizontal = 24.dp)
)
}

View file

@ -3,10 +3,12 @@ package com.acclorite.books_history.presentation.screens.book_info.components
import android.widget.Toast
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.ExperimentalMaterial3Api
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
@ -31,7 +33,7 @@ fun BookInfoDetailsBottomSheet(
val state by viewModel.state.collectAsState()
val context = LocalContext.current
val pattern = SimpleDateFormat("HH:mm dd MMM", Locale.getDefault())
val pattern = SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault())
val lastOpened = pattern.format(Date(state.book.lastOpened ?: 0))
val sizeBytes = state.book.file?.length() ?: 0
@ -46,9 +48,11 @@ fun BookInfoDetailsBottomSheet(
ModalBottomSheet(
modifier = Modifier.fillMaxWidth(),
onDismissRequest = {
viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
},
sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0),
containerColor = MaterialTheme.elevation(ElevationDefaults.BottomSheet)
) {

View file

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
@ -54,6 +55,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
) {
Box(
modifier = Modifier

View file

@ -1,8 +1,10 @@
package com.acclorite.books_history.presentation.screens.book_info.components
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.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -14,11 +16,16 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.R
import com.acclorite.books_history.domain.model.Book
import com.acclorite.books_history.util.removeDigits
import com.acclorite.books_history.util.removeTrailingZero
import com.acclorite.books_history.presentation.data.removeDigits
import com.acclorite.books_history.presentation.data.removeTrailingZero
@Composable
fun BookInfoStatisticSection(book: Book) {
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
) {
Text(
text = "${
(book.progress * 100)
@ -58,4 +65,5 @@ fun BookInfoStatisticSection(book: Book) {
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}

View file

@ -10,7 +10,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import java.io.File
sealed class BrowseEvent() {
sealed class BrowseEvent {
data class OnStoragePermissionRequest(val activity: ComponentActivity) : BrowseEvent()
data class OnLegacyStoragePermissionRequest(val permissionState: PermissionState) :
BrowseEvent()

View file

@ -0,0 +1,16 @@
package com.acclorite.books_history.presentation.screens.history
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.hilt.navigation.compose.hiltViewModel
import com.acclorite.books_history.presentation.Navigator
import com.acclorite.books_history.presentation.screens.history.data.HistoryViewModel
@Composable
fun HistoryScreen(
viewModel: HistoryViewModel = hiltViewModel(),
navigator: Navigator
) {
val state by viewModel.state.collectAsState()
}

View file

@ -0,0 +1,3 @@
package com.acclorite.books_history.presentation.screens.history.data
sealed class HistoryEvent

View file

@ -0,0 +1,5 @@
package com.acclorite.books_history.presentation.screens.history.data
data class HistoryState(
val foo: Int = 0
)

View file

@ -0,0 +1,36 @@
package com.acclorite.books_history.presentation.screens.history.data
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@HiltViewModel
class HistoryViewModel @Inject constructor(
) : ViewModel() {
private val _state = MutableStateFlow(HistoryState())
val state = _state.asStateFlow()
fun onEvent(event: HistoryEvent) {
//
}
}

View file

@ -343,7 +343,12 @@ fun LibraryScreen(
viewModel.onEvent(LibraryEvent.OnSelectBook(it, true))
}
},
onButtonClick = { navigator.navigate(Screen.READER) }
onButtonClick = {
navigator.navigate(
Screen.READER,
Argument("book", it.first)
)
}
)
}
}

View file

@ -36,9 +36,9 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.domain.model.Book
import com.acclorite.books_history.presentation.data.removeDigits
import com.acclorite.books_history.presentation.data.removeTrailingZero
import com.acclorite.books_history.ui.elevation
import com.acclorite.books_history.util.removeDigits
import com.acclorite.books_history.util.removeTrailingZero
@OptIn(ExperimentalFoundationApi::class)
@Composable

View file

@ -26,4 +26,5 @@ sealed class LibraryEvent {
data class OnMoveBooks(val pagerState: PagerState) : LibraryEvent()
data object OnShowHideDeleteDialog : LibraryEvent()
data object OnDeleteBooks : LibraryEvent()
data class OnUpdateBook(val book: Book) : LibraryEvent()
}

View file

@ -257,6 +257,21 @@ class LibraryViewModel @Inject constructor(
onEvent(LibraryEvent.OnClearSelectedBooks)
}
}
is LibraryEvent.OnUpdateBook -> {
val books = _state.value.books.toMutableList()
val index = books.indexOfFirst { it.first.id == event.book.id }
if (index != -1) {
books[index] = Pair(event.book, books[index].second)
}
_state.update {
it.copy(
books = books
)
}
}
}
}

View file

@ -0,0 +1,324 @@
package com.acclorite.books_history.presentation.screens.reader
import android.annotation.SuppressLint
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
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.fillMaxSize
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.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
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.TextStyle
import androidx.compose.ui.text.font.FontStyle
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 com.acclorite.books_history.R
import com.acclorite.books_history.domain.model.Book
import com.acclorite.books_history.presentation.Navigator
import com.acclorite.books_history.presentation.components.IsError
import com.acclorite.books_history.presentation.data.MainViewModel
import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
import com.acclorite.books_history.presentation.screens.reader.components.ReaderBottomBar
import com.acclorite.books_history.presentation.screens.reader.components.ReaderEndItem
import com.acclorite.books_history.presentation.screens.reader.components.ReaderSettingsBottomSheet
import com.acclorite.books_history.presentation.screens.reader.components.ReaderStartItem
import com.acclorite.books_history.presentation.screens.reader.components.ReaderTopBar
import com.acclorite.books_history.presentation.screens.reader.data.ReaderEvent
import com.acclorite.books_history.presentation.screens.reader.data.ReaderViewModel
import com.acclorite.books_history.ui.elevation
import com.acclorite.books_history.util.Constants
import dagger.hilt.android.lifecycle.withCreationCallback
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
@OptIn(FlowPreview::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun ReaderScreen(
mainViewModel: MainViewModel,
navigator: Navigator
) {
val context = LocalContext.current as ComponentActivity
val systemBarsColor = MaterialTheme.elevation(20.dp).copy(0.85f)
val viewModel by context.viewModels<ReaderViewModel>(
extrasProducer = {
val book = navigator.retrieveArgument("book") as? Book
if (book == null) {
navigator.navigateBack()
}
context.defaultViewModelCreationExtras
.withCreationCallback<ReaderViewModel.Factory> { factory ->
factory.create(book ?: Constants.EMPTY_BOOK)
}
}
)
val libraryViewModel: LibraryViewModel = hiltViewModel()
var loading by remember { mutableStateOf(true) }
val state by viewModel.state.collectAsState()
val listState = rememberLazyListState()
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) {
viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context))
}
return super.onPostScroll(consumed, available, source)
}
}
}
val fontFamily = Constants.FONTS.find {
it.id == mainViewModel.fontFamily.collectAsState().value!!
} ?: Constants.FONTS[0]
val fontStyle = when (mainViewModel.isItalic.collectAsState().value!!) {
false -> FontStyle.Normal
true -> FontStyle.Italic
}
val fontSize = mainViewModel.fontSize.collectAsState().value!!
val lineHeight = mainViewModel.lineHeight.collectAsState().value!!
val paragraphHeight = mainViewModel.paragraphHeight.collectAsState().value!!
val paragraphIndentation = mainViewModel.paragraphIndentation.collectAsState().value!!
val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!!
val fontColor = mainViewModel.fontColor.collectAsState().value!!
LaunchedEffect(Unit) {
viewModel.init(
navigator,
context,
listState,
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
},
onLoaded = {
loading = false
}
)
}
LaunchedEffect(listState) {
snapshotFlow {
listState.firstVisibleItemIndex
}.debounce(50).collectLatest {
if (!loading) {
val progress = if (it > 0) {
if ((it + listState.layoutInfo.visibleItemsInfo.size) >= state.book.text.lastIndex) {
1f
} else {
(it.toFloat() / (state.book.text.size - 1).toFloat())
}
} else {
0f
}
viewModel.onEvent(
ReaderEvent.OnChangeProgress(
progress,
navigator,
refreshList = { book ->
libraryViewModel.onEvent(
LibraryEvent.OnUpdateBook(
book
)
)
}
)
)
}
}
}
if (state.showSettingsBottomSheet) {
ReaderSettingsBottomSheet(viewModel = viewModel, mainViewModel = mainViewModel)
}
Scaffold(
Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
containerColor = Color(backgroundColor.toULong()),
topBar = {
AnimatedVisibility(
visible = state.showMenu,
enter = slideInVertically { -it },
exit = slideOutVertically { -it }
) {
ReaderTopBar(
viewModel = viewModel,
navigator = navigator,
containerColor = systemBarsColor
)
}
},
bottomBar = {
AnimatedVisibility(
visible = state.showMenu,
enter = slideInVertically { it },
exit = slideOutVertically { it }
) {
ReaderBottomBar(
viewModel = viewModel,
libraryViewModel = libraryViewModel,
navigator = navigator,
scrollState = listState,
systemBarsColor = systemBarsColor
)
}
}
) {
SelectionContainer {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.clickable(
interactionSource = null,
indication = null,
enabled = !loading,
onClick = {
viewModel.onEvent(
ReaderEvent.OnShowHideMenu(context = context)
)
}
)
) {
if (state.book.text.isNotEmpty()) {
item {
DisableSelection {
ReaderStartItem(viewModel = viewModel)
}
}
}
itemsIndexed(
state.book.text, key = { _, key -> key.id }
) { index, text ->
if (index == 0)
Spacer(modifier = Modifier.height(36.dp))
Text(
text = "${if (paragraphIndentation) " " else ""}${text.line}",
color = Color(fontColor.toULong()),
style = TextStyle(
lineBreak = LineBreak.Paragraph
),
fontFamily = fontFamily.font,
fontStyle = fontStyle,
fontSize = fontSize.sp,
lineHeight = (fontSize + lineHeight).sp,
modifier = Modifier.padding(horizontal = 18.dp)
)
if (index == state.book.text.lastIndex)
Spacer(modifier = Modifier.height(36.dp))
else
Spacer(modifier = Modifier.height((paragraphHeight * 3).dp))
}
if (state.book.text.isNotEmpty()) {
item {
DisableSelection {
ReaderEndItem(
libraryViewModel = libraryViewModel,
viewModel = viewModel,
navigator = navigator
)
}
}
}
}
}
}
if (loading || state.errorMessage != null) {
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
if (!loading && state.errorMessage != null) {
IsError(
errorMessage = state.errorMessage!!.asString(),
icon = painterResource(id = R.drawable.error),
actionTitle = stringResource(id = R.string.go_back),
action = {
navigator.navigateBack()
}
)
} else {
Text(
text = stringResource(id = R.string.loading),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(12.dp))
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(0.55f),
strokeCap = StrokeCap.Round,
color = MaterialTheme.colorScheme.primary,
backgroundColor = MaterialTheme.elevation(2.dp)
)
}
}
}
BackHandler {
navigator.navigateBack()
viewModel.onEvent(ReaderEvent.OnShowSystemBars(context))
}
}

View file

@ -0,0 +1,98 @@
package com.acclorite.books_history.presentation.screens.reader.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.MaterialTheme
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.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.presentation.Navigator
import com.acclorite.books_history.presentation.data.removeDigits
import com.acclorite.books_history.presentation.data.removeTrailingZero
import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
import com.acclorite.books_history.presentation.screens.reader.data.ReaderEvent
import com.acclorite.books_history.presentation.screens.reader.data.ReaderViewModel
@Composable
fun ReaderBottomBar(
viewModel: ReaderViewModel,
libraryViewModel: LibraryViewModel,
navigator: Navigator,
scrollState: LazyListState,
systemBarsColor: Color
) {
val state by viewModel.state.collectAsState()
val book = state.book
Column(
Modifier
.fillMaxWidth()
.background(systemBarsColor)
.clickable(
interactionSource = null,
indication = null,
onClick = {}
)
.navigationBarsPadding()
.padding(horizontal = 24.dp)
.padding(top = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text =
(book.progress * 100)
.toDouble()
.removeDigits(4)
.removeTrailingZero()
.dropWhile { it == '-' } + "%",
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge
)
Slider(
value = book.progress,
onValueChange = {
viewModel.onEvent(
ReaderEvent.OnChangeProgress(
progress = it,
navigator = navigator,
refreshList = { book ->
libraryViewModel.onEvent(
LibraryEvent.OnUpdateBook(
book
)
)
}
)
)
viewModel.onEvent(ReaderEvent.OnScroll(scrollState, it))
},
colors = SliderDefaults.colors(
activeTrackColor = MaterialTheme.colorScheme.primary,
thumbColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(0.15f),
disabledActiveTickColor = Color.Transparent,
disabledActiveTrackColor = Color.Transparent,
disabledInactiveTickColor = Color.Transparent,
disabledInactiveTrackColor = Color.Transparent,
disabledThumbColor = Color.Transparent,
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent
)
)
}
}

View file

@ -0,0 +1,109 @@
package com.acclorite.books_history.presentation.screens.reader.components
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.ButtonDefaults
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.R
import com.acclorite.books_history.domain.model.Category
import com.acclorite.books_history.presentation.Navigator
import com.acclorite.books_history.presentation.Screen
import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
import com.acclorite.books_history.presentation.screens.reader.data.ReaderEvent
import com.acclorite.books_history.presentation.screens.reader.data.ReaderViewModel
@Composable
fun ReaderEndItem(
libraryViewModel: LibraryViewModel,
viewModel: ReaderViewModel,
navigator: Navigator
) {
val state by viewModel.state.collectAsState()
val context = LocalContext.current as ComponentActivity
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
stringResource(id = R.string.thanks_for_reading, state.book.title),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
TextButton(
onClick = {
if (state.book.category != Category.ALREADY_READ) {
viewModel.onEvent(
ReaderEvent.OnMoveBookToAlreadyRead(
refreshList = {
libraryViewModel.onEvent(
LibraryEvent.OnLoadList
)
},
updatePage = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it))
},
navigator = navigator,
context = context
)
)
Toast.makeText(
context,
context.getString(R.string.book_moved),
Toast.LENGTH_LONG
).show()
} else {
viewModel.onEvent(
ReaderEvent.OnShowSystemBars(
context
)
)
navigator.navigate(Screen.LIBRARY)
}
},
contentPadding = PaddingValues(horizontal = 12.dp),
colors = ButtonDefaults.textButtonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.primary
)
) {
Text(
stringResource(
if (state.book.category != Category.ALREADY_READ) R.string.move_to_read
else R.string.back_to_library
),
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}

View file

@ -0,0 +1,300 @@
package com.acclorite.books_history.presentation.screens.reader.components
import androidx.activity.ComponentActivity
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
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.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.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.R
import com.acclorite.books_history.domain.model.ChipItem
import com.acclorite.books_history.presentation.components.CategoryTitle
import com.acclorite.books_history.presentation.data.MainEvent
import com.acclorite.books_history.presentation.data.MainViewModel
import com.acclorite.books_history.presentation.screens.reader.data.ReaderEvent
import com.acclorite.books_history.presentation.screens.reader.data.ReaderViewModel
import com.acclorite.books_history.presentation.screens.settings.components.CheckboxWithTitle
import com.acclorite.books_history.presentation.screens.settings.components.ChipsWithTitle
import com.acclorite.books_history.presentation.screens.settings.components.ColorPickerWithTitle
import com.acclorite.books_history.presentation.screens.settings.components.SliderWithTitle
import com.acclorite.books_history.ui.ElevationDefaults
import com.acclorite.books_history.ui.elevation
import com.acclorite.books_history.util.Constants
@OptIn(
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
ExperimentalLayoutApi::class
)
@Composable
fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderViewModel) {
val pagerState = rememberPagerState { 2 }
val currentPage by remember { derivedStateOf { pagerState.currentPage } }
val context = LocalContext.current
val navigationBarPadding =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
val fontFamily = Constants.FONTS.find {
it.id == mainViewModel.fontFamily.collectAsState().value!!
} ?: Constants.FONTS[0]
val fontStyle = mainViewModel.isItalic.collectAsState().value!!
val fontSize = mainViewModel.fontSize.collectAsState().value!!
val lineHeight = mainViewModel.lineHeight.collectAsState().value!!
val paragraphHeight = mainViewModel.paragraphHeight.collectAsState().value!!
val paragraphIndentation = mainViewModel.paragraphIndentation.collectAsState().value!!
val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!!
val fontColor = mainViewModel.fontColor.collectAsState().value!!
val scrimColor =
if (currentPage == 1) Color.Transparent else BottomSheetDefaults.ScrimColor
val animatedScrimColor by animateColorAsState(
targetValue = scrimColor,
animationSpec = tween(300),
label = "Scrim animation"
)
val height = if (currentPage == 1) 0.5f else 0.7f
val animatedHeight by animateFloatAsState(
targetValue = height,
animationSpec = tween(300),
label = "Height animation"
)
LaunchedEffect(currentPage) {
viewModel.onEvent(
ReaderEvent.OnShowHideMenu(
currentPage != 1,
context as ComponentActivity
)
)
}
ModalBottomSheet(
scrimColor = animatedScrimColor,
dragHandle = {},
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(animatedHeight),
onDismissRequest = {
viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
},
sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0),
containerColor = MaterialTheme.elevation(ElevationDefaults.BottomSheet)
) {
ReaderSettingsBottomSheetTabRow(viewModel = viewModel, pagerState = pagerState)
HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page ->
LazyColumn(Modifier.fillMaxSize()) {
if (page == 0) {
item {
Spacer(modifier = Modifier.height(22.dp))
}
item {
CategoryTitle(
title = stringResource(id = R.string.font_settings),
color = MaterialTheme.colorScheme.onSurface
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
}
item {
ChipsWithTitle(
title = stringResource(id = R.string.font_family_option),
chips = Constants.FONTS
.map {
ChipItem(
it.id,
it.fontName.asString(),
MaterialTheme.typography.labelLarge.copy(
fontFamily = it.font
),
it.id == fontFamily.id
)
},
onClick = {
mainViewModel.onEvent(MainEvent.OnChangeFontFamily(it.id))
}
)
}
item {
ChipsWithTitle(
title = stringResource(id = R.string.font_style_option),
chips = listOf(
ChipItem(
"normal",
stringResource(id = R.string.font_style_normal),
MaterialTheme.typography.labelLarge.copy(
fontFamily = fontFamily.font,
fontStyle = FontStyle.Normal
),
!fontStyle
),
ChipItem(
"italic",
stringResource(id = R.string.font_style_italic),
MaterialTheme.typography.labelLarge.copy(
fontFamily = fontFamily.font,
fontStyle = FontStyle.Italic
),
fontStyle
),
),
onClick = {
mainViewModel.onEvent(
MainEvent.OnChangeFontStyle(
when (it.id) {
"italic" -> true
else -> false
}
)
)
}
)
}
item {
SliderWithTitle(
value = fontSize to "pt",
fromValue = 10,
toValue = 35,
title = stringResource(id = R.string.font_size_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeFontSize(it)
)
}
)
}
item {
Spacer(modifier = Modifier.height(22.dp))
}
item {
CategoryTitle(
title = stringResource(id = R.string.text_settings),
color = MaterialTheme.colorScheme.onSurface
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
}
item {
SliderWithTitle(
value = lineHeight to "pt",
fromValue = 1,
toValue = 16,
title = stringResource(id = R.string.line_height_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeLineHeight(it)
)
}
)
}
item {
SliderWithTitle(
value = paragraphHeight to "pt",
fromValue = 0,
toValue = 24,
title = stringResource(id = R.string.paragraph_height_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeParagraphHeight(it)
)
}
)
}
item {
CheckboxWithTitle(
selected = paragraphIndentation,
title = stringResource(id = R.string.paragraph_indentation_option)
) {
mainViewModel.onEvent(
MainEvent.OnChangeParagraphIndentation(!paragraphIndentation)
)
}
}
}
if (page == 1) {
item {
Spacer(modifier = Modifier.height(14.dp))
}
item {
ColorPickerWithTitle(
value = Color(backgroundColor.toULong()),
title = stringResource(id = R.string.background_color_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeBackgroundColor(
it.value.toLong()
)
)
}
)
}
item {
ColorPickerWithTitle(
value = Color(fontColor.toULong()),
title = stringResource(id = R.string.font_color_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeFontColor(
it.value.toLong()
)
)
}
)
}
}
item {
Spacer(
modifier = Modifier.height(
8.dp + navigationBarPadding
)
)
}
}
}
}
}

View file

@ -0,0 +1,50 @@
package com.acclorite.books_history.presentation.screens.reader.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.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun ReaderSettingsBottomSheetItem(
modifier: Modifier = Modifier,
title: String,
description: String,
verticalPadding: Dp = 8.dp,
onClick: () -> Unit
) {
Column(
modifier
.fillMaxWidth()
.clickable {
onClick()
}
.padding(horizontal = 18.dp, vertical = verticalPadding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start
) {
Text(
title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
fontSize = 18.sp
)
Spacer(modifier = Modifier.height(2.dp))
Text(
description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}

View file

@ -0,0 +1,73 @@
package com.acclorite.books_history.presentation.screens.reader.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.R
import com.acclorite.books_history.presentation.screens.reader.data.ReaderEvent
import com.acclorite.books_history.presentation.screens.reader.data.ReaderViewModel
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: PagerState) {
val tabItems = listOf(
stringResource(id = R.string.general_tab),
stringResource(id = R.string.color_tab)
)
TabRow(
modifier = Modifier
.fillMaxWidth(),
selectedTabIndex = pagerState.currentPage,
divider = {
Divider(color = MaterialTheme.colorScheme.surfaceVariant)
},
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier
.tabIndicatorOffset(tabPositions[pagerState.currentPage])
.padding(horizontal = 64.dp)
.clip(
RoundedCornerShape(
topStartPercent = 100,
topEndPercent = 100
)
),
color = MaterialTheme.colorScheme.primary
)
}
) {
tabItems.forEachIndexed { index, tabItem ->
Tab(
modifier = Modifier,
selected = pagerState.currentPage == index,
onClick = {
viewModel.onEvent(ReaderEvent.OnScrollToSettingsPage(index, pagerState))
},
selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
text = {
Text(
tabItem,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1
)
}
)
}
}
}

View file

@ -0,0 +1,104 @@
package com.acclorite.books_history.presentation.screens.reader.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Image
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.R
import com.acclorite.books_history.presentation.screens.reader.data.ReaderViewModel
import com.acclorite.books_history.ui.elevation
@Composable
fun ReaderStartItem(viewModel: ReaderViewModel) {
val state by viewModel.state.collectAsState()
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.padding(24.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier
.fillMaxWidth()
) {
Box(
modifier = Modifier
.height(135.dp)
.width(90.dp)
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation())
) {
Icon(
imageVector = Icons.Default.Image,
contentDescription = "Cover Image not found",
modifier = Modifier
.align(Alignment.Center)
.fillMaxWidth(0.7f)
.aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp)
)
if (state.book.coverImage != null) {
Image(
bitmap = state.book.coverImage!!.asImageBitmap(),
contentDescription = "Cover",
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.medium),
contentScale = ContentScale.Crop
)
}
}
Spacer(modifier = Modifier.width(18.dp))
Column(verticalArrangement = Arrangement.Center) {
Text(
state.book.title,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier
.fillMaxWidth(),
maxLines = 4,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
Text(
stringResource(id = R.string.happy_reading),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}

View file

@ -0,0 +1,119 @@
package com.acclorite.books_history.presentation.screens.reader.components
import androidx.activity.ComponentActivity
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
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.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.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.acclorite.books_history.R
import com.acclorite.books_history.presentation.Argument
import com.acclorite.books_history.presentation.Navigator
import com.acclorite.books_history.presentation.Screen
import com.acclorite.books_history.presentation.data.removeDigits
import com.acclorite.books_history.presentation.data.removeTrailingZero
import com.acclorite.books_history.presentation.screens.reader.data.ReaderEvent
import com.acclorite.books_history.presentation.screens.reader.data.ReaderViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColor: Color) {
val context = LocalContext.current as ComponentActivity
val state by viewModel.state.collectAsState()
val book = state.book
TopAppBar(
navigationIcon = {
IconButton(onClick = {
navigator.navigateBack()
viewModel.onEvent(ReaderEvent.OnShowSystemBars(context))
}) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Go back",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
title = {
Column(verticalArrangement = Arrangement.Center) {
Text(
book.title,
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 20.sp,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {
navigator.navigateWithoutBackStack(
Screen.BOOK_INFO,
Argument(
"book",
book
)
)
}
)
.basicMarquee(
delayMillis = 4000
)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
stringResource(
id = R.string.read_query,
(book.progress * 100)
.toDouble()
.removeDigits(2)
.removeTrailingZero()
.dropWhile { it == '-' } + "%"
),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge
)
}
},
actions = {
IconButton(onClick = {
viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
}) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Open Settings",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = containerColor
)
)
}

View file

@ -0,0 +1,40 @@
@file:OptIn(ExperimentalFoundationApi::class)
package com.acclorite.books_history.presentation.screens.reader.data
import androidx.activity.ComponentActivity
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.pager.PagerState
import com.acclorite.books_history.domain.model.Book
import com.acclorite.books_history.presentation.Navigator
sealed class ReaderEvent {
data class OnFileNotFound(val onLoaded: () -> Unit) : ReaderEvent()
data class OnLoadText(
val scrollState: LazyListState,
val navigator: Navigator,
val refreshList: (Book) -> Unit,
val onLoaded: () -> Unit
) : ReaderEvent()
data class OnShowHideMenu(val show: Boolean? = null, val context: ComponentActivity) :
ReaderEvent()
data class OnShowSystemBars(val context: ComponentActivity) : ReaderEvent()
data class OnChangeProgress(
val progress: Float,
val navigator: Navigator,
val refreshList: (Book) -> Unit
) : ReaderEvent()
data class OnScroll(val scrollState: LazyListState, val progress: Float) : ReaderEvent()
data object OnShowHideSettingsBottomSheet : ReaderEvent()
data class OnScrollToSettingsPage(val page: Int, val pagerState: PagerState) : ReaderEvent()
data class OnMoveBookToAlreadyRead(
val context: ComponentActivity,
val refreshList: () -> Unit,
val updatePage: (Int) -> Unit,
val navigator: Navigator
) : ReaderEvent()
}

View file

@ -0,0 +1,18 @@
package com.acclorite.books_history.presentation.screens.reader.data
import androidx.compose.runtime.Immutable
import com.acclorite.books_history.domain.model.Book
import com.acclorite.books_history.util.UIText
@Immutable
data class ReaderState(
val book: Book,
val errorMessage: UIText? = null,
val showMenu: Boolean = false,
val showSettingsBottomSheet: Boolean = false,
val currentPage: Int = 0,
)

View file

@ -0,0 +1,293 @@
package com.acclorite.books_history.presentation.screens.reader.data
import androidx.activity.ComponentActivity
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.snapshotFlow
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.acclorite.books_history.R
import com.acclorite.books_history.domain.model.Book
import com.acclorite.books_history.domain.model.Category
import com.acclorite.books_history.domain.use_case.GetText
import com.acclorite.books_history.domain.use_case.UpdateBooks
import com.acclorite.books_history.presentation.Argument
import com.acclorite.books_history.presentation.Navigator
import com.acclorite.books_history.presentation.Screen
import com.acclorite.books_history.util.Resource
import com.acclorite.books_history.util.UIText
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.Date
import kotlin.math.roundToInt
@OptIn(ExperimentalFoundationApi::class)
@HiltViewModel(assistedFactory = ReaderViewModel.Factory::class)
class ReaderViewModel @AssistedInject constructor(
@Assisted book: Book,
private val getText: GetText,
private val updateBooks: UpdateBooks
) : ViewModel() {
private val _state = MutableStateFlow(ReaderState(book))
val state = _state.asStateFlow()
fun onEvent(event: ReaderEvent) {
when (event) {
is ReaderEvent.OnFileNotFound -> {
_state.update {
event.onLoaded()
it.copy(
errorMessage = UIText.StringResource(R.string.error_file_not_found),
)
}
}
is ReaderEvent.OnLoadText -> {
viewModelScope.launch(Dispatchers.IO) {
if (_state.value.book.file == null) {
return@launch
}
if (_state.value.book.text.isEmpty()) {
getText.execute(_state.value.book.file!!).collect { result ->
when (result) {
is Resource.Loading -> Unit
is Resource.Success -> {
if (result.data?.isEmpty() != false) {
_state.update {
it.copy(
errorMessage = UIText.StringResource(
R.string.error_something_went_wrong
),
)
}
event.onLoaded()
return@collect
}
_state.update {
it.copy(
book = it.book.copy(
text = result.data,
)
)
}
}
is Resource.Error -> Unit
}
}
}
_state.update {
it.copy(
book = it.book.copy(
lastOpened = Date().time
)
)
}
updateBooks.execute(
listOf(_state.value.book)
)
event.refreshList(_state.value.book)
event.navigator.putArgument(
Argument("book", _state.value.book)
)
//todo add history
viewModelScope.launch {
snapshotFlow {
event.scrollState.layoutInfo.totalItemsCount
}.collectLatest { itemsCount ->
val scrollTo = (itemsCount * _state.value.book.progress).roundToInt()
if (itemsCount >= _state.value.book.text.size) {
if (scrollTo > 0) {
while (true) {
try {
event.scrollState.scrollToItem(scrollTo)
break
} catch (e: Exception) {
delay(50)
}
}
}
//todo fix blinking
delay(500)
event.onLoaded()
return@collectLatest
}
}
}
}
}
is ReaderEvent.OnShowHideMenu -> {
val shouldShow = event.show ?: !_state.value.showMenu
val insetsController = WindowCompat.getInsetsController(
event.context.window,
event.context.window.decorView
)
insetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
insetsController.apply {
if (shouldShow) {
show(WindowInsetsCompat.Type.systemBars())
} else {
hide(WindowInsetsCompat.Type.systemBars())
}
}
_state.update {
it.copy(
showMenu = shouldShow
)
}
}
is ReaderEvent.OnShowSystemBars -> {
val insetsController = WindowCompat.getInsetsController(
event.context.window,
event.context.window.decorView
)
insetsController.show(WindowInsetsCompat.Type.systemBars())
}
is ReaderEvent.OnScroll -> {
viewModelScope.launch {
val scrollTo = (_state.value.book.text.size * event.progress).roundToInt()
event.scrollState.scrollToItem(
scrollTo
)
}
}
is ReaderEvent.OnChangeProgress -> {
viewModelScope.launch(Dispatchers.IO) {
_state.update {
it.copy(
book = it.book.copy(
progress = event.progress
)
)
}
updateBooks.execute(
listOf(_state.value.book)
)
event.navigator.putArgument(
Argument("book", _state.value.book)
)
event.refreshList(_state.value.book)
}
}
is ReaderEvent.OnShowHideSettingsBottomSheet -> {
_state.update {
it.copy(
currentPage = if (it.showSettingsBottomSheet) it.currentPage else 0,
showSettingsBottomSheet = !it.showSettingsBottomSheet
)
}
}
is ReaderEvent.OnScrollToSettingsPage -> {
viewModelScope.launch {
event.pagerState.scrollToPage(event.page)
}
}
is ReaderEvent.OnMoveBookToAlreadyRead -> {
viewModelScope.launch {
onEvent(ReaderEvent.OnShowSystemBars(event.context))
val book = _state.value.book.copy(
category = Category.ALREADY_READ
)
updateBooks.execute(listOf(book))
event.refreshList()
event.updatePage(
Category.entries.dropLastWhile {
it != Category.ALREADY_READ
}.size - 1
)
event.navigator.navigate(Screen.LIBRARY)
}
}
}
}
fun init(
navigator: Navigator,
context: ComponentActivity,
scrollState: LazyListState,
refreshList: (Book) -> Unit,
onLoaded: () -> Unit
) {
viewModelScope.launch {
val book = navigator.retrieveArgument("book") as? Book
if (book == null) {
navigator.navigateBack()
return@launch
}
_state.update {
ReaderState(book = book)
}
if (book.file == null) {
onEvent(ReaderEvent.OnFileNotFound(onLoaded = { onLoaded() }))
} else {
onEvent(ReaderEvent.OnShowHideMenu(false, context))
onEvent(
ReaderEvent.OnLoadText(
scrollState,
navigator,
refreshList = { refreshList(it) },
onLoaded = {
onLoaded()
}
)
)
}
}
}
@AssistedFactory
interface Factory {
fun create(book: Book): ReaderViewModel
}
}

View file

@ -1,13 +1,11 @@
package com.acclorite.books_history.presentation.screens.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.DisplaySettings
@ -28,14 +26,12 @@ import androidx.compose.ui.text.style.TextOverflow
import com.acclorite.books_history.R
import com.acclorite.books_history.presentation.Navigator
import com.acclorite.books_history.presentation.Screen
import com.acclorite.books_history.presentation.main.MainViewModel
import com.acclorite.books_history.presentation.screens.settings.components.SettingsCategoryItem
import com.acclorite.books_history.ui.elevation
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
viewModel: MainViewModel,
navigator: Navigator
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
@ -74,12 +70,12 @@ fun SettingsScreen(
)
}
) { paddingValues ->
Column(
LazyColumn(
Modifier
.fillMaxSize()
.padding(top = paddingValues.calculateTopPadding())
.verticalScroll(rememberScrollState())
) {
item {
SettingsCategoryItem(
icon = Icons.Default.DisplaySettings,
text = stringResource(id = R.string.general_settings),
@ -87,6 +83,9 @@ fun SettingsScreen(
) {
navigator.navigate(Screen.GENERAL_SETTINGS)
}
}
item {
SettingsCategoryItem(
icon = Icons.Default.Palette,
text = stringResource(id = R.string.appearance_settings),
@ -95,5 +94,8 @@ fun SettingsScreen(
navigator.navigate(Screen.APPEARANCE_SETTINGS)
}
}
//todo Reader settings
}
}
}

View file

@ -0,0 +1,41 @@
package com.acclorite.books_history.presentation.screens.settings.components
import androidx.compose.foundation.clickable
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.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.presentation.components.CategoryTitle
@Composable
fun CheckboxWithTitle(
selected: Boolean,
title: String,
horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 12.dp,
onClick: () -> Unit
) {
Row(
Modifier
.fillMaxWidth()
.clickable {
onClick()
}
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = selected,
onCheckedChange = null,
)
Spacer(modifier = Modifier.width(24.dp))
CategoryTitle(title = title, padding = 0.dp)
}
}

View file

@ -0,0 +1,77 @@
package com.acclorite.books_history.presentation.screens.settings.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
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.material.ChipDefaults
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.domain.model.ChipItem
import com.acclorite.books_history.presentation.components.CategoryTitle
@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
@Composable
fun ChipsWithTitle(
title: String,
chips: List<ChipItem>,
horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 8.dp,
onClick: (ChipItem) -> Unit
) {
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = horizontalPadding, vertical = verticalPadding)
) {
CategoryTitle(title = title, padding = 0.dp)
Spacer(modifier = Modifier.height(8.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
content = {
chips.forEach { item ->
FilterChip(
modifier = Modifier.height(36.dp),
selected = item.selected,
enabled = !item.selected,
shape = MaterialTheme.shapes.small,
border = if (!item.selected) BorderStroke(
1.dp,
MaterialTheme.colorScheme.outline
) else null,
colors = ChipDefaults.filterChipColors(
backgroundColor = Color.Transparent,
disabledBackgroundColor = MaterialTheme.colorScheme.secondaryContainer
),
onClick = { onClick(item) },
) {
Text(
item.title,
style = item.textStyle,
color = if (item.selected) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
maxLines = 1
)
}
}
},
)
}
}

View file

@ -0,0 +1,129 @@
package com.acclorite.books_history.presentation.screens.settings.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.History
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.R
import com.acclorite.books_history.presentation.components.CategoryTitle
@Composable
fun ColorPickerWithTitle(
value: Color,
title: String,
horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 8.dp,
onValueChange: (Color) -> Unit
) {
val initialValue = remember { value }
var color by remember { mutableStateOf(value) }
Column(
Modifier
.fillMaxWidth()
.padding(vertical = verticalPadding, horizontal = horizontalPadding)
) {
CategoryTitle(
title = title,
padding = 0.dp,
// color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
RevertibleSlider(
value = color.red to "",
initialValue = initialValue.red,
title = stringResource(id = R.string.red_color),
onValueChange = {
color = color.copy(red = it)
onValueChange(color)
}
)
RevertibleSlider(
value = color.green to "",
initialValue = initialValue.green,
title = stringResource(id = R.string.green_color),
onValueChange = {
color = color.copy(green = it)
onValueChange(color)
}
)
RevertibleSlider(
value = color.blue to "",
initialValue = initialValue.blue,
title = stringResource(id = R.string.blue_color),
onValueChange = {
color = color.copy(blue = it)
onValueChange(color)
}
)
}
}
@Composable
private fun RevertibleSlider(
value: Pair<Float, String>,
initialValue: Float,
title: String,
horizontalPadding: Dp = 0.dp,
verticalPadding: Dp = 8.dp,
onValueChange: (Float) -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.padding(
horizontal = horizontalPadding,
vertical = verticalPadding
)
) {
SliderWithTitle(
modifier = Modifier.weight(0.85f),
value = value,
title = title,
toValue = 255,
onValueChange = {
onValueChange(it)
},
horizontalPadding = 0.dp,
verticalPadding = 0.dp
)
Box(modifier = Modifier.weight(0.15f)) {
IconButton(
modifier = Modifier.align(Alignment.CenterEnd),
enabled = initialValue != value.first,
onClick = { onValueChange(initialValue) }
) {
Icon(
imageVector = Icons.Default.History,
contentDescription = "Revert to initial",
modifier = Modifier
.size(28.dp),
tint = if (initialValue == value.first) MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.onSurface
)
}
}
}
}

View file

@ -0,0 +1,93 @@
package com.acclorite.books_history.presentation.screens.settings.components
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.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.presentation.components.CategoryTitle
import kotlin.math.roundToInt
@Composable
fun SliderWithTitle(
modifier: Modifier = Modifier,
value: Pair<Int, String>,
fromValue: Int,
toValue: Int,
title: String,
horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 8.dp,
onValueChange: (Int) -> Unit
) {
Row(
modifier
.fillMaxWidth()
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
verticalAlignment = Alignment.CenterVertically
) {
Column(Modifier.fillMaxWidth(0.2f)) {
CategoryTitle(title = title, padding = 0.dp)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${value.first}${value.second}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.width(8.dp))
Slider(
valueRange = fromValue.toFloat()..toValue.toFloat(),
value = value.first.toFloat(),
onValueChange = {
onValueChange(it.roundToInt())
},
steps = toValue - fromValue
)
}
}
@Composable
fun SliderWithTitle(
modifier: Modifier = Modifier,
value: Pair<Float, String>,
title: String,
toValue: Int,
horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 8.dp,
onValueChange: (Float) -> Unit
) {
Row(
modifier
.fillMaxWidth()
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
verticalAlignment = Alignment.CenterVertically
) {
Column(Modifier.fillMaxWidth(0.2f)) {
CategoryTitle(title = title, padding = 0.dp)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${(value.first * toValue).roundToInt()}${value.second}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.width(8.dp))
Slider(
valueRange = 0f..1f,
value = value.first,
onValueChange = {
onValueChange(it)
}
)
}
}

View file

@ -1,13 +1,11 @@
package com.acclorite.books_history.presentation.screens.settings.nested.appearance
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
@ -20,39 +18,30 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
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.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.hilt.navigation.compose.hiltViewModel
import com.acclorite.books_history.R
import com.acclorite.books_history.domain.model.ChipItem
import com.acclorite.books_history.presentation.Navigator
import com.acclorite.books_history.presentation.main.MainViewModel
import com.acclorite.books_history.presentation.screens.settings.components.SettingsOptionItem
import com.acclorite.books_history.presentation.screens.settings.nested.appearance.components.AppearanceSettingsDarkThemeDialog
import com.acclorite.books_history.presentation.data.MainEvent
import com.acclorite.books_history.presentation.data.MainViewModel
import com.acclorite.books_history.presentation.screens.settings.components.ChipsWithTitle
import com.acclorite.books_history.presentation.screens.settings.nested.appearance.components.AppearanceSettingsThemeSwitcher
import com.acclorite.books_history.presentation.screens.settings.nested.appearance.data.AppearanceSettingsEvent
import com.acclorite.books_history.presentation.screens.settings.nested.appearance.data.AppearanceSettingsViewModel
import com.acclorite.books_history.ui.DarkTheme
import com.acclorite.books_history.ui.elevation
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppearanceSettings(
viewModel: AppearanceSettingsViewModel = hiltViewModel(),
mainViewModel: MainViewModel,
navigator: Navigator
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val state by viewModel.state.collectAsState()
val darkTheme = mainViewModel.darkTheme.collectAsState().value!!
if (state.showDarkThemeDialog) {
AppearanceSettingsDarkThemeDialog(viewModel = viewModel, mainViewModel = mainViewModel)
}
Scaffold(
Modifier
.fillMaxSize()
@ -87,23 +76,38 @@ fun AppearanceSettings(
)
}
) { paddingValues ->
Column(
LazyColumn(
Modifier
.fillMaxSize()
.padding(top = paddingValues.calculateTopPadding())
.verticalScroll(rememberScrollState())
) {
SettingsOptionItem(
item {
ChipsWithTitle(
title = stringResource(id = R.string.dark_theme_option),
description = when (darkTheme) {
chips = DarkTheme.entries.map {
ChipItem(
it.toString(),
when (it) {
DarkTheme.OFF -> stringResource(id = R.string.dark_theme_off)
DarkTheme.ON -> stringResource(id = R.string.dark_theme_on)
DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system)
}
},
MaterialTheme.typography.labelLarge,
it == darkTheme
)
},
) {
viewModel.onEvent(AppearanceSettingsEvent.OnShowHideDarkThemeDialog)
mainViewModel.onEvent(
MainEvent.OnChangeDarkTheme(
it.id
)
)
}
}
item {
AppearanceSettingsThemeSwitcher(mainViewModel = mainViewModel)
}
}
}
}

View file

@ -1,58 +0,0 @@
package com.acclorite.books_history.presentation.screens.settings.nested.appearance.components
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.R
import com.acclorite.books_history.presentation.components.CustomDialogWithLazyColumn
import com.acclorite.books_history.presentation.components.SelectableDialogItem
import com.acclorite.books_history.presentation.main.MainEvent
import com.acclorite.books_history.presentation.main.MainViewModel
import com.acclorite.books_history.presentation.screens.settings.nested.appearance.data.AppearanceSettingsEvent
import com.acclorite.books_history.presentation.screens.settings.nested.appearance.data.AppearanceSettingsViewModel
import com.acclorite.books_history.ui.DarkTheme
@Composable
fun AppearanceSettingsDarkThemeDialog(
viewModel: AppearanceSettingsViewModel,
mainViewModel: MainViewModel
) {
val darkTheme = mainViewModel.darkTheme.collectAsState().value!!
CustomDialogWithLazyColumn(
title = stringResource(id = R.string.dark_theme_option),
imageVectorIcon = null,
description = null,
actionText = null,
isActionEnabled = null,
onDismiss = { viewModel.onEvent(AppearanceSettingsEvent.OnShowHideDarkThemeDialog) },
onAction = {},
withDivider = false,
items = {
item {
Spacer(modifier = Modifier.height(12.dp))
}
items(DarkTheme.entries) {
val theme = when (it) {
DarkTheme.OFF -> stringResource(id = R.string.dark_theme_off)
DarkTheme.ON -> stringResource(id = R.string.dark_theme_on)
DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system)
}
SelectableDialogItem(selected = it == darkTheme, title = theme) {
mainViewModel.onEvent(
MainEvent.OnChangeDarkTheme(
it.toString()
)
)
}
}
}
)
}

View file

@ -5,21 +5,18 @@ 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.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.acclorite.books_history.R
import com.acclorite.books_history.presentation.main.MainEvent
import com.acclorite.books_history.presentation.main.MainViewModel
import com.acclorite.books_history.presentation.components.CategoryTitle
import com.acclorite.books_history.presentation.data.MainEvent
import com.acclorite.books_history.presentation.data.MainViewModel
import com.acclorite.books_history.ui.Theme
import com.acclorite.books_history.ui.isDark
import com.acclorite.books_history.util.Constants
@ -38,21 +35,18 @@ fun AppearanceSettingsThemeSwitcher(
Modifier
.fillMaxWidth()
) {
Text(
text = stringResource(id = R.string.app_theme_option),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
fontSize = 18.sp,
modifier = Modifier.padding(start = 16.dp, top = 8.dp)
)
Spacer(modifier = Modifier.height(8.dp))
CategoryTitle(
title = stringResource(id = R.string.app_theme_option)
)
Spacer(modifier = Modifier.height(10.dp))
LazyRow(
Modifier
.fillMaxWidth()
) {
itemsIndexed(themes) { index, themeEntry ->
if (index == 0)
Spacer(modifier = Modifier.width(16.dp))
Spacer(modifier = Modifier.width(18.dp))
AppearanceSettingsThemeSwitcherItem(
theme = themeEntry,
@ -65,7 +59,7 @@ fun AppearanceSettingsThemeSwitcher(
if (index != Theme.entries.lastIndex) {
Spacer(modifier = Modifier.width(8.dp))
} else {
Spacer(modifier = Modifier.width(16.dp))
Spacer(modifier = Modifier.width(18.dp))
}
}
}

View file

@ -133,8 +133,8 @@ fun AppearanceSettingsThemeSwitcherItem(
Spacer(modifier = Modifier.height(6.dp))
Text(
text = theme.second.asString(),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)

View file

@ -1,5 +0,0 @@
package com.acclorite.books_history.presentation.screens.settings.nested.appearance.data
sealed class AppearanceSettingsEvent() {
data object OnShowHideDarkThemeDialog : AppearanceSettingsEvent()
}

View file

@ -1,8 +0,0 @@
package com.acclorite.books_history.presentation.screens.settings.nested.appearance.data
import androidx.compose.runtime.Immutable
@Immutable
data class AppearanceSettingsState(
val showDarkThemeDialog: Boolean = false
)

View file

@ -1,31 +0,0 @@
package com.acclorite.books_history.presentation.screens.settings.nested.appearance.data
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@OptIn(ExperimentalFoundationApi::class)
@HiltViewModel
class AppearanceSettingsViewModel @Inject constructor(
) : ViewModel() {
private val _state = MutableStateFlow(AppearanceSettingsState())
val state = _state.asStateFlow()
fun onEvent(event: AppearanceSettingsEvent) {
when (event) {
is AppearanceSettingsEvent.OnShowHideDarkThemeDialog -> {
_state.update {
it.copy(
showDarkThemeDialog = !it.showDarkThemeDialog
)
}
}
}
}
}

View file

@ -1,13 +1,12 @@
package com.acclorite.books_history.presentation.screens.settings.nested.general
import androidx.compose.foundation.layout.Column
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
@ -20,38 +19,31 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
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.text.style.TextOverflow
import androidx.hilt.navigation.compose.hiltViewModel
import com.acclorite.books_history.R
import com.acclorite.books_history.domain.model.ChipItem
import com.acclorite.books_history.presentation.Navigator
import com.acclorite.books_history.presentation.main.MainViewModel
import com.acclorite.books_history.presentation.screens.settings.components.SettingsOptionItem
import com.acclorite.books_history.presentation.screens.settings.nested.general.components.GeneralSettingsLanguageDialog
import com.acclorite.books_history.presentation.screens.settings.nested.general.data.GeneralSettingsEvent
import com.acclorite.books_history.presentation.screens.settings.nested.general.data.GeneralSettingsViewModel
import com.acclorite.books_history.presentation.data.MainEvent
import com.acclorite.books_history.presentation.data.MainViewModel
import com.acclorite.books_history.presentation.screens.settings.components.ChipsWithTitle
import com.acclorite.books_history.ui.elevation
import com.acclorite.books_history.util.Constants
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GeneralSettings(
viewModel: GeneralSettingsViewModel = hiltViewModel(),
mainViewModel: MainViewModel,
navigator: Navigator
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val state by viewModel.state.collectAsState()
val context = LocalContext.current as ComponentActivity
val language = mainViewModel.language.collectAsState().value!!
if (state.showLanguageDialog) {
GeneralSettingsLanguageDialog(viewModel = viewModel, mainViewModel = mainViewModel)
}
Scaffold(
Modifier
.fillMaxSize()
@ -86,18 +78,30 @@ fun GeneralSettings(
)
}
) { paddingValues ->
Column(
LazyColumn(
Modifier
.fillMaxSize()
.padding(top = paddingValues.calculateTopPadding())
.verticalScroll(rememberScrollState())
) {
SettingsOptionItem(
item {
ChipsWithTitle(
title = stringResource(id = R.string.language_option),
description = Constants.LANGUAGES.find { it.first == language }?.second
?: return@Column
chips = Constants.LANGUAGES.map {
ChipItem(
it.first,
it.second,
MaterialTheme.typography.labelLarge,
it.first == language
)
}
) {
viewModel.onEvent(GeneralSettingsEvent.OnShowHideLanguageDialog)
mainViewModel.onEvent(
MainEvent.OnChangeLanguage(
it.id,
context
)
)
}
}
}
}

View file

@ -1,55 +0,0 @@
package com.acclorite.books_history.presentation.screens.settings.nested.general.components
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.R
import com.acclorite.books_history.presentation.components.CustomDialogWithLazyColumn
import com.acclorite.books_history.presentation.components.SelectableDialogItem
import com.acclorite.books_history.presentation.main.MainEvent
import com.acclorite.books_history.presentation.main.MainViewModel
import com.acclorite.books_history.presentation.screens.settings.nested.general.data.GeneralSettingsEvent
import com.acclorite.books_history.presentation.screens.settings.nested.general.data.GeneralSettingsViewModel
import com.acclorite.books_history.util.Constants
@Composable
fun GeneralSettingsLanguageDialog(
viewModel: GeneralSettingsViewModel,
mainViewModel: MainViewModel
) {
val language = mainViewModel.language.collectAsState().value!!
val activity = LocalContext.current as ComponentActivity
CustomDialogWithLazyColumn(
title = stringResource(id = R.string.language_option),
imageVectorIcon = null,
description = null,
actionText = null,
isActionEnabled = null,
onDismiss = { viewModel.onEvent(GeneralSettingsEvent.OnShowHideLanguageDialog) },
onAction = {},
withDivider = false,
items = {
item {
Spacer(modifier = Modifier.height(12.dp))
}
items(Constants.LANGUAGES) { lang ->
SelectableDialogItem(selected = lang.first == language, title = lang.second) {
mainViewModel.onEvent(
MainEvent.OnChangeLanguage(
lang.first,
activity
)
)
}
}
}
)
}

View file

@ -1,5 +0,0 @@
package com.acclorite.books_history.presentation.screens.settings.nested.general.data
sealed class GeneralSettingsEvent() {
data object OnShowHideLanguageDialog : GeneralSettingsEvent()
}

View file

@ -1,8 +0,0 @@
package com.acclorite.books_history.presentation.screens.settings.nested.general.data
import androidx.compose.runtime.Immutable
@Immutable
data class GeneralSettingsState(
val showLanguageDialog: Boolean = false
)

View file

@ -1,43 +0,0 @@
package com.acclorite.books_history.presentation.screens.settings.nested.general.data
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@OptIn(ExperimentalFoundationApi::class)
@HiltViewModel
class GeneralSettingsViewModel @Inject constructor(
) : ViewModel() {
private val _state = MutableStateFlow(GeneralSettingsState())
val state = _state.asStateFlow()
fun onEvent(event: GeneralSettingsEvent) {
when (event) {
is GeneralSettingsEvent.OnShowHideLanguageDialog -> {
_state.update {
it.copy(
showLanguageDialog = !it.showLanguageDialog
)
}
}
}
}
}

View file

@ -52,6 +52,11 @@ fun BooksHistoryResurrectionTheme(
window.statusBarColor = Color.Transparent.toArgb()
window.navigationBarColor = Color.Transparent.toArgb()
// Fix for nav bar being semi transparent in api 29+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !isDark
WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = !isDark
}

View file

@ -24,7 +24,7 @@ val Typography = Typography(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp
lineHeight = 20.sp
),
titleSmall = TextStyle(
fontFamily = FontFamily.Default,

View file

@ -49,7 +49,13 @@ object Constants {
// Fonts for Reader
val FONTS = listOf(
FontWithName(
"Raleway",
"default",
UIText.StringResource(R.string.default_font),
FontFamily.Default
),
FontWithName(
"raleway",
UIText.StringValue("Raleway"),
FontFamily(
Font(R.font.raleway_regular),
Font(R.font.raleway_regular_italic, style = FontStyle.Italic),
@ -60,7 +66,8 @@ object Constants {
)
),
FontWithName(
"Open Sans",
"open_sans",
UIText.StringValue("Open Sans"),
FontFamily(
Font(R.font.opensans_regular),
Font(R.font.opensans_regular_italic, style = FontStyle.Italic),
@ -71,7 +78,8 @@ object Constants {
)
),
FontWithName(
"Mulish",
"mulish",
UIText.StringValue("Mulish"),
FontFamily(
Font(R.font.mulish_regular),
Font(R.font.mulish_regular_italic, style = FontStyle.Italic),
@ -82,7 +90,8 @@ object Constants {
)
),
FontWithName(
"Arimo",
"arimo",
UIText.StringValue("Arimo"),
FontFamily(
Font(R.font.arimo_regular),
Font(R.font.arimo_regular_italic, style = FontStyle.Italic),
@ -93,7 +102,8 @@ object Constants {
)
),
FontWithName(
"Garamond",
"garamond",
UIText.StringValue("Garamond"),
FontFamily(
Font(R.font.garamond_regular),
Font(R.font.garamond_regular_italic, style = FontStyle.Italic),
@ -104,7 +114,8 @@ object Constants {
)
),
FontWithName(
"Roboto Serif",
"roboto_serif",
UIText.StringValue("Roboto Serif"),
FontFamily(
Font(R.font.robotoserif_regular),
Font(R.font.robotoserif_regular_italic, style = FontStyle.Italic),
@ -115,7 +126,8 @@ object Constants {
)
),
FontWithName(
"Noto Serif",
"noto_serif",
UIText.StringValue("Noto Serif"),
FontFamily(
Font(R.font.notoserif_regular),
Font(R.font.notoserif_regular_italic, style = FontStyle.Italic),
@ -126,7 +138,8 @@ object Constants {
)
),
FontWithName(
"Noto Sans",
"noto_sans",
UIText.StringValue("Noto Sans"),
FontFamily(
Font(R.font.notosans_regular),
Font(R.font.notosans_regular_italic, style = FontStyle.Italic),
@ -137,7 +150,8 @@ object Constants {
)
),
FontWithName(
"Roboto",
"roboto",
UIText.StringValue("Roboto"),
FontFamily(
Font(R.font.roboto_regular),
Font(R.font.roboto_regular_italic, style = FontStyle.Italic),
@ -148,7 +162,8 @@ object Constants {
)
),
FontWithName(
"Jost",
"jost",
UIText.StringValue("Jost"),
FontFamily(
Font(R.font.jost_regular),
Font(R.font.jost_regular_italic, style = FontStyle.Italic),

View file

@ -51,6 +51,12 @@
<string name="error_no_description">
Немає опису.
</string>
<string name="error_file_not_found">
Файл не знайдено. Будь ласка, перевірте чи файл цієї книги все ще існує.
</string>
<string name="error_something_went_wrong">
Щось пішло не так. Будь ласка, перевірте чи файл цієї книги не пошкоджений.
</string>
<!-- Is Empty messages -->
<string name="browse_empty">
@ -64,6 +70,7 @@
<string name="selected_items_count_query">%1$s обрано</string>
<string name="search_query">Знайти %1$s…</string>
<string name="you_read_query">Ви прочитали %1$s цієї книги.</string>
<string name="read_query">%1$s прочитано</string>
<!-- Action Texts -->
<string name="cancel">Відмінити</string>
@ -77,6 +84,9 @@
<string name="start">Почати</string>
<string name="continue_read">Продовжити</string>
<string name="never">Ніколи</string>
<string name="go_back">Назад</string>
<string name="move_to_read">Перемістити цю книгу в \"Прочитано\"</string>
<string name="back_to_library">Назад в Бібліотеку</string>
<!-- Items -->
<string name="files">файли</string>
@ -96,23 +106,44 @@
<string name="already_read_tab">Прочитано</string>
<string name="planning_tab">Планую</string>
<string name="dropped_tab">Кинуто</string>
<string name="general_tab">Головні</string>
<string name="color_tab">Кольори</string>
<!-- Settings categories -->
<string name="general_settings">Головні</string>
<string name="general_settings_desc">Мова застосунку</string>
<string name="appearance_settings">Зовнішній вигляд</string>
<string name="appearance_settings_desc">Тема застосунку, темна тема</string>
<string name="font_settings">Шрифт</string>
<string name="text_settings">Текст</string>
<!-- Settings options -->
<string name="language_option">Мова застосунку</string>
<string name="dark_theme_option">Темна тема</string>
<string name="app_theme_option">Тема застосунку</string>
<string name="font_family_option">Сімейство шрифтів</string>
<string name="font_style_option">Стиль шрифту</string>
<string name="font_size_option">Розмір шрифту</string>
<string name="line_height_option">Висота рядка</string>
<string name="paragraph_height_option">Висота абзацу</string>
<string name="paragraph_indentation_option">Абзацний відступ</string>
<string name="background_color_option">Колір фону</string>
<string name="font_color_option">Колір шрифту</string>
<!-- Colors -->
<string name="red_color">Червоний</string>
<string name="green_color">Зелений</string>
<string name="blue_color">Синій</string>
<!-- Dark Theme properties -->
<string name="dark_theme_off">Вимкнено</string>
<string name="dark_theme_on">Ввімкнено</string>
<string name="dark_theme_follow_system">Тема системи</string>
<!-- Font Style properties -->
<string name="font_style_italic">Похилий</string>
<string name="font_style_normal">Нормальний</string>
<!-- Themes -->
<string name="dynamic_theme">Хаос</string>
<string name="blue_theme">Нептун</string>
@ -147,4 +178,15 @@
<string name="file_size">Розмір файлу</string>
<string name="unknown">Невідомо</string>
<!-- Reader -->
<string name="loading">Завантаження…</string>
<string name="happy_reading">Приємного читання!</string>
<string name="thanks_for_reading">
Дякуємо за те, що прочитали \"%1$s\" з нами.
Ми сподіваємось, що вам сподобалася ця книга!
</string>
<!-- Font families -->
<string name="default_font">За замовчуванням</string>
</resources>

View file

@ -52,6 +52,12 @@
<string name="error_no_description">
No description.
</string>
<string name="error_file_not_found">
File was not found. Please check whether this book\'s file still exist.
</string>
<string name="error_something_went_wrong">
Something went wrong. Please check whether this book\'s file is not corrupted.
</string>
<!-- Is Empty messages -->
<string name="browse_empty">
@ -65,6 +71,7 @@
<string name="selected_items_count_query">%1$s selected</string>
<string name="search_query">Search %1$s…</string>
<string name="you_read_query">You read %1$s of this book.</string>
<string name="read_query">%1$s read</string>
<!-- Action Texts -->
<string name="cancel">Cancel</string>
@ -78,6 +85,9 @@
<string name="start">Start</string>
<string name="continue_read">Continue</string>
<string name="never">Never</string>
<string name="go_back">Go back</string>
<string name="move_to_read">Move this book to \"Already Read\"</string>
<string name="back_to_library">Back to Library</string>
<!-- Items -->
<string name="files">files</string>
@ -97,23 +107,44 @@
<string name="already_read_tab">Already Read</string>
<string name="planning_tab">Planning</string>
<string name="dropped_tab">Dropped</string>
<string name="general_tab">General</string>
<string name="color_tab">Colors</string>
<!-- Settings categories -->
<string name="general_settings">General</string>
<string name="general_settings_desc">App language</string>
<string name="appearance_settings">Appearance</string>
<string name="appearance_settings_desc">App theme, dark theme</string>
<string name="font_settings">Font</string>
<string name="text_settings">Text</string>
<!-- Settings options -->
<string name="language_option">App language</string>
<string name="dark_theme_option">Dark Theme</string>
<string name="dark_theme_option">Dark theme</string>
<string name="app_theme_option">App theme</string>
<string name="font_family_option">Font family</string>
<string name="font_style_option">Font style</string>
<string name="font_size_option">Font size</string>
<string name="line_height_option">Line height</string>
<string name="paragraph_height_option">Paragraph height</string>
<string name="paragraph_indentation_option">Paragraph indentation</string>
<string name="background_color_option">Background color</string>
<string name="font_color_option">Font color</string>
<!-- Colors -->
<string name="red_color">Red</string>
<string name="green_color">Green</string>
<string name="blue_color">Blue</string>
<!-- Dark Theme properties -->
<string name="dark_theme_off">Disabled</string>
<string name="dark_theme_on">Enabled</string>
<string name="dark_theme_follow_system">Follow system</string>
<!-- Font Style properties -->
<string name="font_style_italic">Italic</string>
<string name="font_style_normal">Normal</string>
<!-- Themes -->
<string name="dynamic_theme">Chaos</string>
<string name="blue_theme">Neptune</string>
@ -148,4 +179,15 @@
<string name="file_size">File size</string>
<string name="unknown">Unknown</string>
<!-- Reader -->
<string name="loading">Loading…</string>
<string name="happy_reading">Happy reading!</string>
<string name="thanks_for_reading">
Thank you for reading \"%1$s\" with us.
We hope you enjoyed this book!
</string>
<!-- Font families -->
<string name="default_font">Default</string>
</resources>

View file

@ -6,6 +6,7 @@
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
</style>
<style name="Theme.Start.Splash" parent="Theme.SplashScreen">