From fb3c996904f83c52a406ae58917eda42df4d04dd Mon Sep 17 00:00:00 2001 From: kunet Date: Sat, 29 Mar 2025 17:18:38 -0400 Subject: [PATCH] Add user manga notes (#428) Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit 8fbe630308b962043c7b59422878c94f80156e9f) # Conflicts: # CHANGELOG.md # app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt # app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt # app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt # app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt # app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt # data/src/main/sqldelight/tachiyomi/migrations/5.sqm # domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt --- app/build.gradle.kts | 1 + .../java/eu/kanade/domain/DomainModule.kt | 2 + .../presentation/manga/MangaNotesScreen.kt | 45 ++++ .../kanade/presentation/manga/MangaScreen.kt | 11 + .../manga/components/MangaInfoHeader.kt | 50 ++-- .../manga/components/MangaNotesDisplay.kt | 60 +++++ .../manga/components/MangaNotesSection.kt | 90 +++++++ .../manga/components/MangaNotesTextArea.kt | 224 ++++++++++++++++++ .../manga/components/MangaToolbar.kt | 7 + .../create/creators/MangaBackupCreator.kt | 1 + .../data/backup/models/BackupManga.kt | 3 + .../backup/restore/restorers/MangaRestorer.kt | 1 + .../ui/browse/migration/MigrationFlags.kt | 5 + .../design/MigrationBottomSheetDialog.kt | 3 + .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 2 + .../ui/manga/notes/MangaNotesScreen.kt | 61 +++++ .../res/layout/migration_bottom_sheet.xml | 9 +- .../main/java/tachiyomi/data/LibraryQuery.kt | 15 +- .../java/tachiyomi/data/manga/MangaMapper.kt | 5 + .../data/manga/MangaRepositoryImpl.kt | 1 + .../main/sqldelight/tachiyomi/data/mangas.sq | 6 +- .../sqldelight/tachiyomi/migrations/34.sqm | 3 + .../manga/interactor/UpdateMangaNotes.kt | 18 ++ .../tachiyomi/domain/manga/model/Manga.kt | 2 + .../domain/manga/model/MangaUpdate.kt | 2 + gradle/libs.versions.toml | 2 + .../moko-resources/base/strings.xml | 5 + 27 files changed, 610 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesDisplay.kt create mode 100644 app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt create mode 100644 app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt create mode 100644 data/src/main/sqldelight/tachiyomi/migrations/34.sqm create mode 100644 domain/src/main/java/tachiyomi/domain/manga/interactor/UpdateMangaNotes.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d8ff0cf52..b7652861a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -258,6 +258,7 @@ dependencies { } implementation(libs.insetter) implementation(libs.bundles.richtext) + implementation(libs.richeditor.compose) implementation(libs.aboutLibraries.compose) implementation(libs.bundles.voyager) implementation(libs.compose.materialmotion) diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 7e23ff3ef..bbfe19682 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -82,6 +82,7 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.ResetViewerFlags import tachiyomi.domain.manga.interactor.SetMangaChapterFlags +import tachiyomi.domain.manga.interactor.UpdateMangaNotes import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.release.interactor.GetApplicationRelease import tachiyomi.domain.release.service.ReleaseService @@ -128,6 +129,7 @@ class DomainModule : InjektModule { addFactory { SetMangaViewerFlags(get()) } addFactory { NetworkToLocalManga(get()) } addFactory { UpdateManga(get(), get()) } + addFactory { UpdateMangaNotes(get()) } addFactory { SetMangaCategories(get()) } addFactory { GetExcludedScanlators(get()) } addFactory { SetExcludedScanlators(get()) } diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt new file mode 100644 index 000000000..415e7b728 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt @@ -0,0 +1,45 @@ +package eu.kanade.presentation.manga + +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarTitle +import eu.kanade.presentation.manga.components.MangaNotesTextArea +import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.i18n.stringResource + +@Composable +fun MangaNotesScreen( + state: MangaNotesScreen.State, + navigateUp: () -> Unit, + onUpdate: (String) -> Unit, +) { + Scaffold( + topBar = { topBarScrollBehavior -> + AppBar( + titleContent = { + AppBarTitle( + title = stringResource(MR.strings.action_edit_notes), + subtitle = state.manga.title, + ) + }, + navigateUp = navigateUp, + scrollBehavior = topBarScrollBehavior, + ) + }, + ) { contentPadding -> + MangaNotesTextArea( + state = state, + onUpdate = onUpdate, + modifier = Modifier + .padding(contentPadding) + .consumeWindowInsets(contentPadding) + .imePadding(), + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index c844facca..981e3a57e 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -142,6 +142,7 @@ fun MangaScreen( onEditCategoryClicked: (() -> Unit)?, onEditFetchIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, + onEditNotesClicked: () -> Unit, // SY --> onMetadataViewerClicked: () -> Unit, onEditInfoClicked: () -> Unit, @@ -201,6 +202,7 @@ fun MangaScreen( onEditCategoryClicked = onEditCategoryClicked, onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, + onEditNotesClicked = onEditNotesClicked, // SY --> onMetadataViewerClicked = onMetadataViewerClicked, onEditInfoClicked = onEditInfoClicked, @@ -247,6 +249,7 @@ fun MangaScreen( onEditCategoryClicked = onEditCategoryClicked, onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, + onEditNotesClicked = onEditNotesClicked, // SY --> onMetadataViewerClicked = onMetadataViewerClicked, onEditInfoClicked = onEditInfoClicked, @@ -303,6 +306,7 @@ private fun MangaScreenSmallImpl( onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, + onEditNotesClicked: () -> Unit, // SY --> onMetadataViewerClicked: () -> Unit, onEditInfoClicked: () -> Unit, @@ -382,6 +386,7 @@ private fun MangaScreenSmallImpl( onClickEditCategory = onEditCategoryClicked, onClickRefresh = onRefresh, onClickMigrate = onMigrateClicked, + onClickEditNotes = onEditNotesClicked, // SY --> onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite }, onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow }, @@ -519,8 +524,10 @@ private fun MangaScreenSmallImpl( defaultExpandState = state.isFromSource, description = state.manga.description, tagsProvider = { state.manga.genre }, + notes = state.manga.notes, onTagSearch = onTagSearch, onCopyTagToClipboard = onCopyTagToClipboard, + onEditNotes = onEditNotesClicked, // SY --> doSearch = onSearch, searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) { @@ -626,6 +633,7 @@ fun MangaScreenLargeImpl( onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, + onEditNotesClicked: () -> Unit, // SY --> onMetadataViewerClicked: () -> Unit, onEditInfoClicked: () -> Unit, @@ -696,6 +704,7 @@ fun MangaScreenLargeImpl( onClickEditCategory = onEditCategoryClicked, onClickRefresh = onRefresh, onClickMigrate = onMigrateClicked, + onClickEditNotes = onEditNotesClicked, // SY --> onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite }, onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow }, @@ -814,8 +823,10 @@ fun MangaScreenLargeImpl( defaultExpandState = true, description = state.manga.description, tagsProvider = { state.manga.genre }, + notes = state.manga.notes, onTagSearch = onTagSearch, onCopyTagToClipboard = onCopyTagToClipboard, + onEditNotes = onEditNotesClicked, // SY --> doSearch = onSearch, searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) { diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index 39c808dd5..2683c782f 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -250,8 +250,10 @@ fun ExpandableMangaDescription( defaultExpandState: Boolean, description: String?, tagsProvider: () -> List?, + notes: String, onTagSearch: (String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit, + onEditNotes: () -> Unit, // SY --> searchMetadataChips: SearchMetadataChips?, doSearch: (query: String, global: Boolean) -> Unit, @@ -273,6 +275,8 @@ fun ExpandableMangaDescription( expandedDescription = desc, shrunkDescription = trimmedDescription, expanded = expanded, + notes = notes, + onEditNotesClicked = onEditNotes, modifier = Modifier .padding(top = 8.dp) .padding(horizontal = 16.dp) @@ -598,7 +602,9 @@ private fun ColumnScope.MangaContentInfo( private fun MangaSummary( expandedDescription: String, shrunkDescription: String, + notes: String, expanded: Boolean, + onEditNotesClicked: () -> Unit, modifier: Modifier = Modifier, ) { val animProgress by animateFloatAsState( @@ -610,25 +616,41 @@ private fun MangaSummary( contents = listOf( { Text( - text = "\n\n", // Shows at least 3 lines + // Shows at least 3 lines if no notes + // when there are notes show 6 + text = if (notes.isBlank()) "\n\n" else "\n\n\n\n\n", style = MaterialTheme.typography.bodyMedium, ) }, { - Text( - text = expandedDescription, - style = MaterialTheme.typography.bodyMedium, - ) - }, - { - SelectionContainer { - Text( - text = if (expanded) expandedDescription else shrunkDescription, - maxLines = Int.MAX_VALUE, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.secondaryItemAlpha(), + Column { + MangaNotesSection( + content = notes, + expanded = true, + onEditNotes = onEditNotesClicked, ) + Text( + text = expandedDescription, + style = MaterialTheme.typography.bodyMedium, + ) + } + }, + { + Column { + MangaNotesSection( + content = notes, + expanded = expanded, + onEditNotes = onEditNotesClicked, + ) + SelectionContainer { + Text( + text = if (expanded) expandedDescription else shrunkDescription, + maxLines = Int.MAX_VALUE, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.secondaryItemAlpha(), + ) + } } }, { diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesDisplay.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesDisplay.kt new file mode 100644 index 000000000..a7b93fbed --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesDisplay.kt @@ -0,0 +1,60 @@ +package eu.kanade.presentation.manga.components + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import com.mohamedrejeb.richeditor.model.rememberRichTextState +import com.mohamedrejeb.richeditor.ui.material3.RichText + +private val FADE_TIME = tween(500) + +@Composable +fun MangaNotesDisplay( + content: String, + modifier: Modifier, +) { + val alpha = remember { Animatable(1f) } + var contentUpdatedOnce by remember { mutableStateOf(false) } + + val richTextState = rememberRichTextState() + val primaryColor = MaterialTheme.colorScheme.primary + LaunchedEffect(content) { + richTextState.setMarkdown(content) + + if (!contentUpdatedOnce) { + contentUpdatedOnce = true + return@LaunchedEffect + } + + alpha.snapTo(targetValue = 0f) + alpha.animateTo(targetValue = 1f, animationSpec = FADE_TIME) + } + LaunchedEffect(Unit) { + richTextState.config.unorderedListIndent = 4 + richTextState.config.orderedListIndent = 20 + } + LaunchedEffect(primaryColor) { + richTextState.config.linkColor = primaryColor + } + + SelectionContainer { + RichText( + modifier = modifier + // Only animate size if the notes changes + .then(if (contentUpdatedOnce) Modifier.animateContentSize() else Modifier) + .alpha(alpha.value), + style = MaterialTheme.typography.bodyMedium, + state = richTextState, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt new file mode 100644 index 000000000..37adea58e --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt @@ -0,0 +1,90 @@ +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.filled.EditNote +import androidx.compose.material3.HorizontalDivider +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.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +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( + content: String, + expanded: Boolean, + onEditNotes: () -> Unit, + modifier: Modifier = Modifier, +) { + if (content.isBlank()) return + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + MangaNotesDisplay( + content = content, + modifier = modifier.fillMaxWidth(), + ) + if (expanded) { + Button( + onClick = onEditNotes, + 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.Filled.EditNote, + contentDescription = null, + modifier = Modifier + .size(16.dp), + ) + Text( + stringResource(MR.strings.action_edit_notes), + ) + } + } + } + + HorizontalDivider( + modifier = Modifier + .padding( + top = if (expanded) 0.dp else 12.dp, + bottom = if (expanded) 16.dp else 12.dp, + ), + ) + } +} + +@PreviewLightDark +@Composable +private fun MangaNotesSectionPreview() { + MangaNotesSection( + onEditNotes = {}, + expanded = true, + content = "# Hello world\ntest1234 hi there!", + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt new file mode 100644 index 000000000..12aca3d3b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt @@ -0,0 +1,224 @@ +package eu.kanade.presentation.manga.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted +import androidx.compose.material.icons.outlined.FormatBold +import androidx.compose.material.icons.outlined.FormatItalic +import androidx.compose.material.icons.outlined.FormatListNumbered +import androidx.compose.material.icons.outlined.FormatUnderlined +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import com.mohamedrejeb.richeditor.model.rememberRichTextState +import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor +import com.mohamedrejeb.richeditor.ui.material3.RichTextEditorDefaults.richTextEditorColors +import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.material.padding +import tachiyomi.presentation.core.i18n.stringResource +import kotlin.time.Duration.Companion.seconds + +private const val MAX_LENGTH = 250 +private const val MAX_LENGTH_WARN = MAX_LENGTH * 0.9 + +@Composable +fun MangaNotesTextArea( + state: MangaNotesScreen.State, + onUpdate: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val scope = rememberCoroutineScope() + val richTextState = rememberRichTextState() + val primaryColor = MaterialTheme.colorScheme.primary + + DisposableEffect(scope, richTextState) { + snapshotFlow { richTextState.annotatedString } + .debounce(0.25.seconds) + .distinctUntilChanged() + .map { richTextState.toMarkdown() } + .onEach { onUpdate(it) } + .launchIn(scope) + + onDispose { + onUpdate(richTextState.toMarkdown()) + } + } + LaunchedEffect(Unit) { + richTextState.setMarkdown(state.notes) + richTextState.config.unorderedListIndent = 4 + richTextState.config.orderedListIndent = 20 + } + LaunchedEffect(primaryColor) { + richTextState.config.linkColor = primaryColor + } + val focusRequester = remember { FocusRequester() } + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + } + val textLength = remember(richTextState.annotatedString) { richTextState.toText().length } + + Column( + modifier = modifier + .padding(horizontal = MaterialTheme.padding.small) + .fillMaxSize(), + ) { + RichTextEditor( + state = richTextState, + textStyle = MaterialTheme.typography.bodyLarge, + maxLength = MAX_LENGTH, + placeholder = { + Text(text = stringResource(MR.strings.notes_placeholder)) + }, + colors = richTextEditorColors( + containerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + contentPadding = PaddingValues( + horizontal = MaterialTheme.padding.medium, + ), + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .focusRequester(focusRequester), + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .padding(vertical = MaterialTheme.padding.small) + .fillMaxWidth(), + ) { + LazyRow( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + item { + MangaNotesTextAreaButton( + onClick = { richTextState.toggleSpanStyle(SpanStyle(fontWeight = FontWeight.Bold)) }, + isSelected = richTextState.currentSpanStyle.fontWeight == FontWeight.Bold, + icon = Icons.Outlined.FormatBold, + ) + } + item { + MangaNotesTextAreaButton( + onClick = { richTextState.toggleSpanStyle(SpanStyle(fontStyle = FontStyle.Italic)) }, + isSelected = richTextState.currentSpanStyle.fontStyle == FontStyle.Italic, + icon = Icons.Outlined.FormatItalic, + ) + } + item { + MangaNotesTextAreaButton( + onClick = { + richTextState.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.Underline)) + }, + isSelected = richTextState.currentSpanStyle.textDecoration + ?.contains(TextDecoration.Underline) + ?: false, + icon = Icons.Outlined.FormatUnderlined, + ) + } + item { + VerticalDivider( + modifier = Modifier + .padding(horizontal = MaterialTheme.padding.extraSmall) + .height(MaterialTheme.padding.large), + ) + } + item { + MangaNotesTextAreaButton( + onClick = { richTextState.toggleUnorderedList() }, + isSelected = richTextState.isUnorderedList, + icon = Icons.AutoMirrored.Outlined.FormatListBulleted, + ) + } + item { + MangaNotesTextAreaButton( + onClick = { richTextState.toggleOrderedList() }, + isSelected = richTextState.isOrderedList, + icon = Icons.Outlined.FormatListNumbered, + ) + } + } + + Box( + contentAlignment = Alignment.Center, + ) { + Text( + text = (MAX_LENGTH - textLength).toString(), + color = if (textLength > MAX_LENGTH_WARN) { + MaterialTheme.colorScheme.error + } else { + Color.Unspecified + }, + modifier = Modifier.padding(MaterialTheme.padding.extraSmall), + ) + } + } + } +} + +@Composable +fun MangaNotesTextAreaButton( + onClick: () -> Unit, + icon: ImageVector, + isSelected: Boolean, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .clip(MaterialTheme.shapes.small) + .clickable( + onClick = onClick, + enabled = true, + role = Role.Button, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = icon.name, + tint = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.primary, + modifier = Modifier + .background(color = if (isSelected) MaterialTheme.colorScheme.onBackground else Color.Transparent) + .padding(MaterialTheme.padding.extraSmall), + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt index 8e22b36df..72972c2a8 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt @@ -38,6 +38,7 @@ fun MangaToolbar( onClickEditCategory: (() -> Unit)?, onClickRefresh: () -> Unit, onClickMigrate: (() -> Unit)?, + onClickEditNotes: () -> Unit, // SY --> onClickEditInfo: (() -> Unit)?, onClickRecommend: (() -> Unit)?, @@ -147,6 +148,12 @@ fun MangaToolbar( ), ) } + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_notes), + onClick = onClickEditNotes, + ), + ) // SY --> if (onClickMerge != null) { add( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt index 1f02dc29d..79a73a057 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt @@ -135,6 +135,7 @@ private fun Manga.toBackupManga(/* SY --> */customMangaInfo: CustomMangaInfo?/* lastModifiedAt = this.lastModifiedAt, favoriteModifiedAt = this.favoriteModifiedAt, version = this.version, + notes = this.notes, // SY --> ).also { backupManga -> customMangaInfo?.let { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt index 1d5295889..fbe19b173 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt @@ -38,8 +38,10 @@ data class BackupManga( @ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE, @ProtoNumber(106) var lastModifiedAt: Long = 0, @ProtoNumber(107) var favoriteModifiedAt: Long? = null, + // Mihon values start here @ProtoNumber(108) var excludedScanlators: List = emptyList(), @ProtoNumber(109) var version: Long = 0, + @ProtoNumber(110) var notes: String = "", // SY specific values @ProtoNumber(600) var mergedMangaReferences: List = emptyList(), @@ -77,6 +79,7 @@ data class BackupManga( lastModifiedAt = this@BackupManga.lastModifiedAt, favoriteModifiedAt = this@BackupManga.favoriteModifiedAt, version = this@BackupManga.version, + notes = this@BackupManga.notes, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt index ff2be7731..91003d09d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt @@ -159,6 +159,7 @@ class MangaRestorer( updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode), version = manga.version, isSyncing = 1, + notes = manga.notes, ) } return manga diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt index f19ed1040..a4e822c5a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt @@ -8,6 +8,7 @@ object MigrationFlags { const val CUSTOM_COVER = 0b01000 const val EXTRA = 0b10000 const val DELETE_CHAPTERS = 0b100000 + const val NOTES = 0b1000000 fun hasChapters(value: Int): Boolean { return value and CHAPTERS != 0 @@ -32,4 +33,8 @@ object MigrationFlags { fun hasDeleteChapters(value: Int): Boolean { return value and DELETE_CHAPTERS != 0 } + + fun hasNotes(value: Int): Boolean { + return value and NOTES != 0 + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationBottomSheetDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationBottomSheetDialog.kt index 998f9627e..e9a797c29 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationBottomSheetDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationBottomSheetDialog.kt @@ -59,6 +59,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags) binding.migExtra.isChecked = MigrationFlags.hasExtra(flags) binding.migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteChapters(flags) + binding.migNotes.isChecked = MigrationFlags.hasNotes(flags) binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags(binding) } @@ -66,6 +67,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migDeleteDownloaded.setOnCheckedChangeListener { _, _ -> setFlags(binding) } + binding.migNotes.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.useSmartSearch.bindToPreference(preferences.smartMigration()) binding.extraSearchParamText.isVisible = false @@ -108,6 +110,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra if (binding.migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA if (binding.migDeleteDownloaded.isChecked) flags = flags or MigrationFlags.DELETE_CHAPTERS + if (binding.migNotes.isChecked) flags = flags or MigrationFlags.NOTES preferences.migrateFlags().set(flags) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 760db8123..1a8c76be8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -51,6 +51,7 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.home.HomeScreen import eu.kanade.tachiyomi.ui.manga.merged.EditMergedSettingsDialog +import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen import eu.kanade.tachiyomi.ui.manga.track.TrackInfoDialogHomeScreen import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.setting.SettingsScreen @@ -205,6 +206,7 @@ class MangaScreen( successState.manga.favorite }, previewsRowCount = successState.previewsRowCount, + onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) }, // SY --> onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite }, onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt new file mode 100644 index 000000000..03ee92df1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt @@ -0,0 +1,61 @@ +package eu.kanade.tachiyomi.ui.manga.notes + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.model.screenModelScope +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.presentation.manga.MangaNotesScreen +import eu.kanade.presentation.util.Screen +import kotlinx.coroutines.flow.update +import tachiyomi.core.common.util.lang.launchNonCancellable +import tachiyomi.domain.manga.interactor.UpdateMangaNotes +import tachiyomi.domain.manga.model.Manga +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MangaNotesScreen( + private val manga: Manga, +) : Screen() { + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + + val screenModel = rememberScreenModel { Model(manga) } + val state by screenModel.state.collectAsState() + + MangaNotesScreen( + state = state, + navigateUp = navigator::pop, + onUpdate = screenModel::updateNotes, + ) + } + + private class Model( + private val manga: Manga, + private val updateMangaNotes: UpdateMangaNotes = Injekt.get(), + ) : StateScreenModel(State(manga, manga.notes)) { + + fun updateNotes(content: String) { + if (content == state.value.notes) return + + mutableState.update { + it.copy(notes = content) + } + + screenModelScope.launchNonCancellable { + updateMangaNotes(manga.id, content) + } + } + } + + @Immutable + data class State( + val manga: Manga, + val notes: String, + ) +} diff --git a/app/src/main/res/layout/migration_bottom_sheet.xml b/app/src/main/res/layout/migration_bottom_sheet.xml index 48208974b..0be9c39f5 100644 --- a/app/src/main/res/layout/migration_bottom_sheet.xml +++ b/app/src/main/res/layout/migration_bottom_sheet.xml @@ -43,7 +43,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="horizontal" - app:constraint_referenced_ids="mig_chapters,mig_categories,mig_tracking,mig_custom_cover,mig_extra,mig_delete_downloaded" + app:constraint_referenced_ids="mig_chapters,mig_categories,mig_tracking,mig_custom_cover,mig_extra,mig_delete_downloaded,mig_notes" app:flow_horizontalBias="0" app:flow_horizontalGap="8dp" app:flow_horizontalStyle="packed" @@ -94,6 +94,13 @@ android:layout_height="wrap_content" android:checked="true" android:text="@string/delete_downloaded" /> + + favorite_modified_at = cursor.getLong(22), version = cursor.getLong(23)!!, is_syncing = cursor.getLong(24)!!, - totalCount = cursor.getLong(25)!!, - readCount = cursor.getDouble(26)!!, - latestUpload = cursor.getLong(27)!!, - chapterFetchedAt = cursor.getLong(28)!!, - lastRead = cursor.getLong(29)!!, - bookmarkCount = cursor.getDouble(30)!!, - category = cursor.getLong(31)!!, + notes = cursor.getString(25)!!, + totalCount = cursor.getLong(26)!!, + readCount = cursor.getDouble(27)!!, + latestUpload = cursor.getLong(28)!!, + chapterFetchedAt = cursor.getLong(29)!!, + lastRead = cursor.getLong(30)!!, + bookmarkCount = cursor.getDouble(31)!!, + category = cursor.getLong(32)!!, ) } diff --git a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt index be0255398..6d65dd66f 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt @@ -36,6 +36,7 @@ object MangaMapper { version: Long, @Suppress("UNUSED_PARAMETER") isSyncing: Long, + notes: String, ): Manga = Manga( id = id, source = source, @@ -62,6 +63,7 @@ object MangaMapper { lastModifiedAt = lastModifiedAt, favoriteModifiedAt = favoriteModifiedAt, version = version, + notes = notes, ) fun mapLibraryManga( @@ -93,6 +95,7 @@ object MangaMapper { favoriteModifiedAt: Long?, version: Long, isSyncing: Long, + notes: String, totalCount: Long, readCount: Double, latestUpload: Long, @@ -129,6 +132,7 @@ object MangaMapper { favoriteModifiedAt, version, isSyncing, + notes, ), category = category, totalChapters = totalCount, @@ -165,6 +169,7 @@ object MangaMapper { lastModifiedAt = libraryView.last_modified_at, favoriteModifiedAt = libraryView.favorite_modified_at, version = libraryView.version, + notes = libraryView.notes, ), category = libraryView.category, totalChapters = libraryView.totalCount, diff --git a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt index 3aea14393..50a7827ce 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt @@ -184,6 +184,7 @@ class MangaRepositoryImpl( updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode), version = value.version, isSyncing = 0, + notes = value.notes, ) } } diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq index 00c56e75e..0628c3226 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -28,7 +28,8 @@ CREATE TABLE mangas( last_modified_at INTEGER NOT NULL DEFAULT 0, favorite_modified_at INTEGER, version INTEGER NOT NULL DEFAULT 0, - is_syncing INTEGER NOT NULL DEFAULT 0 + is_syncing INTEGER NOT NULL DEFAULT 0, + notes TEXT NOT NULL DEFAULT "" ); CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1; @@ -187,7 +188,8 @@ UPDATE mangas SET update_strategy = coalesce(:updateStrategy, update_strategy), calculate_interval = coalesce(:calculateInterval, calculate_interval), version = coalesce(:version, version), - is_syncing = coalesce(:isSyncing, is_syncing) + is_syncing = coalesce(:isSyncing, is_syncing), + notes = coalesce(:notes, notes) WHERE _id = :mangaId; selectLastInsertedRowId: diff --git a/data/src/main/sqldelight/tachiyomi/migrations/34.sqm b/data/src/main/sqldelight/tachiyomi/migrations/34.sqm new file mode 100644 index 000000000..0cc85ecf4 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/34.sqm @@ -0,0 +1,3 @@ +-- Add notes column +ALTER TABLE mangas +ADD notes TEXT NOT NULL DEFAULT ""; diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/UpdateMangaNotes.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/UpdateMangaNotes.kt new file mode 100644 index 000000000..cadb3aed7 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/UpdateMangaNotes.kt @@ -0,0 +1,18 @@ +package tachiyomi.domain.manga.interactor + +import tachiyomi.domain.manga.model.MangaUpdate +import tachiyomi.domain.manga.repository.MangaRepository + +class UpdateMangaNotes( + private val mangaRepository: MangaRepository, +) { + + suspend operator fun invoke(mangaId: Long, notes: String): Boolean { + return mangaRepository.update( + MangaUpdate( + id = mangaId, + notes = notes, + ), + ) + } +} diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt index 2d6432d70..b92a55b23 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt @@ -36,6 +36,7 @@ data class Manga( val lastModifiedAt: Long, val favoriteModifiedAt: Long?, val version: Long, + val notes: String, ) : Serializable { // SY --> @@ -163,6 +164,7 @@ data class Manga( lastModifiedAt = 0L, favoriteModifiedAt = null, version = 0L, + notes = "", ) // SY --> diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt b/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt index 7fa5ce4ad..a582dc2fe 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt @@ -24,6 +24,7 @@ data class MangaUpdate( val updateStrategy: UpdateStrategy? = null, val initialized: Boolean? = null, val version: Long? = null, + val notes: String? = null, // SY --> val filteredScanlators: List? = null, // SY <-- @@ -54,5 +55,6 @@ fun Manga.toMangaUpdate(): MangaUpdate { updateStrategy = updateStrategy, initialized = initialized, version = version, + notes = notes, ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3133c25ea..07677d350 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -56,6 +56,8 @@ natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" } richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" } +richeditor-compose = "com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-rc10" + material = "com.google.android.material:material:1.12.0" flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533" photoview = "com.github.chrisbanes:PhotoView:2.3.0" diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 06ecd076b..c9a045f53 100755 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -147,6 +147,8 @@ Move series to top Move to bottom Move series to bottom + Notes + Edit notes Install Share Save @@ -983,4 +985,7 @@ HTTP %d, check website in WebView No Internet connection Couldn\'t reach %s + + + Enjoyed the part where…