BookInfoScreen: Editing Author and Description + Fixed slow loading (caused by BookInfoEvent.OnCheckCoverReset).

This commit is contained in:
acclorite 2024-07-06 13:42:16 +03:00
parent 45c1bd2b80
commit 936627e742
11 changed files with 483 additions and 144 deletions

View file

@ -16,7 +16,7 @@ class BookMapperImpl @Inject constructor() : BookMapper {
scrollIndex = book.scrollIndex, scrollIndex = book.scrollIndex,
scrollOffset = book.scrollOffset, scrollOffset = book.scrollOffset,
progress = book.progress, progress = book.progress,
author = book.author.string, author = book.author.getAsString(),
textPath = book.textPath, textPath = book.textPath,
description = book.description, description = book.description,
image = if (book.coverImage != null) book.coverImage.toString() else null, image = if (book.coverImage != null) book.coverImage.toString() else null,

View file

@ -11,9 +11,9 @@ import java.io.Serializable
@Parcelize @Parcelize
@Immutable @Immutable
sealed class UIText(val string: String?) : Parcelable { sealed class UIText : Parcelable {
data class StringValue(val value: String) : UIText(value) data class StringValue(val value: String) : UIText()
class StringResource(@StringRes val resId: Int, vararg val args: Serializable) : UIText(null) class StringResource(@StringRes val resId: Int, vararg val args: Serializable) : UIText()
@Composable @Composable
fun asString(): String { fun asString(): String {
@ -29,4 +29,11 @@ sealed class UIText(val string: String?) : Parcelable {
is StringResource -> context.getString(resId, *args) is StringResource -> context.getString(resId, *args)
} }
} }
fun getAsString(): String? {
return when (this) {
is StringValue -> value
is StringResource -> null
}
}
} }

View file

@ -1,8 +1,6 @@
package ua.acclorite.book_story.presentation.screens.book_info package ua.acclorite.book_story.presentation.screens.book_info
import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandHorizontally import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -15,6 +13,7 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -24,10 +23,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
@ -44,32 +40,25 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.OnNavigate import ua.acclorite.book_story.domain.util.OnNavigate
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.components.CustomSnackbar import ua.acclorite.book_story.presentation.components.CustomSnackbar
import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.data.LocalNavigator import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoBackground import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoBackground
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDescriptionSection import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDescriptionSection
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoInfoSection import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoInfoSection
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoMoreDropDown
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoStatisticSection import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoStatisticSection
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoTopBar
import ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet.BookInfoChangeCoverBottomSheet import ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet.BookInfoChangeCoverBottomSheet
import ua.acclorite.book_story.presentation.screens.book_info.components.confirm_update_dialog.BookInfoConfirmUpdateDialog import ua.acclorite.book_story.presentation.screens.book_info.components.confirm_update_dialog.BookInfoConfirmUpdateDialog
import ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet.BookInfoDetailsBottomSheet import ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet.BookInfoDetailsBottomSheet
@ -84,8 +73,6 @@ import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions
@Composable @Composable
fun BookInfoScreenRoot(screen: Screen.BookInfo) { fun BookInfoScreenRoot(screen: Screen.BookInfo) {
@ -140,7 +127,6 @@ private fun BookInfoScreen(
) )
} }
) )
val firstVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } }
if (state.value.showChangeCoverBottomSheet) { if (state.value.showChangeCoverBottomSheet) {
BookInfoChangeCoverBottomSheet( BookInfoChangeCoverBottomSheet(
@ -185,102 +171,22 @@ private fun BookInfoScreen(
Scaffold( Scaffold(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.imePadding()
.windowInsetsPadding(WindowInsets.navigationBars) .windowInsetsPadding(WindowInsets.navigationBars)
.pullRefresh(refreshState) .pullRefresh(refreshState)
.nestedScroll(scrollBehavior.nestedScrollConnection), .nestedScroll(scrollBehavior.nestedScrollConnection),
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
topBar = { topBar = {
AnimatedTopAppBar( BookInfoTopBar(
containerColor = Color.Transparent,
scrollBehavior = scrollBehavior,
isTopBarScrolled = null,
content1NavigationIcon = {
if (state.value.editTitle) {
CustomIconButton(
icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = R.string.hide_edit_title_content_desc,
disableOnClick = true
) {
onEvent(BookInfoEvent.OnShowHideEditTitle)
}
} else {
GoBackButton(onNavigate = onNavigate, enabled = !state.value.isRefreshing) {
onEvent(BookInfoEvent.OnCancelUpdate)
}
}
},
content1Title = {
DefaultTransition(visible = firstVisibleItemIndex > 0) {
Text(
state.value.book.title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
},
content1Actions = {
CustomIconButton(
icon = Icons.Default.Refresh,
contentDescription = R.string.refresh_book_content_desc,
disableOnClick = false,
enabled = !state.value.isRefreshing && !state.value.isLoadingUpdate
) {
onEvent(
BookInfoEvent.OnLoadUpdate(
snackbarState,
context
)
)
}
Box {
DefaultTransition(
visible = !state.value.editTitle
) {
BookInfoMoreDropDown(
state = state, state = state,
onEvent = onEvent, onEvent = onEvent,
onLibraryEvent = onLibraryEvent,
onHistoryEvent = onHistoryEvent,
onNavigate = onNavigate,
scrollBehavior = scrollBehavior,
listState = listState,
snackbarState = snackbarState snackbarState = snackbarState
) )
}
CustomAnimatedVisibility(
visible = state.value.editTitle,
enter = Transitions.DefaultTransitionIn,
exit = fadeOut(tween(200))
) {
CustomIconButton(
icon = Icons.Default.Done,
contentDescription = R.string.apply_changes_content_desc,
disableOnClick = true,
enabled = !state.value.isRefreshing &&
state.value.titleValue.isNotBlank() &&
state.value.titleValue.trim() !=
state.value.book.title.trim(),
color = if (state.value.titleValue.isNotBlank()
&& state.value.titleValue != state.value.book.title
) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurfaceVariant
) {
onEvent(
BookInfoEvent.OnUpdateTitle(
refreshList = {
onLibraryEvent(LibraryEvent.OnUpdateBook(it))
onHistoryEvent(HistoryEvent.OnUpdateBook(it))
}
))
Toast.makeText(
context,
context.getString(R.string.title_changed),
Toast.LENGTH_LONG
).show()
}
}
}
}
)
}, },
bottomBar = { bottomBar = {
CustomSnackbar( CustomSnackbar(
@ -324,7 +230,10 @@ private fun BookInfoScreen(
item { item {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// Description // Description
BookInfoDescriptionSection(state = state) BookInfoDescriptionSection(
state = state,
onEvent = onEvent
)
} }
} }
@ -382,8 +291,24 @@ private fun BookInfoScreen(
} }
BackHandler { BackHandler {
var exitedEditMode = false
if (state.value.editTitle) { if (state.value.editTitle) {
onEvent(BookInfoEvent.OnShowHideEditTitle) onEvent(BookInfoEvent.OnShowHideEditTitle)
exitedEditMode = true
}
if (state.value.editAuthor) {
onEvent(BookInfoEvent.OnShowHideEditAuthor)
exitedEditMode = true
}
if (state.value.editDescription) {
onEvent(BookInfoEvent.OnShowHideEditDescription)
exitedEditMode = true
}
if (exitedEditMode) {
return@BackHandler return@BackHandler
} }

View file

@ -1,34 +1,93 @@
package ua.acclorite.book_story.presentation.screens.book_info.components package ua.acclorite.book_story.presentation.screens.book_info.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
/** /**
* Description section. * Description section.
*
* @param state [BookInfoState].
* @param onEvent [BookInfoEvent] callback.
*/ */
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun BookInfoDescriptionSection(state: State<BookInfoState>) { fun BookInfoDescriptionSection(
SelectionContainer { state: State<BookInfoState>,
onEvent: (BookInfoEvent) -> Unit
) {
val descriptionFocusRequester = remember { FocusRequester() }
if (!state.value.editDescription) {
Text( Text(
if (state.value.book.description?.isNotBlank() == true) state.value.book.description!! if (state.value.book.description?.isNotBlank() == true) state.value.book.description!!
else stringResource(id = R.string.error_no_description), else stringResource(id = R.string.error_no_description),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
modifier = Modifier.padding(horizontal = 24.dp) modifier = Modifier
.padding(horizontal = 24.dp)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {},
onLongClick = {
onEvent(BookInfoEvent.OnShowHideEditDescription)
}
),
) )
} else {
BasicTextField(
value = state.value.descriptionValue,
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurface
),
onValueChange = {
if (it.length < 5000 || it.length < state.value.authorValue.length) {
onEvent(BookInfoEvent.OnDescriptionValueChange(it))
}
},
modifier = Modifier
.padding(horizontal = 24.dp)
.focusRequester(descriptionFocusRequester)
.onGloballyPositioned {
onEvent(BookInfoEvent.OnDescriptionRequestFocus(descriptionFocusRequester))
},
keyboardOptions = KeyboardOptions(KeyboardCapitalization.Sentences),
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant)
) { innerText ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterStart
) {
innerText()
}
}
} }
Spacer(modifier = Modifier.height(96.dp)) Spacer(modifier = Modifier.height(96.dp))

View file

@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.Image
import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.Person
@ -56,7 +55,8 @@ fun BookInfoInfoSection(
state: State<BookInfoState>, state: State<BookInfoState>,
onEvent: (BookInfoEvent) -> Unit onEvent: (BookInfoEvent) -> Unit
) { ) {
val focusRequester = remember { FocusRequester() } val titleFocusRequester = remember { FocusRequester() }
val authorFocusRequester = remember { FocusRequester() }
Row( Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier verticalAlignment = Alignment.CenterVertically, modifier = Modifier
@ -135,9 +135,9 @@ fun BookInfoInfoSection(
} }
}, },
modifier = Modifier modifier = Modifier
.focusRequester(focusRequester) .focusRequester(titleFocusRequester)
.onGloballyPositioned { .onGloballyPositioned {
onEvent(BookInfoEvent.OnRequestFocus(focusRequester)) onEvent(BookInfoEvent.OnTitleRequestFocus(titleFocusRequester))
}, },
keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words), keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words),
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant)
@ -160,19 +160,54 @@ fun BookInfoInfoSection(
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
SelectionContainer {
if (!state.value.editAuthor) {
Text( Text(
state.value.book.author.asString(), state.value.book.author.asString(),
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth()
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {},
onLongClick = {
onEvent(BookInfoEvent.OnShowHideEditAuthor)
}
),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} else {
BasicTextField(
singleLine = true,
value = state.value.authorValue,
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant
),
onValueChange = {
if (it.length < 50 || it.length < state.value.authorValue.length) {
onEvent(BookInfoEvent.OnAuthorValueChange(it))
}
},
modifier = Modifier
.focusRequester(authorFocusRequester)
.onGloballyPositioned {
onEvent(BookInfoEvent.OnAuthorRequestFocus(authorFocusRequester))
},
keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words),
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant)
) { innerText ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterStart
) {
innerText()
}
}
} }
} }
} }
} }
} }

View file

@ -0,0 +1,188 @@
package ua.acclorite.book_story.presentation.screens.book_info.components
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.OnNavigate
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions
/**
* Book Info Top Bar.
*
* @param state [BookInfoState].
* @param onEvent [BookInfoEvent] callback.
* @param onLibraryEvent [LibraryEvent] callback.
* @param onHistoryEvent [HistoryEvent] callback.
* @param onNavigate Navigator callback.
* @param scrollBehavior [TopAppBarScrollBehavior].
* @param listState [LazyListState].
* @param snackbarState [SnackbarHostState].
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BookInfoTopBar(
state: State<BookInfoState>,
onEvent: (BookInfoEvent) -> Unit,
onLibraryEvent: (LibraryEvent) -> Unit,
onHistoryEvent: (HistoryEvent) -> Unit,
onNavigate: OnNavigate,
scrollBehavior: TopAppBarScrollBehavior,
listState: LazyListState,
snackbarState: SnackbarHostState
) {
val context = LocalContext.current
val firstVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } }
AnimatedTopAppBar(
containerColor = Color.Transparent,
scrollBehavior = scrollBehavior,
isTopBarScrolled = null,
content1NavigationIcon = {
if (
state.value.editTitle ||
state.value.editAuthor ||
state.value.editDescription
) {
CustomIconButton(
icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = R.string.exit_editing_content_desc,
disableOnClick = true
) {
if (state.value.editTitle) {
onEvent(BookInfoEvent.OnShowHideEditTitle)
}
if (state.value.editAuthor) {
onEvent(BookInfoEvent.OnShowHideEditAuthor)
}
if (state.value.editDescription) {
onEvent(BookInfoEvent.OnShowHideEditDescription)
}
}
} else {
GoBackButton(onNavigate = onNavigate, enabled = !state.value.isRefreshing) {
onEvent(BookInfoEvent.OnCancelUpdate)
}
}
},
content1Title = {
DefaultTransition(
firstVisibleItemIndex > 0 && !state.value.editTitle
) {
Text(
state.value.book.title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
},
content1Actions = {
CustomIconButton(
icon = Icons.Default.Refresh,
contentDescription = R.string.refresh_book_content_desc,
disableOnClick = false,
enabled = !state.value.isRefreshing && !state.value.isLoadingUpdate
) {
onEvent(
BookInfoEvent.OnLoadUpdate(
snackbarState,
context
)
)
}
Box {
DefaultTransition(
visible = !state.value.editTitle &&
!state.value.editAuthor &&
!state.value.editDescription
) {
BookInfoMoreDropDown(
state = state,
onEvent = onEvent,
snackbarState = snackbarState
)
}
CustomAnimatedVisibility(
visible = state.value.editTitle ||
state.value.editAuthor ||
state.value.editDescription,
enter = Transitions.DefaultTransitionIn,
exit = fadeOut(tween(200))
) {
CustomIconButton(
icon = Icons.Default.Done,
contentDescription = R.string.apply_changes_content_desc,
disableOnClick = true,
enabled = !state.value.isRefreshing &&
(state.value.titleValue.isNotBlank() &&
state.value.titleValue.trim() !=
state.value.book.title.trim()) ||
(state.value.authorValue.isNotBlank() &&
state.value.authorValue.trim() !=
state.value.book.author.getAsString()?.trim()) ||
(state.value.descriptionValue.isNotBlank() &&
state.value.descriptionValue.trim() !=
state.value.book.description?.trim()),
color = if ((state.value.titleValue.isNotBlank() &&
state.value.titleValue.trim() !=
state.value.book.title.trim()) ||
(state.value.authorValue.isNotBlank() &&
state.value.authorValue.trim() !=
state.value.book.author.getAsString()?.trim()) ||
(state.value.descriptionValue.isNotBlank() &&
state.value.descriptionValue.trim() !=
state.value.book.description?.trim())
) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurfaceVariant
) {
onEvent(
BookInfoEvent.OnUpdateData(
refreshList = {
onLibraryEvent(LibraryEvent.OnUpdateBook(it))
onHistoryEvent(HistoryEvent.OnUpdateBook(it))
}
))
}
}
}
}
)
}

View file

@ -23,10 +23,21 @@ sealed class BookInfoEvent {
val refreshList: (Book) -> Unit, val refreshList: (Book) -> Unit,
val showResult: (UIText) -> Unit val showResult: (UIText) -> Unit
) : BookInfoEvent() ) : BookInfoEvent()
data object OnShowHideEditTitle : BookInfoEvent() data object OnShowHideEditTitle : BookInfoEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent() data class OnTitleRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent()
data class OnTitleValueChange(val value: String) : BookInfoEvent() data class OnTitleValueChange(val value: String) : BookInfoEvent()
data class OnUpdateTitle(val refreshList: (Book) -> Unit) : BookInfoEvent()
data object OnShowHideEditAuthor : BookInfoEvent()
data class OnAuthorRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent()
data class OnAuthorValueChange(val value: String) : BookInfoEvent()
data object OnShowHideEditDescription : BookInfoEvent()
data class OnDescriptionRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent()
data class OnDescriptionValueChange(val value: String) : BookInfoEvent()
data class OnUpdateData(val refreshList: (Book) -> Unit) : BookInfoEvent()
data object OnShowHideDeleteDialog : BookInfoEvent() data object OnShowHideDeleteDialog : BookInfoEvent()
data object OnShowHideMoveDialog : BookInfoEvent() data object OnShowHideMoveDialog : BookInfoEvent()
data class OnDeleteBook(val refreshList: () -> Unit, val onNavigate: OnNavigate) : data class OnDeleteBook(val refreshList: () -> Unit, val onNavigate: OnNavigate) :

View file

@ -18,9 +18,17 @@ data class BookInfoState(
val textChanged: Boolean = false, val textChanged: Boolean = false,
val editTitle: Boolean = false, val editTitle: Boolean = false,
val hasFocused: Boolean = false, val hasTitleFocused: Boolean = false,
val titleValue: String = "", val titleValue: String = "",
val editAuthor: Boolean = false,
val hasAuthorFocused: Boolean = false,
val authorValue: String = "",
val editDescription: Boolean = false,
val hasDescriptionFocused: Boolean = false,
val descriptionValue: String = "",
val showChangeCoverBottomSheet: Boolean = false, val showChangeCoverBottomSheet: Boolean = false,
val canResetCover: Boolean = false, val canResetCover: Boolean = false,

View file

@ -165,7 +165,7 @@ class BookInfoViewModel @Inject constructor(
_state.update { _state.update {
it.copy( it.copy(
titleValue = it.book.title, titleValue = it.book.title,
hasFocused = false hasTitleFocused = false
) )
} }
} }
@ -178,12 +178,12 @@ class BookInfoViewModel @Inject constructor(
} }
} }
is BookInfoEvent.OnRequestFocus -> { is BookInfoEvent.OnTitleRequestFocus -> {
if (!_state.value.hasFocused) { if (!_state.value.hasTitleFocused) {
event.focusRequester.requestFocus() event.focusRequester.requestFocus()
_state.update { _state.update {
it.copy( it.copy(
hasFocused = true hasTitleFocused = true
) )
} }
} }
@ -197,26 +197,130 @@ class BookInfoViewModel @Inject constructor(
} }
} }
is BookInfoEvent.OnUpdateTitle -> { is BookInfoEvent.OnShowHideEditAuthor -> {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
val title = _state.value.titleValue.trim().replace("\n", "") val shouldHide = _state.value.editAuthor
updateBooks.execute( if (!shouldHide) {
listOf(
_state.value.book.copy(title = title)
)
)
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( authorValue = it.book.author.getAsString() ?: "",
title = title hasAuthorFocused = false
) )
}
}
_state.update {
it.copy(
editAuthor = !shouldHide
)
}
}
}
is BookInfoEvent.OnAuthorRequestFocus -> {
if (!_state.value.hasAuthorFocused) {
event.focusRequester.requestFocus()
_state.update {
it.copy(
hasAuthorFocused = true
)
}
}
}
is BookInfoEvent.OnAuthorValueChange -> {
_state.update {
it.copy(
authorValue = event.value
)
}
}
is BookInfoEvent.OnShowHideEditDescription -> {
viewModelScope.launch(Dispatchers.IO) {
val shouldHide = _state.value.editDescription
if (!shouldHide) {
_state.update {
it.copy(
descriptionValue = it.book.description ?: "",
hasDescriptionFocused = false
)
}
}
_state.update {
it.copy(
editDescription = !shouldHide
)
}
}
}
is BookInfoEvent.OnDescriptionRequestFocus -> {
if (!_state.value.hasDescriptionFocused) {
event.focusRequester.requestFocus()
_state.update {
it.copy(
hasDescriptionFocused = true
)
}
}
}
is BookInfoEvent.OnDescriptionValueChange -> {
_state.update {
it.copy(
descriptionValue = event.value
)
}
}
is BookInfoEvent.OnUpdateData -> {
viewModelScope.launch {
val title = _state.value.titleValue.trim().replace("\n", "")
val author = _state.value.authorValue.trim().replace("\n", "")
val description = _state.value.descriptionValue.trim().replace("\n", "")
val titleChanged = title != _state.value.book.title
&& _state.value.editTitle
&& title.isNotBlank()
val authorChanged = author != _state.value.book.author.getAsString()
&& _state.value.editAuthor
&& author.isNotBlank()
val descriptionChanged = description != _state.value.book.description
&& _state.value.editDescription
&& description.isNotBlank()
val book = _state.value.book.copy(
title = if (titleChanged) title else _state.value.book.title,
author = if (authorChanged) UIText.StringValue(author)
else _state.value.book.author,
description = if (descriptionChanged) description
else _state.value.book.description
)
updateBooks.execute(listOf(book))
_state.update {
it.copy(
book = book
) )
} }
event.refreshList(_state.value.book) event.refreshList(_state.value.book)
if (state.value.editTitle) {
onEvent(BookInfoEvent.OnShowHideEditTitle) onEvent(BookInfoEvent.OnShowHideEditTitle)
} }
if (state.value.editAuthor) {
onEvent(BookInfoEvent.OnShowHideEditAuthor)
}
if (state.value.editDescription) {
onEvent(BookInfoEvent.OnShowHideEditDescription)
}
}
} }
is BookInfoEvent.OnShowHideDeleteDialog -> { is BookInfoEvent.OnShowHideDeleteDialog -> {
@ -346,7 +450,9 @@ class BookInfoViewModel @Inject constructor(
_state.update { _state.update {
it.copy( it.copy(
isLoadingUpdate = true, isLoadingUpdate = true,
editTitle = false editTitle = false,
editAuthor = false,
editDescription = false
) )
} }
@ -708,10 +814,10 @@ class BookInfoViewModel @Inject constructor(
_state.update { _state.update {
BookInfoState( BookInfoState(
book = book, book = book
canResetCover = canResetCover.execute(screen.bookId)
) )
} }
onEvent(BookInfoEvent.OnCheckCoverReset)
} }
} }
} }

View file

@ -575,7 +575,7 @@
<string name="arrow_anim_content_desc">Анімація стрілки</string> <string name="arrow_anim_content_desc">Анімація стрілки</string>
<string name="required_content_desc">Необхідно</string> <string name="required_content_desc">Необхідно</string>
<string name="reset_start_content_desc">Скинути початковий посібник</string> <string name="reset_start_content_desc">Скинути початковий посібник</string>
<string name="hide_edit_title_content_desc">Вийти з редагування назви</string> <string name="exit_editing_content_desc">Вийти з режиму редагування</string>
<string name="open_in_web_content_desc">Відкрити в браузері</string> <string name="open_in_web_content_desc">Відкрити в браузері</string>
<string name="translator_content_desc">Перекладач</string> <string name="translator_content_desc">Перекладач</string>
<string name="translator_language_content_desc">Поміняти мови місцями</string> <string name="translator_language_content_desc">Поміняти мови місцями</string>

View file

@ -596,7 +596,7 @@
<string name="arrow_anim_content_desc">Arrow animation</string> <string name="arrow_anim_content_desc">Arrow animation</string>
<string name="required_content_desc">Required</string> <string name="required_content_desc">Required</string>
<string name="reset_start_content_desc">Reset start guide</string> <string name="reset_start_content_desc">Reset start guide</string>
<string name="hide_edit_title_content_desc">Exit title editing</string> <string name="exit_editing_content_desc">Exit editing mode</string>
<string name="open_in_web_content_desc">Open in web</string> <string name="open_in_web_content_desc">Open in web</string>
<string name="translator_content_desc">Translator</string> <string name="translator_content_desc">Translator</string>
<string name="translator_language_content_desc">Change language places</string> <string name="translator_language_content_desc">Change language places</string>