Compare commits

...

22 Commits

Author SHA1 Message Date
Kyle Nguyen
c861683cd5
Merge 1b39810397 into f7752a98b2 2024-10-25 00:15:58 +02:00
Cuong-Tran
f7752a98b2
Avoid blocking call to load categories in settings (#1364) 2024-10-24 23:51:47 +06:00
abdurisaq
2ba7ed3280
Fix settings SliderItem steps count (#1356) 2024-10-24 12:59:22 +00:00
Roshan Varughese
c153ac01f5
Rework Auto Track on Mark as Read (#1365) 2024-10-24 12:23:28 +00:00
imkunet
1b39810397
Reduce char limit, switch encoding 2024-09-20 16:28:35 -04:00
imkunet
2159eb37a0
Rename composable from example name 2024-09-20 04:48:32 -04:00
imkunet
6dbc9efa17
Abandon markdown for rich text
Thank you Syer10 for recommending this library
- Already built component for elegant rich text editing
- Adds a dependency, could possibly remove another (NewUpdateScreen.kt's
markdown viewer)
2024-09-20 04:31:06 -04:00
imkunet
7f7177f597
Spotless 2024-09-19 19:03:02 -04:00
imkunet
2c2c5cc5ef
Design revision 2 2024-09-19 19:02:28 -04:00
imkunet
170c000d65
Design revision
see the Discord for more details
2024-09-19 06:16:22 -04:00
imkunet
0e0a9aac5b
Make spotless 2024-09-19 03:12:00 -04:00
imkunet
7a50521ad7
Add migration to add notes to manga 2024-09-19 03:05:45 -04:00
imkunet
5c690cc6c2
Fixes to pass relevant testing 2024-09-19 03:05:02 -04:00
imkunet
05fc1d7b4f
Impose note editor length limit 2024-09-19 03:05:02 -04:00
imkunet
f262f68ea7
Fixed padding 2024-09-19 03:05:02 -04:00
imkunet
44cffcfcf7
Make cursor jump to end by default 2024-09-19 03:05:02 -04:00
imkunet
5a42eb0d5f
Added fade transition between states 2024-09-19 03:05:02 -04:00
imkunet
e861b97bac
Make the edit notes button edit directly 2024-09-19 03:05:01 -04:00
imkunet
23be98b6d4
Added tablet notes 2024-09-19 03:05:01 -04:00
imkunet
610e37510c
Polishing off note editor
- moved note to manga screen
- added backup support
- make notes save
- change the way the initial model is loaded
2024-09-19 03:05:01 -04:00
imkunet
309086920c
Add the text editor
pending work is to save the notes and make it work with the backup
2024-09-19 02:50:43 -04:00
imkunet
28c23d184a
Initial markdown render test 2024-09-19 02:50:42 -04:00
30 changed files with 630 additions and 22 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

@ -35,4 +35,6 @@ class TrackPreferences(
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
fun autoUpdateTrackOnMarkRead() = preferenceStore.getBoolean("pref_auto_update_manga_on_mark_read", true)
}

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

@ -15,7 +15,6 @@ import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences
@ -35,7 +34,7 @@ object SettingsDownloadScreen : SearchableSettings {
@Composable
override fun getPreferences(): List<Preference> {
val getCategories = remember { Injekt.get<GetCategories>() }
val allCategories by getCategories.subscribe().collectAsState(initial = runBlocking { getCategories.await() })
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
return listOf(

View File

@ -24,7 +24,6 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.ResetCategoryFlags
import tachiyomi.domain.category.model.Category
@ -53,7 +52,7 @@ object SettingsLibraryScreen : SearchableSettings {
override fun getPreferences(): List<Preference> {
val getCategories = remember { Injekt.get<GetCategories>() }
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
val allCategories by getCategories.subscribe().collectAsState(initial = runBlocking { getCategories.await() })
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
return listOf(
getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences),

View File

@ -125,6 +125,10 @@ object SettingsTrackingScreen : SearchableSettings {
pref = trackPreferences.autoUpdateTrack(),
title = stringResource(MR.strings.pref_auto_update_manga_sync),
),
Preference.PreferenceItem.SwitchPreference(
pref = trackPreferences.autoUpdateTrackOnMarkRead(),
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
),
Preference.PreferenceGroup(
title = stringResource(MR.strings.services),
preferenceItems = persistentListOf(

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

@ -25,6 +25,7 @@ import eu.kanade.domain.manga.model.downloadedFilter
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.track.interactor.TrackChapter
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.util.formattedMessage
@ -38,6 +39,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.chapter.getNextUnread
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.async
@ -92,6 +94,7 @@ class MangaScreenModel(
private val mangaId: Long,
private val isFromSource: Boolean,
private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val trackPreferences: TrackPreferences = Injekt.get(),
readerPreferences: ReaderPreferences = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(),
private val trackChapter: TrackChapter = Injekt.get(),
@ -739,6 +742,14 @@ class MangaScreenModel(
if (!shouldPromptTrackingUpdate) return@launchIO
if (trackPreferences.autoUpdateTrackOnMarkRead().get()) {
trackChapter.await(context, mangaId, maxChapterNumber)
withUIContext {
context.toast(context.stringResource(MR.strings.trackers_updated_summary, maxChapterNumber.toInt()))
}
return@launchIO
}
val result = snackbarHostState.showSnackbar(
message = context.stringResource(MR.strings.confirm_tracker_update, maxChapterNumber.toInt()),
actionLabel = context.stringResource(MR.strings.action_ok),

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>
@ -509,6 +512,7 @@
<!-- Tracking section -->
<string name="tracking_guide">Tracking guide</string>
<string name="pref_auto_update_manga_sync">Update progress after reading</string>
<string name="pref_auto_update_manga_on_mark_read">Update progress when marked as read</string>
<string name="services">Trackers</string>
<string name="tracking_info">One-way sync to update the chapter progress in external tracker services. Set up tracking for individual entries from their tracking button.</string>
<string name="enhanced_services">Enhanced trackers</string>
@ -739,6 +743,7 @@
<string name="exclude_scanlators">Exclude scanlators</string>
<string name="no_scanlators_found">No scanlators found</string>
<string name="confirm_tracker_update">Update trackers to chapter %d?</string>
<string name="trackers_updated_summary">Trackers updated to chapter %d</string>
<!-- Tracking Screen -->
<string name="manga_tracking_tab">Tracking</string>
@ -925,6 +930,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 +971,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>

View File

@ -202,7 +202,7 @@ fun SliderItem(
},
modifier = Modifier.weight(1.5f),
valueRange = min.toFloat()..max.toFloat(),
steps = max - min,
steps = max - min - 1,
)
}
}