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
This commit is contained in:
parent
70b25825ec
commit
fb3c996904
@ -258,6 +258,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
implementation(libs.insetter)
|
implementation(libs.insetter)
|
||||||
implementation(libs.bundles.richtext)
|
implementation(libs.bundles.richtext)
|
||||||
|
implementation(libs.richeditor.compose)
|
||||||
implementation(libs.aboutLibraries.compose)
|
implementation(libs.aboutLibraries.compose)
|
||||||
implementation(libs.bundles.voyager)
|
implementation(libs.bundles.voyager)
|
||||||
implementation(libs.compose.materialmotion)
|
implementation(libs.compose.materialmotion)
|
||||||
|
@ -82,6 +82,7 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
|||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||||
|
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||||
import tachiyomi.domain.release.service.ReleaseService
|
import tachiyomi.domain.release.service.ReleaseService
|
||||||
@ -128,6 +129,7 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { SetMangaViewerFlags(get()) }
|
addFactory { SetMangaViewerFlags(get()) }
|
||||||
addFactory { NetworkToLocalManga(get()) }
|
addFactory { NetworkToLocalManga(get()) }
|
||||||
addFactory { UpdateManga(get(), get()) }
|
addFactory { UpdateManga(get(), get()) }
|
||||||
|
addFactory { UpdateMangaNotes(get()) }
|
||||||
addFactory { SetMangaCategories(get()) }
|
addFactory { SetMangaCategories(get()) }
|
||||||
addFactory { GetExcludedScanlators(get()) }
|
addFactory { GetExcludedScanlators(get()) }
|
||||||
addFactory { SetExcludedScanlators(get()) }
|
addFactory { SetExcludedScanlators(get()) }
|
||||||
|
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -142,6 +142,7 @@ fun MangaScreen(
|
|||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onEditFetchIntervalClicked: (() -> Unit)?,
|
onEditFetchIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
onEditNotesClicked: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
onMetadataViewerClicked: () -> Unit,
|
onMetadataViewerClicked: () -> Unit,
|
||||||
onEditInfoClicked: () -> Unit,
|
onEditInfoClicked: () -> Unit,
|
||||||
@ -201,6 +202,7 @@ fun MangaScreen(
|
|||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
onEditIntervalClicked = onEditFetchIntervalClicked,
|
onEditIntervalClicked = onEditFetchIntervalClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
|
onEditNotesClicked = onEditNotesClicked,
|
||||||
// SY -->
|
// SY -->
|
||||||
onMetadataViewerClicked = onMetadataViewerClicked,
|
onMetadataViewerClicked = onMetadataViewerClicked,
|
||||||
onEditInfoClicked = onEditInfoClicked,
|
onEditInfoClicked = onEditInfoClicked,
|
||||||
@ -247,6 +249,7 @@ fun MangaScreen(
|
|||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
onEditIntervalClicked = onEditFetchIntervalClicked,
|
onEditIntervalClicked = onEditFetchIntervalClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
|
onEditNotesClicked = onEditNotesClicked,
|
||||||
// SY -->
|
// SY -->
|
||||||
onMetadataViewerClicked = onMetadataViewerClicked,
|
onMetadataViewerClicked = onMetadataViewerClicked,
|
||||||
onEditInfoClicked = onEditInfoClicked,
|
onEditInfoClicked = onEditInfoClicked,
|
||||||
@ -303,6 +306,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onEditIntervalClicked: (() -> Unit)?,
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
onEditNotesClicked: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
onMetadataViewerClicked: () -> Unit,
|
onMetadataViewerClicked: () -> Unit,
|
||||||
onEditInfoClicked: () -> Unit,
|
onEditInfoClicked: () -> Unit,
|
||||||
@ -382,6 +386,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
onClickEditCategory = onEditCategoryClicked,
|
onClickEditCategory = onEditCategoryClicked,
|
||||||
onClickRefresh = onRefresh,
|
onClickRefresh = onRefresh,
|
||||||
onClickMigrate = onMigrateClicked,
|
onClickMigrate = onMigrateClicked,
|
||||||
|
onClickEditNotes = onEditNotesClicked,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
|
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
|
||||||
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
|
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
|
||||||
@ -519,8 +524,10 @@ private fun MangaScreenSmallImpl(
|
|||||||
defaultExpandState = state.isFromSource,
|
defaultExpandState = state.isFromSource,
|
||||||
description = state.manga.description,
|
description = state.manga.description,
|
||||||
tagsProvider = { state.manga.genre },
|
tagsProvider = { state.manga.genre },
|
||||||
|
notes = state.manga.notes,
|
||||||
onTagSearch = onTagSearch,
|
onTagSearch = onTagSearch,
|
||||||
onCopyTagToClipboard = onCopyTagToClipboard,
|
onCopyTagToClipboard = onCopyTagToClipboard,
|
||||||
|
onEditNotes = onEditNotesClicked,
|
||||||
// SY -->
|
// SY -->
|
||||||
doSearch = onSearch,
|
doSearch = onSearch,
|
||||||
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
|
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
|
||||||
@ -626,6 +633,7 @@ fun MangaScreenLargeImpl(
|
|||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onEditIntervalClicked: (() -> Unit)?,
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
onEditNotesClicked: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
onMetadataViewerClicked: () -> Unit,
|
onMetadataViewerClicked: () -> Unit,
|
||||||
onEditInfoClicked: () -> Unit,
|
onEditInfoClicked: () -> Unit,
|
||||||
@ -696,6 +704,7 @@ fun MangaScreenLargeImpl(
|
|||||||
onClickEditCategory = onEditCategoryClicked,
|
onClickEditCategory = onEditCategoryClicked,
|
||||||
onClickRefresh = onRefresh,
|
onClickRefresh = onRefresh,
|
||||||
onClickMigrate = onMigrateClicked,
|
onClickMigrate = onMigrateClicked,
|
||||||
|
onClickEditNotes = onEditNotesClicked,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
|
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
|
||||||
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
|
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
|
||||||
@ -814,8 +823,10 @@ fun MangaScreenLargeImpl(
|
|||||||
defaultExpandState = true,
|
defaultExpandState = true,
|
||||||
description = state.manga.description,
|
description = state.manga.description,
|
||||||
tagsProvider = { state.manga.genre },
|
tagsProvider = { state.manga.genre },
|
||||||
|
notes = state.manga.notes,
|
||||||
onTagSearch = onTagSearch,
|
onTagSearch = onTagSearch,
|
||||||
onCopyTagToClipboard = onCopyTagToClipboard,
|
onCopyTagToClipboard = onCopyTagToClipboard,
|
||||||
|
onEditNotes = onEditNotesClicked,
|
||||||
// SY -->
|
// SY -->
|
||||||
doSearch = onSearch,
|
doSearch = onSearch,
|
||||||
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
|
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
|
||||||
|
@ -250,8 +250,10 @@ fun ExpandableMangaDescription(
|
|||||||
defaultExpandState: Boolean,
|
defaultExpandState: Boolean,
|
||||||
description: String?,
|
description: String?,
|
||||||
tagsProvider: () -> List<String>?,
|
tagsProvider: () -> List<String>?,
|
||||||
|
notes: String,
|
||||||
onTagSearch: (String) -> Unit,
|
onTagSearch: (String) -> Unit,
|
||||||
onCopyTagToClipboard: (tag: String) -> Unit,
|
onCopyTagToClipboard: (tag: String) -> Unit,
|
||||||
|
onEditNotes: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
searchMetadataChips: SearchMetadataChips?,
|
searchMetadataChips: SearchMetadataChips?,
|
||||||
doSearch: (query: String, global: Boolean) -> Unit,
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
@ -273,6 +275,8 @@ fun ExpandableMangaDescription(
|
|||||||
expandedDescription = desc,
|
expandedDescription = desc,
|
||||||
shrunkDescription = trimmedDescription,
|
shrunkDescription = trimmedDescription,
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
|
notes = notes,
|
||||||
|
onEditNotesClicked = onEditNotes,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
@ -598,7 +602,9 @@ private fun ColumnScope.MangaContentInfo(
|
|||||||
private fun MangaSummary(
|
private fun MangaSummary(
|
||||||
expandedDescription: String,
|
expandedDescription: String,
|
||||||
shrunkDescription: String,
|
shrunkDescription: String,
|
||||||
|
notes: String,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
|
onEditNotesClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val animProgress by animateFloatAsState(
|
val animProgress by animateFloatAsState(
|
||||||
@ -610,17 +616,32 @@ private fun MangaSummary(
|
|||||||
contents = listOf(
|
contents = listOf(
|
||||||
{
|
{
|
||||||
Text(
|
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,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Column {
|
||||||
|
MangaNotesSection(
|
||||||
|
content = notes,
|
||||||
|
expanded = true,
|
||||||
|
onEditNotes = onEditNotesClicked,
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
text = expandedDescription,
|
text = expandedDescription,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Column {
|
||||||
|
MangaNotesSection(
|
||||||
|
content = notes,
|
||||||
|
expanded = expanded,
|
||||||
|
onEditNotes = onEditNotesClicked,
|
||||||
|
)
|
||||||
SelectionContainer {
|
SelectionContainer {
|
||||||
Text(
|
Text(
|
||||||
text = if (expanded) expandedDescription else shrunkDescription,
|
text = if (expanded) expandedDescription else shrunkDescription,
|
||||||
@ -630,6 +651,7 @@ private fun MangaSummary(
|
|||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)
|
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)
|
||||||
|
@ -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<Float>(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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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!",
|
||||||
|
)
|
||||||
|
}
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,7 @@ fun MangaToolbar(
|
|||||||
onClickEditCategory: (() -> Unit)?,
|
onClickEditCategory: (() -> Unit)?,
|
||||||
onClickRefresh: () -> Unit,
|
onClickRefresh: () -> Unit,
|
||||||
onClickMigrate: (() -> Unit)?,
|
onClickMigrate: (() -> Unit)?,
|
||||||
|
onClickEditNotes: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickEditInfo: (() -> Unit)?,
|
onClickEditInfo: (() -> Unit)?,
|
||||||
onClickRecommend: (() -> Unit)?,
|
onClickRecommend: (() -> Unit)?,
|
||||||
@ -147,6 +148,12 @@ fun MangaToolbar(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(MR.strings.action_notes),
|
||||||
|
onClick = onClickEditNotes,
|
||||||
|
),
|
||||||
|
)
|
||||||
// SY -->
|
// SY -->
|
||||||
if (onClickMerge != null) {
|
if (onClickMerge != null) {
|
||||||
add(
|
add(
|
||||||
|
@ -135,6 +135,7 @@ private fun Manga.toBackupManga(/* SY --> */customMangaInfo: CustomMangaInfo?/*
|
|||||||
lastModifiedAt = this.lastModifiedAt,
|
lastModifiedAt = this.lastModifiedAt,
|
||||||
favoriteModifiedAt = this.favoriteModifiedAt,
|
favoriteModifiedAt = this.favoriteModifiedAt,
|
||||||
version = this.version,
|
version = this.version,
|
||||||
|
notes = this.notes,
|
||||||
// SY -->
|
// SY -->
|
||||||
).also { backupManga ->
|
).also { backupManga ->
|
||||||
customMangaInfo?.let {
|
customMangaInfo?.let {
|
||||||
|
@ -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 = "",
|
||||||
|
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
||||||
@ -77,6 +79,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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,6 +159,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
|
||||||
|
@ -8,6 +8,7 @@ object MigrationFlags {
|
|||||||
const val CUSTOM_COVER = 0b01000
|
const val CUSTOM_COVER = 0b01000
|
||||||
const val EXTRA = 0b10000
|
const val EXTRA = 0b10000
|
||||||
const val DELETE_CHAPTERS = 0b100000
|
const val DELETE_CHAPTERS = 0b100000
|
||||||
|
const val NOTES = 0b1000000
|
||||||
|
|
||||||
fun hasChapters(value: Int): Boolean {
|
fun hasChapters(value: Int): Boolean {
|
||||||
return value and CHAPTERS != 0
|
return value and CHAPTERS != 0
|
||||||
@ -32,4 +33,8 @@ object MigrationFlags {
|
|||||||
fun hasDeleteChapters(value: Int): Boolean {
|
fun hasDeleteChapters(value: Int): Boolean {
|
||||||
return value and DELETE_CHAPTERS != 0
|
return value and DELETE_CHAPTERS != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasNotes(value: Int): Boolean {
|
||||||
|
return value and NOTES != 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra
|
|||||||
binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
|
binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
|
||||||
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
|
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
|
||||||
binding.migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteChapters(flags)
|
binding.migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteChapters(flags)
|
||||||
|
binding.migNotes.isChecked = MigrationFlags.hasNotes(flags)
|
||||||
|
|
||||||
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
||||||
binding.migCategories.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.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
||||||
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
||||||
binding.migDeleteDownloaded.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
binding.migDeleteDownloaded.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
||||||
|
binding.migNotes.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
||||||
|
|
||||||
binding.useSmartSearch.bindToPreference(preferences.smartMigration())
|
binding.useSmartSearch.bindToPreference(preferences.smartMigration())
|
||||||
binding.extraSearchParamText.isVisible = false
|
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.migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER
|
||||||
if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
|
if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
|
||||||
if (binding.migDeleteDownloaded.isChecked) flags = flags or MigrationFlags.DELETE_CHAPTERS
|
if (binding.migDeleteDownloaded.isChecked) flags = flags or MigrationFlags.DELETE_CHAPTERS
|
||||||
|
if (binding.migNotes.isChecked) flags = flags or MigrationFlags.NOTES
|
||||||
preferences.migrateFlags().set(flags)
|
preferences.migrateFlags().set(flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.category.CategoryScreen
|
||||||
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
||||||
import eu.kanade.tachiyomi.ui.manga.merged.EditMergedSettingsDialog
|
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.manga.track.TrackInfoDialogHomeScreen
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsScreen
|
import eu.kanade.tachiyomi.ui.setting.SettingsScreen
|
||||||
@ -205,6 +206,7 @@ class MangaScreen(
|
|||||||
successState.manga.favorite
|
successState.manga.favorite
|
||||||
},
|
},
|
||||||
previewsRowCount = successState.previewsRowCount,
|
previewsRowCount = successState.previewsRowCount,
|
||||||
|
onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) },
|
||||||
// SY -->
|
// SY -->
|
||||||
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
|
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
|
||||||
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
||||||
|
@ -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>(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,
|
||||||
|
)
|
||||||
|
}
|
@ -43,7 +43,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
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_horizontalBias="0"
|
||||||
app:flow_horizontalGap="8dp"
|
app:flow_horizontalGap="8dp"
|
||||||
app:flow_horizontalStyle="packed"
|
app:flow_horizontalStyle="packed"
|
||||||
@ -94,6 +94,13 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:checked="true"
|
android:checked="true"
|
||||||
android:text="@string/delete_downloaded" />
|
android:text="@string/delete_downloaded" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/mig_notes"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="@string/action_notes" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
@ -34,13 +34,14 @@ private val mapper = { cursor: SqlCursor ->
|
|||||||
favorite_modified_at = cursor.getLong(22),
|
favorite_modified_at = cursor.getLong(22),
|
||||||
version = cursor.getLong(23)!!,
|
version = cursor.getLong(23)!!,
|
||||||
is_syncing = cursor.getLong(24)!!,
|
is_syncing = cursor.getLong(24)!!,
|
||||||
totalCount = cursor.getLong(25)!!,
|
notes = cursor.getString(25)!!,
|
||||||
readCount = cursor.getDouble(26)!!,
|
totalCount = cursor.getLong(26)!!,
|
||||||
latestUpload = cursor.getLong(27)!!,
|
readCount = cursor.getDouble(27)!!,
|
||||||
chapterFetchedAt = cursor.getLong(28)!!,
|
latestUpload = cursor.getLong(28)!!,
|
||||||
lastRead = cursor.getLong(29)!!,
|
chapterFetchedAt = cursor.getLong(29)!!,
|
||||||
bookmarkCount = cursor.getDouble(30)!!,
|
lastRead = cursor.getLong(30)!!,
|
||||||
category = cursor.getLong(31)!!,
|
bookmarkCount = cursor.getDouble(31)!!,
|
||||||
|
category = cursor.getLong(32)!!,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ object MangaMapper {
|
|||||||
version: Long,
|
version: Long,
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
isSyncing: Long,
|
isSyncing: Long,
|
||||||
|
notes: String,
|
||||||
): Manga = Manga(
|
): Manga = Manga(
|
||||||
id = id,
|
id = id,
|
||||||
source = source,
|
source = source,
|
||||||
@ -62,6 +63,7 @@ object MangaMapper {
|
|||||||
lastModifiedAt = lastModifiedAt,
|
lastModifiedAt = lastModifiedAt,
|
||||||
favoriteModifiedAt = favoriteModifiedAt,
|
favoriteModifiedAt = favoriteModifiedAt,
|
||||||
version = version,
|
version = version,
|
||||||
|
notes = notes,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun mapLibraryManga(
|
fun mapLibraryManga(
|
||||||
@ -93,6 +95,7 @@ object MangaMapper {
|
|||||||
favoriteModifiedAt: Long?,
|
favoriteModifiedAt: Long?,
|
||||||
version: Long,
|
version: Long,
|
||||||
isSyncing: Long,
|
isSyncing: Long,
|
||||||
|
notes: String,
|
||||||
totalCount: Long,
|
totalCount: Long,
|
||||||
readCount: Double,
|
readCount: Double,
|
||||||
latestUpload: Long,
|
latestUpload: Long,
|
||||||
@ -129,6 +132,7 @@ object MangaMapper {
|
|||||||
favoriteModifiedAt,
|
favoriteModifiedAt,
|
||||||
version,
|
version,
|
||||||
isSyncing,
|
isSyncing,
|
||||||
|
notes,
|
||||||
),
|
),
|
||||||
category = category,
|
category = category,
|
||||||
totalChapters = totalCount,
|
totalChapters = totalCount,
|
||||||
@ -165,6 +169,7 @@ object MangaMapper {
|
|||||||
lastModifiedAt = libraryView.last_modified_at,
|
lastModifiedAt = libraryView.last_modified_at,
|
||||||
favoriteModifiedAt = libraryView.favorite_modified_at,
|
favoriteModifiedAt = libraryView.favorite_modified_at,
|
||||||
version = libraryView.version,
|
version = libraryView.version,
|
||||||
|
notes = libraryView.notes,
|
||||||
),
|
),
|
||||||
category = libraryView.category,
|
category = libraryView.category,
|
||||||
totalChapters = libraryView.totalCount,
|
totalChapters = libraryView.totalCount,
|
||||||
|
@ -184,6 +184,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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,8 @@ CREATE TABLE mangas(
|
|||||||
last_modified_at INTEGER NOT NULL DEFAULT 0,
|
last_modified_at INTEGER NOT NULL DEFAULT 0,
|
||||||
favorite_modified_at INTEGER,
|
favorite_modified_at INTEGER,
|
||||||
version INTEGER NOT NULL DEFAULT 0,
|
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;
|
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
|
||||||
@ -187,7 +188,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:
|
||||||
|
3
data/src/main/sqldelight/tachiyomi/migrations/34.sqm
Normal file
3
data/src/main/sqldelight/tachiyomi/migrations/34.sqm
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
-- Add notes column
|
||||||
|
ALTER TABLE mangas
|
||||||
|
ADD notes TEXT NOT NULL DEFAULT "";
|
@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -36,6 +36,7 @@ data class Manga(
|
|||||||
val lastModifiedAt: Long,
|
val lastModifiedAt: Long,
|
||||||
val favoriteModifiedAt: Long?,
|
val favoriteModifiedAt: Long?,
|
||||||
val version: Long,
|
val version: Long,
|
||||||
|
val notes: String,
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@ -163,6 +164,7 @@ data class Manga(
|
|||||||
lastModifiedAt = 0L,
|
lastModifiedAt = 0L,
|
||||||
favoriteModifiedAt = null,
|
favoriteModifiedAt = null,
|
||||||
version = 0L,
|
version = 0L,
|
||||||
|
notes = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
@ -24,6 +24,7 @@ data class MangaUpdate(
|
|||||||
val updateStrategy: UpdateStrategy? = null,
|
val updateStrategy: UpdateStrategy? = null,
|
||||||
val initialized: Boolean? = null,
|
val initialized: Boolean? = null,
|
||||||
val version: Long? = null,
|
val version: Long? = null,
|
||||||
|
val notes: String? = null,
|
||||||
// SY -->
|
// SY -->
|
||||||
val filteredScanlators: List<String>? = null,
|
val filteredScanlators: List<String>? = null,
|
||||||
// SY <--
|
// SY <--
|
||||||
@ -54,5 +55,6 @@ fun Manga.toMangaUpdate(): MangaUpdate {
|
|||||||
updateStrategy = updateStrategy,
|
updateStrategy = updateStrategy,
|
||||||
initialized = initialized,
|
initialized = initialized,
|
||||||
version = version,
|
version = version,
|
||||||
|
notes = notes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
|
||||||
richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", 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"
|
material = "com.google.android.material:material:1.12.0"
|
||||||
flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
|
flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
|
||||||
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
||||||
|
@ -147,6 +147,8 @@
|
|||||||
<string name="action_move_to_top_all_for_series">Move series to top</string>
|
<string name="action_move_to_top_all_for_series">Move series to top</string>
|
||||||
<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_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>
|
||||||
@ -983,4 +985,7 @@
|
|||||||
<string name="exception_http">HTTP %d, check website in WebView</string>
|
<string name="exception_http">HTTP %d, check website in WebView</string>
|
||||||
<string name="exception_offline">No Internet connection</string>
|
<string name="exception_offline">No Internet connection</string>
|
||||||
<string name="exception_unknown_host">Couldn\'t reach %s</string>
|
<string name="exception_unknown_host">Couldn\'t reach %s</string>
|
||||||
|
|
||||||
|
<!-- Notes screen -->
|
||||||
|
<string name="notes_placeholder">Enjoyed the part where…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user