diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 08787e1f8..addd07f13 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -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()) } diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt new file mode 100644 index 000000000..4d36d4e0f --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt @@ -0,0 +1,144 @@ +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.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(), + ) { + 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()) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 3115d0457..2d8ac8f49 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -55,6 +55,7 @@ import eu.kanade.presentation.manga.components.MangaActionRow import eu.kanade.presentation.manga.components.MangaBottomActionMenu import eu.kanade.presentation.manga.components.MangaChapterListItem import eu.kanade.presentation.manga.components.MangaInfoBox +import eu.kanade.presentation.manga.components.MangaNotesSection import eu.kanade.presentation.manga.components.MangaToolbar import eu.kanade.presentation.manga.components.MissingChapterCountListItem import eu.kanade.presentation.util.formatChapterNumber @@ -112,6 +113,8 @@ fun MangaScreen( onEditCategoryClicked: (() -> Unit)?, onEditFetchIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, + onNotesClicked: () -> Unit, + onNotesEditClicked: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -160,6 +163,8 @@ fun MangaScreen( onEditCategoryClicked = onEditCategoryClicked, onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, + onNotesClicked = onNotesClicked, + onNotesEditClicked = onNotesEditClicked, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, @@ -195,6 +200,8 @@ fun MangaScreen( onEditCategoryClicked = onEditCategoryClicked, onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, + onNotesClicked = onNotesClicked, + onNotesEditClicked = onNotesEditClicked, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, @@ -240,6 +247,8 @@ private fun MangaScreenSmallImpl( onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, + onNotesClicked: () -> Unit, + onNotesEditClicked: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -305,6 +314,7 @@ private fun MangaScreenSmallImpl( onClickEditCategory = onEditCategoryClicked, onClickRefresh = onRefresh, onClickMigrate = onMigrateClicked, + onClickNotes = onNotesClicked, actionModeCounter = selectedChapterCount, onSelectAll = { onAllChapterSelected(true) }, onInvertSelection = { onInvertSelection() }, @@ -419,6 +429,16 @@ private fun MangaScreenSmallImpl( ) } + item( + key = MangaScreenItem.NOTES_SECTION, + contentType = MangaScreenItem.NOTES_SECTION, + ) { + MangaNotesSection( + onClickNotes = onNotesEditClicked, + content = state.manga.notes, + ) + } + item( key = MangaScreenItem.CHAPTER_HEADER, contentType = MangaScreenItem.CHAPTER_HEADER, @@ -484,6 +504,8 @@ fun MangaScreenLargeImpl( onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, + onNotesClicked: () -> Unit, + onNotesEditClicked: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -542,6 +564,7 @@ fun MangaScreenLargeImpl( onClickEditCategory = onEditCategoryClicked, onClickRefresh = onRefresh, onClickMigrate = onMigrateClicked, + onClickNotes = onNotesClicked, actionModeCounter = selectedChapterCount, onSelectAll = { onAllChapterSelected(true) }, onInvertSelection = { onInvertSelection() }, @@ -643,6 +666,10 @@ fun MangaScreenLargeImpl( onTagSearch = onTagSearch, onCopyTagToClipboard = onCopyTagToClipboard, ) + MangaNotesSection( + onClickNotes = onNotesEditClicked, + content = state.manga.notes, + ) } }, endContent = { diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt index 22a6664d0..785038c78 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt @@ -17,6 +17,7 @@ enum class MangaScreenItem { INFO_BOX, ACTION_ROW, DESCRIPTION_WITH_TAG, + NOTES_SECTION, CHAPTER_HEADER, CHAPTER, } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt new file mode 100644 index 000000000..a20931187 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt @@ -0,0 +1,101 @@ +package eu.kanade.presentation.manga.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.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.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( + onClickNotes: () -> Unit, + content: String?, + modifier: Modifier = Modifier, +) { + Column(modifier.fillMaxWidth()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth(), + ) { + if (!content.isNullOrBlank()) { + RichText( + modifier = Modifier + .fillMaxWidth() + .padding(MaterialTheme.padding.medium), + style = RichTextStyle( + stringStyle = RichTextStringStyle( + linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary), + ), + ), + ) { + Markdown(content = content) + } + } + + 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( + if (content.isNullOrBlank()) { + MR.strings.action_add_notes + } else { + MR.strings.action_edit_notes + }, + ), + ) + } + } + } + } +} + +@PreviewLightDark +@Composable +private fun MangaNotesSectionPreview() { + MangaNotesSection( + onClickNotes = {}, + content = "# Hello world\ntest1234 hi there!", + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt new file mode 100644 index 000000000..7737f08ac --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt @@ -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) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt index 4415bbf27..94b06fd90 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt @@ -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(), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt index 040de931b..c615c18af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt @@ -99,4 +99,5 @@ private fun Manga.toBackupManga() = lastModifiedAt = this.lastModifiedAt, favoriteModifiedAt = this.favoriteModifiedAt, version = this.version, + notes = notes, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt index 3bfc1ffcc..0adc1ed4f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt @@ -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 = 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, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt index 213fc4bf7..9f6d6bcb3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt @@ -129,6 +129,7 @@ class MangaRestorer( updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode), version = manga.version, isSyncing = 1, + notes = manga.notes, ) } return manga diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 9d71a8f51..3106cfdb0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt new file mode 100644 index 000000000..ea7f88db4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt @@ -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) }, + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreenModel.kt new file mode 100644 index 000000000..ea3b9f680 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreenModel.kt @@ -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.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 +} diff --git a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt index 53b45ad09..45c7e5dc9 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt @@ -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, diff --git a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt index f7aaf4c99..0e804dd52 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt @@ -167,6 +167,7 @@ class MangaRepositoryImpl( updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode), version = value.version, isSyncing = 0, + notes = value.notes, ) } } diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq index 3e21e5bc0..c37e9bce8 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -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: diff --git a/data/src/main/sqldelight/tachiyomi/migrations/4.sqm b/data/src/main/sqldelight/tachiyomi/migrations/4.sqm new file mode 100644 index 000000000..82ed296d7 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/4.sqm @@ -0,0 +1,3 @@ +-- Add notes column +ALTER TABLE mangas +ADD notes TEXT; diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/SetMangaNotes.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/SetMangaNotes.kt new file mode 100644 index 000000000..763cc3a27 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/SetMangaNotes.kt @@ -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, + ), + ) + } +} diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt index fb634c1cd..f65537e49 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt @@ -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, ) } } diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt b/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt index 7f1cc2f87..67882d848 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt @@ -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, ) } diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 2d202ccf6..65bd59ffc 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -146,6 +146,9 @@ Move series to top Move to bottom Move series to bottom + Notes + Add Notes + Edit Notes Install Share Save @@ -912,6 +915,7 @@ Failed to bypass Cloudflare Tap here for help with Cloudflare *required + There are no notes here yet! WebView is required for the app to function