Polishing off note editor

- moved note to manga screen
- added backup support
- make notes save
- change the way the initial model is loaded
This commit is contained in:
imkunet 2024-02-19 03:30:36 -05:00 committed by imkunet
parent 309086920c
commit 610e37510c
No known key found for this signature in database
GPG Key ID: 32E0ECFB90A68C42
14 changed files with 177 additions and 47 deletions

View File

@ -3,9 +3,14 @@ package eu.kanade.presentation.manga
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -39,12 +44,13 @@ fun MangaNotesScreen(
beginEditing: () -> Unit, beginEditing: () -> Unit,
endEditing: () -> Unit, endEditing: () -> Unit,
onSave: (String) -> Unit, onSave: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
titleContent = { titleContent = {
AppBarTitle(title = stringResource(MR.strings.action_notes), subtitle = state.title) AppBarTitle(title = stringResource(MR.strings.action_notes), subtitle = state.manga.title)
}, },
navigateUp = navigateUp, navigateUp = navigateUp,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
@ -60,7 +66,11 @@ fun MangaNotesScreen(
) { ) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { text = {
Text(text = stringResource(if (state.editing) MR.strings.action_apply else MR.strings.action_edit)) Text(
text = stringResource(
if (state.editing) MR.strings.action_apply else MR.strings.action_edit
),
)
}, },
icon = { icon = {
Icon( Icon(
@ -73,22 +83,28 @@ fun MangaNotesScreen(
) )
} }
}, },
modifier = modifier,
) { paddingValues -> ) { paddingValues ->
if (state.editing) { if (state.editing) {
MangaNotesTextArea( MangaNotesTextArea(
state = state,
onSave = onSave,
modifier = Modifier modifier = Modifier
.padding( .padding(
top = paddingValues.calculateTopPadding() + MaterialTheme.padding.small, top = paddingValues.calculateTopPadding() + MaterialTheme.padding.small,
bottom = MaterialTheme.padding.large, bottom = MaterialTheme.padding.small,
) )
.padding(horizontal = MaterialTheme.padding.small), .padding(horizontal = MaterialTheme.padding.small)
state = state, .windowInsetsPadding(
onSave = onSave, WindowInsets.navigationBars
.only(WindowInsetsSides.Bottom),
),
) )
return@Scaffold return@Scaffold
} }
if (state.content == null) {
if (state.notes.isNullOrBlank()) {
EmptyScreen( EmptyScreen(
stringRes = MR.strings.information_no_notes, stringRes = MR.strings.information_no_notes,
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
@ -101,8 +117,8 @@ fun MangaNotesScreen(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.fillMaxWidth() .fillMaxWidth()
.padding( .padding(
horizontal = MaterialTheme.padding.small, horizontal = MaterialTheme.padding.medium,
vertical = paddingValues.calculateTopPadding() + MaterialTheme.padding.small, vertical = paddingValues.calculateTopPadding() + MaterialTheme.padding.medium,
), ),
style = RichTextStyle( style = RichTextStyle(
stringStyle = RichTextStringStyle( stringStyle = RichTextStringStyle(
@ -110,7 +126,7 @@ fun MangaNotesScreen(
), ),
), ),
) { ) {
Markdown(content = state.content) Markdown(content = state.notes)
} }
return@Scaffold return@Scaffold

View File

@ -55,6 +55,7 @@ import eu.kanade.presentation.manga.components.MangaActionRow
import eu.kanade.presentation.manga.components.MangaBottomActionMenu import eu.kanade.presentation.manga.components.MangaBottomActionMenu
import eu.kanade.presentation.manga.components.MangaChapterListItem import eu.kanade.presentation.manga.components.MangaChapterListItem
import eu.kanade.presentation.manga.components.MangaInfoBox import eu.kanade.presentation.manga.components.MangaInfoBox
import eu.kanade.presentation.manga.components.MangaNotesSection
import eu.kanade.presentation.manga.components.MangaToolbar import eu.kanade.presentation.manga.components.MangaToolbar
import eu.kanade.presentation.manga.components.MissingChapterCountListItem import eu.kanade.presentation.manga.components.MissingChapterCountListItem
import eu.kanade.presentation.util.formatChapterNumber import eu.kanade.presentation.util.formatChapterNumber
@ -424,6 +425,16 @@ private fun MangaScreenSmallImpl(
) )
} }
item(
key = MangaScreenItem.NOTES_SECTION,
contentType = MangaScreenItem.NOTES_SECTION,
) {
MangaNotesSection(
onClickNotes = onClickNotes,
content = state.manga.notes,
)
}
item( item(
key = MangaScreenItem.CHAPTER_HEADER, key = MangaScreenItem.CHAPTER_HEADER,
contentType = MangaScreenItem.CHAPTER_HEADER, contentType = MangaScreenItem.CHAPTER_HEADER,

View File

@ -17,6 +17,7 @@ enum class MangaScreenItem {
INFO_BOX, INFO_BOX,
ACTION_ROW, ACTION_ROW,
DESCRIPTION_WITH_TAG, DESCRIPTION_WITH_TAG,
NOTES_SECTION,
CHAPTER_HEADER, CHAPTER_HEADER,
CHAPTER, CHAPTER,
} }

View File

@ -0,0 +1,101 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.string.RichTextStringStyle
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Button
import tachiyomi.presentation.core.components.material.ButtonDefaults
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun MangaNotesSection(
onClickNotes: () -> Unit,
content: String?,
modifier: Modifier = Modifier,
) {
Column(modifier.fillMaxWidth()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth(),
) {
if (!content.isNullOrBlank()) {
RichText(
modifier = Modifier
.fillMaxWidth()
.padding(MaterialTheme.padding.medium),
style = RichTextStyle(
stringStyle = RichTextStringStyle(
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
),
),
) {
Markdown(content = content)
}
}
Button(
onClick = onClickNotes,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.primary,
),
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 4.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Edit,
contentDescription = null,
modifier = Modifier
.size(16.dp),
)
Text(
stringResource(
if (content.isNullOrBlank()) {
MR.strings.action_add_notes
} else {
MR.strings.action_edit_notes
},
),
)
}
}
}
}
}
@PreviewLightDark
@Composable
private fun MangaNotesSectionPreview() {
MangaNotesSection(
onClickNotes = {},
content = "# Hello world\ntest1234 hi there!",
)
}

View File

@ -19,11 +19,11 @@ import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreenState
@Composable @Composable
fun MangaNotesTextArea( fun MangaNotesTextArea(
modifier: Modifier = Modifier,
state: MangaNotesScreenState.Success, state: MangaNotesScreenState.Success,
onSave: (String) -> Unit, onSave: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
var text by remember { mutableStateOf(state.content.orEmpty()) } var text by remember { mutableStateOf(state.notes.orEmpty()) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
Column( Column(

View File

@ -99,4 +99,5 @@ private fun Manga.toBackupManga() =
lastModifiedAt = this.lastModifiedAt, lastModifiedAt = this.lastModifiedAt,
favoriteModifiedAt = this.favoriteModifiedAt, favoriteModifiedAt = this.favoriteModifiedAt,
version = this.version, version = this.version,
notes = notes,
) )

View File

@ -38,8 +38,10 @@ data class BackupManga(
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE, @ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
@ProtoNumber(106) var lastModifiedAt: Long = 0, @ProtoNumber(106) var lastModifiedAt: Long = 0,
@ProtoNumber(107) var favoriteModifiedAt: Long? = null, @ProtoNumber(107) var favoriteModifiedAt: Long? = null,
// Mihon values start here
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(), @ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
@ProtoNumber(109) var version: Long = 0, @ProtoNumber(109) var version: Long = 0,
@ProtoNumber(110) var notes: String? = null,
) { ) {
fun getMangaImpl(): Manga { fun getMangaImpl(): Manga {
return Manga.create().copy( return Manga.create().copy(
@ -60,6 +62,7 @@ data class BackupManga(
lastModifiedAt = this@BackupManga.lastModifiedAt, lastModifiedAt = this@BackupManga.lastModifiedAt,
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt, favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
version = this@BackupManga.version, version = this@BackupManga.version,
notes = this@BackupManga.notes,
) )
} }
} }

View File

@ -129,6 +129,7 @@ class MangaRestorer(
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode), updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
version = manga.version, version = manga.version,
isSyncing = 1, isSyncing = 1,
notes = manga.notes,
) )
} }
return manga return manga

View File

@ -165,7 +165,7 @@ class MangaScreen(
onMigrateClicked = { onMigrateClicked = {
navigator.push(MigrateSearchScreen(successState.manga.id)) navigator.push(MigrateSearchScreen(successState.manga.id))
}.takeIf { successState.manga.favorite }, }.takeIf { successState.manga.favorite },
onNotesClicked = { navigator.push(MangaNotesScreen(successState.manga.id)) }, onNotesClicked = { navigator.push(MangaNotesScreen(successState.manga)) },
onMultiBookmarkClicked = screenModel::bookmarkChapters, onMultiBookmarkClicked = screenModel::bookmarkChapters,
onMultiMarkAsReadClicked = screenModel::markChaptersRead, onMultiMarkAsReadClicked = screenModel::markChaptersRead,
onMarkPreviousAsReadClicked = screenModel::markPreviousChapterRead, onMarkPreviousAsReadClicked = screenModel::markPreviousChapterRead,

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.manga.notes
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
@ -10,22 +9,17 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.manga.MangaNotesScreen import eu.kanade.presentation.manga.MangaNotesScreen
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
class MangaNotesScreen(private val mangaId: Long) : Screen() { class MangaNotesScreen(private val manga: Manga) : Screen() {
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { MangaNotesScreenModel(mangaId = mangaId) } val screenModel = rememberScreenModel { MangaNotesScreenModel(manga = manga) }
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
DisposableEffect(Unit) {
onDispose {
}
}
if (state is MangaNotesScreenState.Loading) { if (state is MangaNotesScreenState.Loading) {
LoadingScreen() LoadingScreen()
return return

View File

@ -3,36 +3,27 @@ package eu.kanade.tachiyomi.ui.manga.notes
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.tachiyomi.ui.category.CategoryEvent
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.SetMangaNotes import tachiyomi.domain.manga.interactor.SetMangaNotes
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class MangaNotesScreenModel( class MangaNotesScreenModel(
val mangaId: Long, val manga: Manga,
getManga: GetManga = Injekt.get(), private val setMangaNotes: SetMangaNotes = Injekt.get(),
setMangaNotes: SetMangaNotes = Injekt.get(),
) : StateScreenModel<MangaNotesScreenState>(MangaNotesScreenState.Loading) { ) : StateScreenModel<MangaNotesScreenState>(MangaNotesScreenState.Loading) {
private val _events: Channel<CategoryEvent> = Channel() private val successState: MangaNotesScreenState.Success?
val events = _events.receiveAsFlow() get() = state.value as? MangaNotesScreenState.Success
init { init {
screenModelScope.launch { mutableState.update {
getManga.subscribe(mangaId).collectLatest { manga -> MangaNotesScreenState.Success(
mutableState.update { manga = manga,
MangaNotesScreenState.Success( notes = manga.notes,
content = manga.notes, )
title = manga.title,
)
}
}
} }
} }
@ -55,14 +46,21 @@ class MangaNotesScreenModel(
} }
fun saveText(content: String) { fun saveText(content: String) {
// don't save what isn't modified
if (content == successState?.notes) return
mutableState.update { mutableState.update {
when (it) { when (it) {
MangaNotesScreenState.Loading -> it MangaNotesScreenState.Loading -> it
is MangaNotesScreenState.Success -> it.copy(content = content) is MangaNotesScreenState.Success -> {
it.copy(notes = content)
}
} }
} }
// do the magic to set it backend screenModelScope.launchNonCancellable {
setMangaNotes.awaitSetNotes(manga, content)
}
} }
} }
@ -73,8 +71,8 @@ sealed interface MangaNotesScreenState {
@Immutable @Immutable
data class Success( data class Success(
val content: String?, val manga: Manga,
val title: String, val notes: String?,
val editing: Boolean = false, val editing: Boolean = false,
) : MangaNotesScreenState ) : MangaNotesScreenState

View File

@ -167,6 +167,7 @@ class MangaRepositoryImpl(
updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode), updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode),
version = value.version, version = value.version,
isSyncing = 0, isSyncing = 0,
notes = value.notes
) )
} }
} }

View File

@ -167,7 +167,8 @@ UPDATE mangas SET
update_strategy = coalesce(:updateStrategy, update_strategy), update_strategy = coalesce(:updateStrategy, update_strategy),
calculate_interval = coalesce(:calculateInterval, calculate_interval), calculate_interval = coalesce(:calculateInterval, calculate_interval),
version = coalesce(:version, version), version = coalesce(:version, version),
is_syncing = coalesce(:isSyncing, is_syncing) is_syncing = coalesce(:isSyncing, is_syncing),
notes = coalesce(:notes, notes)
WHERE _id = :mangaId; WHERE _id = :mangaId;
selectLastInsertedRowId: selectLastInsertedRowId:

View File

@ -147,6 +147,8 @@
<string name="action_move_to_bottom">Move to bottom</string> <string name="action_move_to_bottom">Move to bottom</string>
<string name="action_move_to_bottom_all_for_series">Move series to bottom</string> <string name="action_move_to_bottom_all_for_series">Move series to bottom</string>
<string name="action_notes">Notes</string> <string name="action_notes">Notes</string>
<string name="action_add_notes">Add Notes</string>
<string name="action_edit_notes">Edit Notes</string>
<string name="action_install">Install</string> <string name="action_install">Install</string>
<string name="action_share">Share</string> <string name="action_share">Share</string>
<string name="action_save">Save</string> <string name="action_save">Save</string>