This commit is contained in:
Kyle Nguyen 2024-09-19 10:16:28 +00:00 committed by GitHub
commit a8bcd3c6d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 660 additions and 48 deletions

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,147 @@
package eu.kanade.presentation.manga
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.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.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.string.RichTextStringStyle
import 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.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
@Composable
fun MangaNotesScreen(
state: MangaNotesScreenState.Success,
navigateUp: () -> Unit,
beginEditing: () -> Unit,
endEditing: () -> 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,
)
},
floatingActionButton = {
AnimatedVisibility(
true,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier
.imePadding(),
) {
ExtendedFloatingActionButton(
text = {
Text(
text = stringResource(
if (state.editing) MR.strings.action_apply else MR.strings.action_edit,
),
)
},
icon = {
Icon(
imageVector = if (state.editing) Icons.Filled.Check else Icons.Filled.Edit,
contentDescription = null,
)
},
onClick = { if (state.editing) endEditing() else beginEditing() },
expanded = true,
)
}
},
modifier = modifier
.imePadding(),
) { paddingValues ->
AnimatedVisibility(
state.editing,
enter = fadeIn(),
exit = fadeOut(),
) {
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),
),
)
}
AnimatedVisibility(
!state.editing && state.notes.isNullOrBlank(),
enter = fadeIn(),
exit = fadeOut(),
) {
EmptyScreen(
stringRes = MR.strings.information_no_notes,
modifier = Modifier.padding(paddingValues),
)
}
AnimatedVisibility(
!state.editing && !state.notes.isNullOrBlank(),
enter = fadeIn(),
exit = fadeOut(),
) {
SelectionContainer {
RichText(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = paddingValues.calculateTopPadding() + MaterialTheme.padding.medium,
),
style = RichTextStyle(
stringStyle = RichTextStringStyle(
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
),
),
) {
Markdown(content = state.notes.orEmpty())
}
}
}
}
}

View File

@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@ -25,6 +26,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
@ -44,6 +46,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap
@ -112,6 +115,8 @@ fun MangaScreen(
onEditCategoryClicked: (() -> Unit)?,
onEditFetchIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
onNotesClicked: () -> Unit,
onNotesEditClicked: () -> Unit,
// For bottom action menu
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
@ -160,6 +165,8 @@ fun MangaScreen(
onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditFetchIntervalClicked,
onMigrateClicked = onMigrateClicked,
onNotesClicked = onNotesClicked,
onNotesEditClicked = onNotesEditClicked,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
@ -195,6 +202,8 @@ fun MangaScreen(
onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditFetchIntervalClicked,
onMigrateClicked = onMigrateClicked,
onNotesClicked = onNotesClicked,
onNotesEditClicked = onNotesEditClicked,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
@ -240,6 +249,8 @@ private fun MangaScreenSmallImpl(
onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
onNotesClicked: () -> Unit,
onNotesEditClicked: () -> Unit,
// For bottom action menu
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
@ -305,6 +316,7 @@ private fun MangaScreenSmallImpl(
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onClickNotes = onNotesClicked,
actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
@ -326,27 +338,47 @@ private fun MangaScreenSmallImpl(
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
val isFABVisible = remember(chapters) {
chapters.fastAny { !it.chapter.read } && !isAnySelected
}
AnimatedVisibility(
visible = isFABVisible,
enter = fadeIn(),
exit = fadeOut(),
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
ExtendedFloatingActionButton(
text = {
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
)
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.shouldExpandFAB(),
)
AnimatedVisibility(
visible = !isAnySelected,
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = { Text(stringResource(MR.strings.action_notes)) },
icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = null) },
onClick = onNotesEditClicked,
expanded = chapterListState.shouldExpandFAB(),
)
}
val isFABVisible = remember(chapters) {
chapters.fastAny { !it.chapter.read } && !isAnySelected
}
AnimatedVisibility(
visible = isFABVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(
if (isReading) MR.strings.action_resume else MR.strings.action_start,
),
)
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.shouldExpandFAB(),
)
}
}
},
) { contentPadding ->
@ -414,8 +446,10 @@ private fun MangaScreenSmallImpl(
defaultExpandState = state.isFromSource,
description = state.manga.description,
tagsProvider = { state.manga.genre },
noteContent = state.manga.notes,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
onClickNotes = onNotesEditClicked,
)
}
@ -484,6 +518,8 @@ fun MangaScreenLargeImpl(
onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
onNotesClicked: () -> Unit,
onNotesEditClicked: () -> Unit,
// For bottom action menu
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
@ -542,6 +578,7 @@ fun MangaScreenLargeImpl(
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onClickNotes = onNotesClicked,
actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
@ -576,21 +613,39 @@ fun MangaScreenLargeImpl(
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(
if (isReading) MR.strings.action_resume else MR.strings.action_start,
),
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
AnimatedVisibility(
visible = !isAnySelected,
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = { Text(stringResource(MR.strings.action_notes)) },
icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = null) },
onClick = onNotesEditClicked,
expanded = chapterListState.shouldExpandFAB(),
)
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.shouldExpandFAB(),
)
}
ExtendedFloatingActionButton(
text = {
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(
if (isReading) MR.strings.action_resume else MR.strings.action_start,
),
)
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.shouldExpandFAB(),
)
}
}
},
) { contentPadding ->
@ -640,8 +695,10 @@ fun MangaScreenLargeImpl(
defaultExpandState = true,
description = state.manga.description,
tagsProvider = { state.manga.genre },
noteContent = state.manga.notes,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
onClickNotes = onNotesEditClicked,
)
}
},
@ -761,6 +818,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,
)
Column {
MangaNotesSection(
content = noteContent,
expanded = true,
onClickNotes = onNotesEditClicked,
)
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 = expanded,
onClickNotes = onNotesEditClicked,
)
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,114 @@
package eu.kanade.presentation.manga.components
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.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.text.SpanStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.string.RichTextStringStyle
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Button
import tachiyomi.presentation.core.components.material.ButtonDefaults
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun MangaNotesSection(
content: String?,
expanded: Boolean,
onClickNotes: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier.fillMaxWidth()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth(),
) {
if (!content.isNullOrBlank()) {
Column(
modifier = Modifier
.animateContentSize(
animationSpec = spring(),
alignment = Alignment.Center,
),
) {
if (expanded) {
Button(
onClick = onClickNotes,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.primary,
),
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 4.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Edit,
contentDescription = null,
modifier = Modifier
.size(16.dp),
)
Text(
stringResource(MR.strings.action_edit_notes),
)
}
}
}
}
RichText(
modifier = Modifier
.fillMaxWidth(),
style = RichTextStyle(
stringStyle = RichTextStringStyle(
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
),
),
) {
Markdown(content = content)
}
HorizontalDivider(
modifier = Modifier
.padding(vertical = 16.dp),
)
}
}
}
}
@PreviewLightDark
@Composable
private fun MangaNotesSectionPreview() {
MangaNotesSection(
onClickNotes = {},
expanded = true,
content = "# Hello world\ntest1234 hi there!",
)
}

View File

@ -0,0 +1,78 @@
package eu.kanade.presentation.manga.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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 androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreenState
private const val MAX_LENGTH = 10_000
@Composable
fun MangaNotesTextArea(
state: MangaNotesScreenState.Success,
onSave: (String) -> Unit,
modifier: Modifier = Modifier,
) {
var text by remember {
mutableStateOf(TextFieldValue(state.notes.orEmpty(), TextRange(Int.MAX_VALUE)))
}
val focusRequester = remember { FocusRequester() }
Box(
modifier = modifier
.fillMaxSize(),
) {
OutlinedTextField(
value = text,
onValueChange = { if (it.text.length <= MAX_LENGTH) text = it },
modifier = Modifier
.fillMaxSize()
.focusRequester(focusRequester),
supportingText = {
val displayWarning = text.text.length > MAX_LENGTH / 10 * 9
if (!displayWarning) {
Text(
text = "0",
modifier = Modifier.alpha(0f),
)
}
AnimatedVisibility(
displayWarning,
enter = fadeIn(),
exit = fadeOut(),
) {
Text(
text = "${text.text.length} / $MAX_LENGTH",
)
}
},
)
}
LaunchedEffect(focusRequester) {
focusRequester.requestFocus()
}
DisposableEffect(Unit) {
onDispose {
onSave(text.text)
}
}
}

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,8 @@ class MangaScreen(
onMigrateClicked = {
navigator.push(MigrateSearchScreen(successState.manga.id))
}.takeIf { successState.manga.favorite },
onNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) },
onNotesEditClicked = { navigator.push(MangaNotesScreen(manga = successState.manga, editing = true)) },
onMultiBookmarkClicked = screenModel::bookmarkChapters,
onMultiMarkAsReadClicked = screenModel::markChaptersRead,
onMarkPreviousAsReadClicked = screenModel::markPreviousChapterRead,

View File

@ -0,0 +1,57 @@
package eu.kanade.tachiyomi.ui.manga.notes
import androidx.activity.compose.BackHandler
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,
private val editing: Boolean = false,
) : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel {
MangaNotesScreenModel(
manga = manga,
editing = editing,
)
}
val state by screenModel.state.collectAsState()
if (state is MangaNotesScreenState.Loading) {
LoadingScreen()
return
}
val successState = state as MangaNotesScreenState.Success
BackHandler(
onBack = {
if (!successState.editing) {
navigator.pop()
return@BackHandler
}
screenModel.endEditing()
},
)
MangaNotesScreen(
state = successState,
navigateUp = navigator::pop,
beginEditing = { screenModel.beginEditing() },
endEditing = { screenModel.endEditing() },
onSave = { screenModel.saveText(it) },
)
}
}

View File

@ -0,0 +1,81 @@
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,
editing: Boolean,
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,
editing = editing,
)
}
}
fun beginEditing() {
mutableState.update {
when (it) {
MangaNotesScreenState.Loading -> it
is MangaNotesScreenState.Success -> it.copy(editing = true)
}
}
}
fun endEditing() {
mutableState.update {
when (it) {
MangaNotesScreenState.Loading -> it
is MangaNotesScreenState.Success -> it.copy(editing = false)
}
}
}
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?,
val editing: Boolean = false,
) : 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

@ -167,6 +167,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;
@ -166,7 +167,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

@ -146,6 +146,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">Notes</string>
<string name="action_add_notes">Add Notes</string>
<string name="action_edit_notes">Edit Notes</string>
<string name="action_install">Install</string>
<string name="action_share">Share</string>
<string name="action_save">Save</string>
@ -912,6 +915,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">There are no notes here yet!</string>
<!-- Do not translate "WebView" -->
<string name="information_webview_required">WebView is required for the app to function</string>
<!-- Do not translate "WebView" -->