This commit is contained in:
Kyle Nguyen 2024-10-24 19:43:03 +09:00 committed by GitHub
commit 1436926906
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 608 additions and 17 deletions

View File

@ -235,6 +235,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)

View File

@ -77,6 +77,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.SetMangaNotes
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.domain.release.service.ReleaseService
@ -122,6 +123,7 @@ class DomainModule : InjektModule {
addFactory { GetUpcomingManga(get()) }
addFactory { ResetViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) }
addFactory { SetMangaNotes(get()) }
addFactory { FetchInterval(get()) }
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
addFactory { SetMangaViewerFlags(get()) }

View File

@ -0,0 +1,57 @@
package eu.kanade.presentation.manga
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.MaterialTheme
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.MangaNotesScreenState
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun MangaNotesScreen(
state: MangaNotesScreenState.Success,
navigateUp: () -> Unit,
onSave: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
topBar = { scrollBehavior ->
AppBar(
titleContent = {
AppBarTitle(title = stringResource(MR.strings.action_notes), subtitle = state.manga.title)
},
navigateUp = navigateUp,
scrollBehavior = scrollBehavior,
)
},
modifier = modifier
.imePadding(),
) { paddingValues ->
MangaNotesTextArea(
state = state,
onSave = onSave,
modifier = Modifier
.padding(
top = paddingValues.calculateTopPadding() + MaterialTheme.padding.small,
bottom = MaterialTheme.padding.small,
)
.padding(horizontal = MaterialTheme.padding.small)
.windowInsetsPadding(
WindowInsets.navigationBars
.only(WindowInsetsSides.Bottom),
),
)
}
}

View File

@ -112,6 +112,7 @@ fun MangaScreen(
onEditCategoryClicked: (() -> Unit)?,
onEditFetchIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
onNotesClicked: () -> Unit,
// For bottom action menu
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
@ -160,6 +161,7 @@ fun MangaScreen(
onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditFetchIntervalClicked,
onMigrateClicked = onMigrateClicked,
onNotesClicked = onNotesClicked,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
@ -195,6 +197,7 @@ fun MangaScreen(
onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditFetchIntervalClicked,
onMigrateClicked = onMigrateClicked,
onNotesClicked = onNotesClicked,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
@ -240,6 +243,7 @@ private fun MangaScreenSmallImpl(
onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
onNotesClicked: () -> Unit,
// For bottom action menu
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
@ -305,6 +309,7 @@ private fun MangaScreenSmallImpl(
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onClickNotes = onNotesClicked,
actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
@ -340,7 +345,9 @@ private fun MangaScreenSmallImpl(
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
text = stringResource(
if (isReading) MR.strings.action_resume else MR.strings.action_start,
),
)
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
@ -414,8 +421,10 @@ private fun MangaScreenSmallImpl(
defaultExpandState = state.isFromSource,
description = state.manga.description,
tagsProvider = { state.manga.genre },
noteContent = state.manga.notes,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
onClickNotes = onNotesClicked,
)
}
@ -484,6 +493,7 @@ fun MangaScreenLargeImpl(
onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
onNotesClicked: () -> Unit,
// For bottom action menu
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
@ -542,6 +552,7 @@ fun MangaScreenLargeImpl(
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onClickNotes = onNotesClicked,
actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
@ -640,8 +651,10 @@ fun MangaScreenLargeImpl(
defaultExpandState = true,
description = state.manga.description,
tagsProvider = { state.manga.genre },
noteContent = state.manga.notes,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
onClickNotes = onNotesClicked,
)
}
},
@ -761,6 +774,7 @@ private fun LazyListScope.sharedChapterItems(
is ChapterList.MissingCount -> {
MissingChapterCountListItem(count = item.count)
}
is ChapterList.Item -> {
MangaChapterListItem(
title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) {

View File

@ -236,8 +236,10 @@ fun ExpandableMangaDescription(
defaultExpandState: Boolean,
description: String?,
tagsProvider: () -> List<String>?,
noteContent: String?,
onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit,
onClickNotes: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
@ -255,6 +257,8 @@ fun ExpandableMangaDescription(
expandedDescription = desc,
shrunkDescription = trimmedDescription,
expanded = expanded,
noteContent = noteContent,
onNotesEditClicked = onClickNotes,
modifier = Modifier
.padding(top = 8.dp)
.padding(horizontal = 16.dp)
@ -559,7 +563,9 @@ private fun ColumnScope.MangaContentInfo(
private fun MangaSummary(
expandedDescription: String,
shrunkDescription: String,
noteContent: String?,
expanded: Boolean,
onNotesEditClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
val animProgress by animateFloatAsState(
@ -571,25 +577,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 (noteContent.isNullOrBlank()) "\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 = noteContent,
expanded = true,
onClickNotes = onNotesEditClicked,
)
Text(
text = expandedDescription,
style = MaterialTheme.typography.bodyMedium,
)
}
},
{
Column {
MangaNotesSection(
content = noteContent,
expanded = expanded,
onClickNotes = onNotesEditClicked,
)
SelectionContainer {
Text(
text = if (expanded) expandedDescription else shrunkDescription,
maxLines = Int.MAX_VALUE,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.secondaryItemAlpha(),
)
}
}
},
{

View File

@ -0,0 +1,28 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi
import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.RichText
@OptIn(ExperimentalRichTextApi::class)
@Composable
fun MangaNotesDisplay(
content: String,
modifier: Modifier,
) {
val richTextState = rememberRichTextState().setMarkdown(markdown = content)
richTextState.config.linkColor = MaterialTheme.colorScheme.primary
richTextState.config.listIndent = 15
SelectionContainer {
RichText(
modifier = modifier,
style = MaterialTheme.typography.bodyMedium,
state = richTextState,
)
}
}

View File

@ -0,0 +1,110 @@
package eu.kanade.presentation.manga.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.spring
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
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,
onClickNotes: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (!content.isNullOrBlank()) {
MangaNotesDisplay(
content = content,
modifier = modifier.fillMaxWidth(),
)
AnimatedVisibility(
visible = expanded,
enter = fadeIn(animationSpec = spring()) + expandVertically(animationSpec = spring()),
exit = fadeOut(animationSpec = spring()) + shrinkVertically(animationSpec = spring()),
) {
Button(
onClick = onClickNotes,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.primary,
),
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 4.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Filled.EditNote,
contentDescription = null,
modifier = Modifier
.size(16.dp),
)
Text(
stringResource(MR.strings.action_edit_notes),
)
}
}
}
Column(
modifier = Modifier
.animateContentSize(
animationSpec = spring(),
),
) {
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(
onClickNotes = {},
expanded = true,
content = "# Hello world\ntest1234 hi there!",
)
}

View File

@ -0,0 +1,200 @@
package eu.kanade.presentation.manga.components
import androidx.compose.animation.AnimatedVisibility
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.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.isImeVisible
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.RoundedCornerShape
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.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.RichTextState
import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreenState
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
private const val MAX_LENGTH = 250
private fun RichTextState.render(): String {
var current: String
var mutated = this.toMarkdown().replace("\n<br>\n<br>", "")
do {
current = mutated
mutated = mutated.trim { it.isWhitespace() || it == '\n' }
mutated = mutated.removeSuffix("<br>").removePrefix("<br>")
} while (mutated != current)
return current
}
@Composable
fun MangaNotesTextArea(
state: MangaNotesScreenState.Success,
onSave: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val richTextState = rememberRichTextState()
richTextState.config.linkColor = MaterialTheme.colorScheme.primary
richTextState.config.listIndent = 15
val focusRequester = remember { FocusRequester() }
Column(
modifier = modifier
.fillMaxSize(),
) {
RichTextEditor(
state = richTextState,
textStyle = MaterialTheme.typography.bodyLarge,
maxLength = MAX_LENGTH,
placeholder = {
Text(text = stringResource(MR.strings.notes_placeholder))
},
supportingText = {
Text(
text = (MAX_LENGTH - richTextState.render().length).toString(),
color = if (richTextState.render().length >
MAX_LENGTH / 10 * 9
) {
MaterialTheme.colorScheme.error
} else {
Color.Unspecified
},
)
},
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.focusRequester(focusRequester),
)
AnimatedVisibility(
visible = WindowInsets.isImeVisible,
) {
LazyRow(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.padding(top = 4.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) == true,
icon = Icons.Outlined.FormatUnderlined,
)
}
item {
VerticalDivider(
modifier = Modifier
.height(24.dp),
)
}
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,
)
}
}
}
}
LaunchedEffect(focusRequester) {
state.notes?.let { richTextState.setMarkdown(it) }
focusRequester.requestFocus()
}
DisposableEffect(Unit) {
onDispose {
onSave(richTextState.render())
}
}
}
@Composable
fun MangaNotesTextAreaButton(
onClick: () -> Unit,
icon: ImageVector,
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.clip(RoundedCornerShape(10.dp))
.clickable(
onClick = onClick,
enabled = true,
role = Role.Button,
),
contentAlignment = Alignment.Center,
) {
Icon(
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(6.dp),
)
}
}

View File

@ -44,6 +44,7 @@ fun MangaToolbar(
onClickEditCategory: (() -> Unit)?,
onClickRefresh: () -> Unit,
onClickMigrate: (() -> Unit)?,
onClickNotes: () -> Unit,
// For action mode
actionModeCounter: Int,
@ -149,6 +150,13 @@ fun MangaToolbar(
),
)
}
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_notes),
onClick = onClickNotes,
),
)
}
.build(),
)

View File

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

View File

@ -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<String> = emptyList(),
@ProtoNumber(109) var version: Long = 0,
@ProtoNumber(110) var notes: String? = null,
) {
fun getMangaImpl(): Manga {
return Manga.create().copy(
@ -60,6 +62,7 @@ data class BackupManga(
lastModifiedAt = this@BackupManga.lastModifiedAt,
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
version = this@BackupManga.version,
notes = this@BackupManga.notes,
)
}
}

View File

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

View File

@ -50,6 +50,7 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
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.notes.MangaNotesScreen
import eu.kanade.tachiyomi.ui.manga.track.TrackInfoDialogHomeScreen
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.setting.SettingsScreen
@ -164,6 +165,7 @@ class MangaScreen(
onMigrateClicked = {
navigator.push(MigrateSearchScreen(successState.manga.id))
}.takeIf { successState.manga.favorite },
onNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) },
onMultiBookmarkClicked = screenModel::bookmarkChapters,
onMultiMarkAsReadClicked = screenModel::markChaptersRead,
onMarkPreviousAsReadClicked = screenModel::markPreviousChapterRead,

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.ui.manga.notes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import cafe.adriel.voyager.core.model.rememberScreenModel
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 tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.screens.LoadingScreen
class MangaNotesScreen(
private val manga: Manga,
) : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel {
MangaNotesScreenModel(
manga = manga,
)
}
val state by screenModel.state.collectAsState()
if (state is MangaNotesScreenState.Loading) {
LoadingScreen()
return
}
val successState = state as MangaNotesScreenState.Success
MangaNotesScreen(
state = successState,
navigateUp = navigator::pop,
onSave = { screenModel.saveText(it) },
)
}
}

View File

@ -0,0 +1,59 @@
package eu.kanade.tachiyomi.ui.manga.notes
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import kotlinx.coroutines.flow.update
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.domain.manga.interactor.SetMangaNotes
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaNotesScreenModel(
val manga: Manga,
private val setMangaNotes: SetMangaNotes = Injekt.get(),
) : StateScreenModel<MangaNotesScreenState>(MangaNotesScreenState.Loading) {
private val successState: MangaNotesScreenState.Success?
get() = state.value as? MangaNotesScreenState.Success
init {
mutableState.update {
MangaNotesScreenState.Success(
manga = manga,
notes = manga.notes,
)
}
}
fun saveText(content: String) {
// don't save what isn't modified
if (content == successState?.notes) return
mutableState.update {
when (it) {
MangaNotesScreenState.Loading -> it
is MangaNotesScreenState.Success -> {
it.copy(notes = content)
}
}
}
screenModelScope.launchNonCancellable {
setMangaNotes.awaitSetNotes(manga, content)
}
}
}
sealed interface MangaNotesScreenState {
@Immutable
data object Loading : MangaNotesScreenState
@Immutable
data class Success(
val manga: Manga,
val notes: String?,
) : MangaNotesScreenState
}

View File

@ -31,6 +31,7 @@ object MangaMapper {
version: Long,
@Suppress("UNUSED_PARAMETER")
isSyncing: Long,
notes: String?,
): Manga = Manga(
id = id,
source = source,
@ -55,6 +56,7 @@ object MangaMapper {
lastModifiedAt = lastModifiedAt,
favoriteModifiedAt = favoriteModifiedAt,
version = version,
notes = notes,
)
fun mapLibraryManga(
@ -82,6 +84,7 @@ object MangaMapper {
favoriteModifiedAt: Long?,
version: Long,
isSyncing: Long,
notes: String?,
totalCount: Long,
readCount: Double,
latestUpload: Long,
@ -115,6 +118,7 @@ object MangaMapper {
favoriteModifiedAt,
version,
isSyncing,
notes,
),
category = category,
totalChapters = totalCount,

View File

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

View File

@ -27,7 +27,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
);
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
@ -175,7 +176,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:

View File

@ -0,0 +1,3 @@
-- Add notes column
ALTER TABLE mangas
ADD notes TEXT;

View File

@ -0,0 +1,19 @@
package tachiyomi.domain.manga.interactor
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository
class SetMangaNotes(
private val mangaRepository: MangaRepository,
) {
suspend fun awaitSetNotes(manga: Manga, notes: String): Boolean {
return mangaRepository.update(
MangaUpdate(
id = manga.id,
notes = notes,
),
)
}
}

View File

@ -32,6 +32,7 @@ data class Manga(
val lastModifiedAt: Long,
val favoriteModifiedAt: Long?,
val version: Long,
val notes: String?,
) : Serializable {
val expectedNextUpdate: Instant?
@ -126,6 +127,7 @@ data class Manga(
lastModifiedAt = 0L,
favoriteModifiedAt = null,
version = 0L,
notes = null,
)
}
}

View File

@ -24,6 +24,7 @@ data class MangaUpdate(
val updateStrategy: UpdateStrategy? = null,
val initialized: Boolean? = null,
val version: Long? = null,
val notes: String? = null,
)
fun Manga.toMangaUpdate(): MangaUpdate {
@ -49,5 +50,6 @@ fun Manga.toMangaUpdate(): MangaUpdate {
updateStrategy = updateStrategy,
initialized = initialized,
version = version,
notes = notes,
)
}

View File

@ -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-rc06"
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"

View File

@ -147,6 +147,9 @@
<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_all_for_series">Move series to bottom</string>
<string name="action_notes">Note</string>
<string name="action_add_notes">Add Note</string>
<string name="action_edit_notes">Edit Note</string>
<string name="action_install">Install</string>
<string name="action_share">Share</string>
<string name="action_save">Save</string>
@ -925,6 +928,7 @@
<string name="information_cloudflare_bypass_failure">Failed to bypass Cloudflare</string>
<string name="information_cloudflare_help">Tap here for help with Cloudflare</string>
<string name="information_required_plain">*required</string>
<string name="information_no_notes">No notes</string>
<!-- Do not translate "WebView" -->
<string name="information_webview_required">WebView is required for the app to function</string>
<!-- Do not translate "WebView" -->
@ -965,4 +969,7 @@
<string name="exception_http">HTTP %d, check website in WebView</string>
<string name="exception_offline">No Internet connection</string>
<string name="exception_unknown_host">Couldn\'t reach %s</string>
<!-- Notes screen -->
<string name="notes_placeholder">My analysis of the story begins with ontological antirealism, that is to say...</string>
</resources>