mirror of
https://github.com/mihonapp/mihon.git
synced 2024-12-26 10:58:24 +01:00
Merge 1b39810397
into 4a7fe44e0e
This commit is contained in:
commit
b3e10926b2
@ -254,6 +254,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)
|
||||
|
@ -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()) }
|
||||
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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!",
|
||||
)
|
||||
}
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
@ -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(),
|
||||
)
|
||||
|
@ -99,4 +99,5 @@ private fun Manga.toBackupManga() =
|
||||
lastModifiedAt = this.lastModifiedAt,
|
||||
favoriteModifiedAt = this.favoriteModifiedAt,
|
||||
version = this.version,
|
||||
notes = notes,
|
||||
)
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -129,6 +129,7 @@ class MangaRestorer(
|
||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||
version = manga.version,
|
||||
isSyncing = 1,
|
||||
notes = manga.notes,
|
||||
)
|
||||
}
|
||||
return manga
|
||||
|
@ -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,
|
||||
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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,
|
||||
|
@ -171,6 +171,7 @@ class MangaRepositoryImpl(
|
||||
updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode),
|
||||
version = value.version,
|
||||
isSyncing = 0,
|
||||
notes = value.notes,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
3
data/src/main/sqldelight/tachiyomi/migrations/4.sqm
Normal file
3
data/src/main/sqldelight/tachiyomi/migrations/4.sqm
Normal file
@ -0,0 +1,3 @@
|
||||
-- Add notes column
|
||||
ALTER TABLE mangas
|
||||
ADD notes TEXT;
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
@ -932,6 +935,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" -->
|
||||
@ -972,4 +976,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>
|
||||
|
Loading…
Reference in New Issue
Block a user