From 28c23d184a1bf0a0c84b5ef1da1776b1b09b78cf Mon Sep 17 00:00:00 2001 From: imkunet Date: Sun, 18 Feb 2024 06:19:25 -0500 Subject: [PATCH 01/18] Initial markdown render test --- .../java/eu/kanade/domain/DomainModule.kt | 2 + .../presentation/manga/MangaNotesScreen.kt | 89 +++++++++++++++++++ .../kanade/presentation/manga/MangaScreen.kt | 7 ++ .../manga/components/MangaToolbar.kt | 8 ++ .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 2 + .../ui/manga/notes/MangaNotesScreen.kt | 42 +++++++++ .../ui/manga/notes/MangaNotesScreenModel.kt | 75 ++++++++++++++++ .../java/tachiyomi/data/manga/MangaMapper.kt | 4 + .../main/sqldelight/tachiyomi/data/mangas.sq | 3 +- .../domain/manga/interactor/SetMangaNotes.kt | 19 ++++ .../tachiyomi/domain/manga/model/Manga.kt | 2 + .../domain/manga/model/MangaUpdate.kt | 2 + .../moko-resources/base/strings.xml | 2 + 13 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreenModel.kt create mode 100644 domain/src/main/java/tachiyomi/domain/manga/interactor/SetMangaNotes.kt 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..492a275f9 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt @@ -0,0 +1,89 @@ +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.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, +) { + Scaffold( + topBar = { scrollBehavior -> + AppBar( + titleContent = { + AppBarTitle(title = stringResource(MR.strings.action_notes), subtitle = state.title) + }, + navigateUp = navigateUp, + scrollBehavior = scrollBehavior, + ) + }, + floatingActionButton = { + AnimatedVisibility( + true, + enter = fadeIn(), + exit = fadeOut(), + ) { + 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, + ) + } + }, + ) { paddingValues -> + if (state.isEmpty || state.content == null) { + EmptyScreen( + stringRes = MR.strings.information_no_notes, + modifier = Modifier.padding(paddingValues), + ) + return@Scaffold + } + + RichText( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = MaterialTheme.padding.small, + vertical = paddingValues.calculateTopPadding() + MaterialTheme.padding.small, + ), + style = RichTextStyle( + stringStyle = RichTextStringStyle( + linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary), + ), + ), + ) { + Markdown(content = state.content) + } + } +} 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..99d086526 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -112,6 +112,7 @@ fun MangaScreen( onEditCategoryClicked: (() -> Unit)?, onEditFetchIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, + onNotesClicked: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -160,6 +161,7 @@ fun MangaScreen( onEditCategoryClicked = onEditCategoryClicked, onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, + onClickNotes = onNotesClicked, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, @@ -195,6 +197,7 @@ fun MangaScreen( onEditCategoryClicked = onEditCategoryClicked, onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, + onClickNotes = onNotesClicked, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, @@ -240,6 +243,7 @@ private fun MangaScreenSmallImpl( onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, + onClickNotes: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -305,6 +309,7 @@ private fun MangaScreenSmallImpl( onClickEditCategory = onEditCategoryClicked, onClickRefresh = onRefresh, onClickMigrate = onMigrateClicked, + onClickNotes = onClickNotes, actionModeCounter = selectedChapterCount, onSelectAll = { onAllChapterSelected(true) }, onInvertSelection = { onInvertSelection() }, @@ -484,6 +489,7 @@ fun MangaScreenLargeImpl( onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, + onClickNotes: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -542,6 +548,7 @@ fun MangaScreenLargeImpl( onClickEditCategory = onEditCategoryClicked, onClickRefresh = onRefresh, onClickMigrate = onMigrateClicked, + onClickNotes = onClickNotes, actionModeCounter = selectedChapterCount, onSelectAll = { onAllChapterSelected(true) }, onInvertSelection = { onInvertSelection() }, 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..919e15957 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/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 9d71a8f51..6a6081967 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,7 @@ class MangaScreen( onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite }, + onNotesClicked = { navigator.push(MangaNotesScreen(successState.manga.id)) }, 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..9896b8239 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.ui.manga.notes + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +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.presentation.core.screens.LoadingScreen + +class MangaNotesScreen(private val mangaId: Long) : Screen() { + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + + val screenModel = rememberScreenModel { MangaNotesScreenModel(mangaId = mangaId) } + val state by screenModel.state.collectAsState() + + DisposableEffect(Unit) { + onDispose { + + } + } + + if (state is MangaNotesScreenState.Loading) { + LoadingScreen() + return + } + + val successState = state as MangaNotesScreenState.Success + + MangaNotesScreen( + state = successState, + navigateUp = navigator::pop, + beginEditing = { screenModel.beginEditing() }, + endEditing = { screenModel.endEditing() }, + ) + } +} 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..a9e1b8e74 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/notes/MangaNotesScreenModel.kt @@ -0,0 +1,75 @@ +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 eu.kanade.tachiyomi.ui.category.CategoryEvent +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import tachiyomi.domain.manga.interactor.GetManga +import tachiyomi.domain.manga.interactor.SetMangaNotes +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MangaNotesScreenModel( + val mangaId: Long, + getManga: GetManga = Injekt.get(), + setMangaNotes: SetMangaNotes = Injekt.get(), +) : StateScreenModel(MangaNotesScreenState.Loading) { + + private val _events: Channel = Channel() + val events = _events.receiveAsFlow() + + init { + screenModelScope.launch { + getManga.subscribe(mangaId).collectLatest { manga -> + mutableState.update { + MangaNotesScreenState.Success( + content = manga.notes, + title = manga.title, + ) + } + } + } + } + + 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) + } + } + } +} + +sealed interface MangaNotesScreenState { + + @Immutable + data object Loading : MangaNotesScreenState + + @Immutable + data class Success( + val content: String?, + val title: String, + + val editing: Boolean = false, + ) : MangaNotesScreenState { + + val isEmpty: Boolean + get() = content.isNullOrEmpty() + } +} + 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/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq index 3e21e5bc0..525bd71c2 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; 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..a0b80a5fd 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -146,6 +146,7 @@ Move series to top Move to bottom Move series to bottom + Notes Install Share Save @@ -912,6 +913,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 From 309086920c9856a3040a4d26be9e44f66f161103 Mon Sep 17 00:00:00 2001 From: imkunet Date: Sun, 18 Feb 2024 18:07:40 -0500 Subject: [PATCH 02/18] Add the text editor pending work is to save the notes and make it work with the backup --- .../presentation/manga/MangaNotesScreen.kt | 33 +++++++++++- .../manga/components/MangaNotesTextArea.kt | 53 +++++++++++++++++++ .../ui/manga/notes/MangaNotesScreen.kt | 13 +++++ .../ui/manga/notes/MangaNotesScreenModel.kt | 18 ++++--- 4 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt index 492a275f9..84dd8bbbd 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt @@ -4,7 +4,10 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +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 @@ -20,6 +23,7 @@ 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 @@ -34,6 +38,7 @@ fun MangaNotesScreen( navigateUp: () -> Unit, beginEditing: () -> Unit, endEditing: () -> Unit, + onSave: (String) -> Unit, ) { Scaffold( topBar = { scrollBehavior -> @@ -50,19 +55,40 @@ fun MangaNotesScreen( 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) }, + icon = { + Icon( + imageVector = if (state.editing) Icons.Filled.Check else Icons.Filled.Edit, + contentDescription = null, + ) + }, onClick = { if (state.editing) endEditing() else beginEditing() }, expanded = true, ) } }, ) { paddingValues -> - if (state.isEmpty || state.content == null) { + if (state.editing) { + MangaNotesTextArea( + modifier = Modifier + .padding( + top = paddingValues.calculateTopPadding() + MaterialTheme.padding.small, + bottom = MaterialTheme.padding.large, + ) + .padding(horizontal = MaterialTheme.padding.small), + state = state, + onSave = onSave, + ) + + return@Scaffold + } + if (state.content == null) { EmptyScreen( stringRes = MR.strings.information_no_notes, modifier = Modifier.padding(paddingValues), @@ -72,6 +98,7 @@ fun MangaNotesScreen( RichText( modifier = Modifier + .verticalScroll(rememberScrollState()) .fillMaxWidth() .padding( horizontal = MaterialTheme.padding.small, @@ -85,5 +112,7 @@ fun MangaNotesScreen( ) { Markdown(content = state.content) } + + return@Scaffold } } 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..c7f71144c --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt @@ -0,0 +1,53 @@ +package eu.kanade.presentation.manga.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.material3.OutlinedTextField +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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreenState + +@Composable +fun MangaNotesTextArea( + modifier: Modifier = Modifier, + state: MangaNotesScreenState.Success, + onSave: (String) -> Unit, +) { + var text by remember { mutableStateOf(state.content.orEmpty()) } + val focusRequester = remember { FocusRequester() } + + Column( + modifier = modifier + .fillMaxSize() + .imePadding(), + ) { + OutlinedTextField( + value = text, + onValueChange = { text = it }, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .focusRequester(focusRequester), + ) + } + + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + } + + DisposableEffect(Unit) { + onDispose { + onSave(text) + } + } +} 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 index 9896b8239..ddcb273f3 100644 --- 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 @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.ui.manga.notes +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState @@ -32,11 +33,23 @@ class MangaNotesScreen(private val mangaId: Long) : Screen() { 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 index a9e1b8e74..5e19c6665 100644 --- 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 @@ -53,6 +53,17 @@ class MangaNotesScreenModel( } } } + + fun saveText(content: String) { + mutableState.update { + when (it) { + MangaNotesScreenState.Loading -> it + is MangaNotesScreenState.Success -> it.copy(content = content) + } + } + + // do the magic to set it backend + } } sealed interface MangaNotesScreenState { @@ -66,10 +77,5 @@ sealed interface MangaNotesScreenState { val title: String, val editing: Boolean = false, - ) : MangaNotesScreenState { - - val isEmpty: Boolean - get() = content.isNullOrEmpty() - } + ) : MangaNotesScreenState } - From 610e37510c43a9b875e536f0a7e25fe82f8a505d Mon Sep 17 00:00:00 2001 From: imkunet Date: Mon, 19 Feb 2024 03:30:36 -0500 Subject: [PATCH 03/18] Polishing off note editor - moved note to manga screen - added backup support - make notes save - change the way the initial model is loaded --- .../presentation/manga/MangaNotesScreen.kt | 36 +++++-- .../kanade/presentation/manga/MangaScreen.kt | 11 ++ .../manga/MangaScreenConstants.kt | 1 + .../manga/components/MangaNotesSection.kt | 101 ++++++++++++++++++ .../manga/components/MangaNotesTextArea.kt | 4 +- .../create/creators/MangaBackupCreator.kt | 1 + .../data/backup/models/BackupManga.kt | 3 + .../backup/restore/restorers/MangaRestorer.kt | 1 + .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 2 +- .../ui/manga/notes/MangaNotesScreen.kt | 12 +-- .../ui/manga/notes/MangaNotesScreenModel.kt | 46 ++++---- .../data/manga/MangaRepositoryImpl.kt | 1 + .../main/sqldelight/tachiyomi/data/mangas.sq | 3 +- .../moko-resources/base/strings.xml | 2 + 14 files changed, 177 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt index 84dd8bbbd..b2968d8a4 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt @@ -3,9 +3,14 @@ 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 @@ -39,12 +44,13 @@ fun MangaNotesScreen( beginEditing: () -> Unit, endEditing: () -> Unit, onSave: (String) -> Unit, + modifier: Modifier = Modifier, ) { Scaffold( topBar = { scrollBehavior -> AppBar( titleContent = { - AppBarTitle(title = stringResource(MR.strings.action_notes), subtitle = state.title) + AppBarTitle(title = stringResource(MR.strings.action_notes), subtitle = state.manga.title) }, navigateUp = navigateUp, scrollBehavior = scrollBehavior, @@ -60,7 +66,11 @@ fun MangaNotesScreen( ) { ExtendedFloatingActionButton( text = { - Text(text = stringResource(if (state.editing) MR.strings.action_apply else MR.strings.action_edit)) + Text( + text = stringResource( + if (state.editing) MR.strings.action_apply else MR.strings.action_edit + ), + ) }, icon = { Icon( @@ -73,22 +83,28 @@ fun MangaNotesScreen( ) } }, + modifier = modifier, ) { paddingValues -> if (state.editing) { MangaNotesTextArea( + state = state, + onSave = onSave, modifier = Modifier .padding( top = paddingValues.calculateTopPadding() + MaterialTheme.padding.small, - bottom = MaterialTheme.padding.large, + bottom = MaterialTheme.padding.small, ) - .padding(horizontal = MaterialTheme.padding.small), - state = state, - onSave = onSave, + .padding(horizontal = MaterialTheme.padding.small) + .windowInsetsPadding( + WindowInsets.navigationBars + .only(WindowInsetsSides.Bottom), + ), ) return@Scaffold } - if (state.content == null) { + + if (state.notes.isNullOrBlank()) { EmptyScreen( stringRes = MR.strings.information_no_notes, modifier = Modifier.padding(paddingValues), @@ -101,8 +117,8 @@ fun MangaNotesScreen( .verticalScroll(rememberScrollState()) .fillMaxWidth() .padding( - horizontal = MaterialTheme.padding.small, - vertical = paddingValues.calculateTopPadding() + MaterialTheme.padding.small, + horizontal = MaterialTheme.padding.medium, + vertical = paddingValues.calculateTopPadding() + MaterialTheme.padding.medium, ), style = RichTextStyle( stringStyle = RichTextStringStyle( @@ -110,7 +126,7 @@ fun MangaNotesScreen( ), ), ) { - Markdown(content = state.content) + Markdown(content = state.notes) } return@Scaffold 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 99d086526..d80146081 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 @@ -424,6 +425,16 @@ private fun MangaScreenSmallImpl( ) } + item( + key = MangaScreenItem.NOTES_SECTION, + contentType = MangaScreenItem.NOTES_SECTION, + ) { + MangaNotesSection( + onClickNotes = onClickNotes, + content = state.manga.notes, + ) + } + item( key = MangaScreenItem.CHAPTER_HEADER, contentType = MangaScreenItem.CHAPTER_HEADER, 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 index c7f71144c..03eb3732f 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt @@ -19,11 +19,11 @@ import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreenState @Composable fun MangaNotesTextArea( - modifier: Modifier = Modifier, state: MangaNotesScreenState.Success, onSave: (String) -> Unit, + modifier: Modifier = Modifier, ) { - var text by remember { mutableStateOf(state.content.orEmpty()) } + var text by remember { mutableStateOf(state.notes.orEmpty()) } val focusRequester = remember { FocusRequester() } Column( 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 6a6081967..84e573666 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 @@ -165,7 +165,7 @@ class MangaScreen( onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite }, - onNotesClicked = { navigator.push(MangaNotesScreen(successState.manga.id)) }, + onNotesClicked = { navigator.push(MangaNotesScreen(successState.manga)) }, 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 index ddcb273f3..51a48a77e 100644 --- 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 @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.manga.notes import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import cafe.adriel.voyager.core.model.rememberScreenModel @@ -10,22 +9,17 @@ 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 mangaId: Long) : Screen() { +class MangaNotesScreen(private val manga: Manga) : Screen() { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { MangaNotesScreenModel(mangaId = mangaId) } + val screenModel = rememberScreenModel { MangaNotesScreenModel(manga = manga) } val state by screenModel.state.collectAsState() - DisposableEffect(Unit) { - onDispose { - - } - } - if (state is MangaNotesScreenState.Loading) { LoadingScreen() return 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 index 5e19c6665..2f5b6f6bc 100644 --- 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 @@ -3,36 +3,27 @@ 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 eu.kanade.tachiyomi.ui.category.CategoryEvent -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import tachiyomi.domain.manga.interactor.GetManga +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 mangaId: Long, - getManga: GetManga = Injekt.get(), - setMangaNotes: SetMangaNotes = Injekt.get(), + val manga: Manga, + private val setMangaNotes: SetMangaNotes = Injekt.get(), ) : StateScreenModel(MangaNotesScreenState.Loading) { - private val _events: Channel = Channel() - val events = _events.receiveAsFlow() + private val successState: MangaNotesScreenState.Success? + get() = state.value as? MangaNotesScreenState.Success init { - screenModelScope.launch { - getManga.subscribe(mangaId).collectLatest { manga -> - mutableState.update { - MangaNotesScreenState.Success( - content = manga.notes, - title = manga.title, - ) - } - } + mutableState.update { + MangaNotesScreenState.Success( + manga = manga, + notes = manga.notes, + ) } } @@ -55,14 +46,21 @@ class MangaNotesScreenModel( } 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(content = content) + is MangaNotesScreenState.Success -> { + it.copy(notes = content) + } } } - // do the magic to set it backend + screenModelScope.launchNonCancellable { + setMangaNotes.awaitSetNotes(manga, content) + } } } @@ -73,8 +71,8 @@ sealed interface MangaNotesScreenState { @Immutable data class Success( - val content: String?, - val title: String, + val manga: Manga, + val notes: String?, val editing: Boolean = false, ) : MangaNotesScreenState diff --git a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt index f7aaf4c99..98cd877ae 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 525bd71c2..c37e9bce8 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -167,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/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index a0b80a5fd..65bd59ffc 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -147,6 +147,8 @@ Move to bottom Move series to bottom Notes + Add Notes + Edit Notes Install Share Save From 23be98b6d4a406f95633c268362baa8e2ce1dddf Mon Sep 17 00:00:00 2001 From: imkunet Date: Mon, 19 Feb 2024 04:25:09 -0500 Subject: [PATCH 04/18] Added tablet notes --- .../kanade/presentation/manga/MangaScreen.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) 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 d80146081..d1707788f 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -162,7 +162,7 @@ fun MangaScreen( onEditCategoryClicked = onEditCategoryClicked, onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, - onClickNotes = onNotesClicked, + onNotesClicked = onNotesClicked, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, @@ -198,7 +198,7 @@ fun MangaScreen( onEditCategoryClicked = onEditCategoryClicked, onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, - onClickNotes = onNotesClicked, + onNotesClicked = onNotesClicked, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, @@ -244,7 +244,7 @@ private fun MangaScreenSmallImpl( onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, - onClickNotes: () -> Unit, + onNotesClicked: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -310,7 +310,7 @@ private fun MangaScreenSmallImpl( onClickEditCategory = onEditCategoryClicked, onClickRefresh = onRefresh, onClickMigrate = onMigrateClicked, - onClickNotes = onClickNotes, + onClickNotes = onNotesClicked, actionModeCounter = selectedChapterCount, onSelectAll = { onAllChapterSelected(true) }, onInvertSelection = { onInvertSelection() }, @@ -430,7 +430,7 @@ private fun MangaScreenSmallImpl( contentType = MangaScreenItem.NOTES_SECTION, ) { MangaNotesSection( - onClickNotes = onClickNotes, + onClickNotes = onNotesClicked, content = state.manga.notes, ) } @@ -500,7 +500,7 @@ fun MangaScreenLargeImpl( onEditCategoryClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, - onClickNotes: () -> Unit, + onNotesClicked: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -559,7 +559,7 @@ fun MangaScreenLargeImpl( onClickEditCategory = onEditCategoryClicked, onClickRefresh = onRefresh, onClickMigrate = onMigrateClicked, - onClickNotes = onClickNotes, + onClickNotes = onNotesClicked, actionModeCounter = selectedChapterCount, onSelectAll = { onAllChapterSelected(true) }, onInvertSelection = { onInvertSelection() }, @@ -661,6 +661,10 @@ fun MangaScreenLargeImpl( onTagSearch = onTagSearch, onCopyTagToClipboard = onCopyTagToClipboard, ) + MangaNotesSection( + onClickNotes = onNotesClicked, + content = state.manga.notes, + ) } }, endContent = { From e861b97bac11aee545de8b67aa78b2c881ec8b85 Mon Sep 17 00:00:00 2001 From: imkunet Date: Mon, 19 Feb 2024 06:04:07 -0500 Subject: [PATCH 05/18] Make the edit notes button edit directly --- .../java/eu/kanade/presentation/manga/MangaScreen.kt | 9 +++++++-- .../java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt | 3 ++- .../tachiyomi/ui/manga/notes/MangaNotesScreen.kt | 12 ++++++++++-- .../ui/manga/notes/MangaNotesScreenModel.kt | 2 ++ 4 files changed, 21 insertions(+), 5 deletions(-) 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 d1707788f..2d8ac8f49 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -114,6 +114,7 @@ fun MangaScreen( onEditFetchIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, onNotesClicked: () -> Unit, + onNotesEditClicked: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -163,6 +164,7 @@ fun MangaScreen( onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, onNotesClicked = onNotesClicked, + onNotesEditClicked = onNotesEditClicked, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, @@ -199,6 +201,7 @@ fun MangaScreen( onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, onNotesClicked = onNotesClicked, + onNotesEditClicked = onNotesEditClicked, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, @@ -245,6 +248,7 @@ private fun MangaScreenSmallImpl( onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, onNotesClicked: () -> Unit, + onNotesEditClicked: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -430,7 +434,7 @@ private fun MangaScreenSmallImpl( contentType = MangaScreenItem.NOTES_SECTION, ) { MangaNotesSection( - onClickNotes = onNotesClicked, + onClickNotes = onNotesEditClicked, content = state.manga.notes, ) } @@ -501,6 +505,7 @@ fun MangaScreenLargeImpl( onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, onNotesClicked: () -> Unit, + onNotesEditClicked: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -662,7 +667,7 @@ fun MangaScreenLargeImpl( onCopyTagToClipboard = onCopyTagToClipboard, ) MangaNotesSection( - onClickNotes = onNotesClicked, + onClickNotes = onNotesEditClicked, content = state.manga.notes, ) } 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 84e573666..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 @@ -165,7 +165,8 @@ class MangaScreen( onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite }, - onNotesClicked = { navigator.push(MangaNotesScreen(successState.manga)) }, + 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 index 51a48a77e..1c3fce925 100644 --- 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 @@ -12,12 +12,20 @@ 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() { +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) } + val screenModel = rememberScreenModel { + MangaNotesScreenModel( + manga = manga, + editing = editing, + ) + } val state by screenModel.state.collectAsState() if (state is MangaNotesScreenState.Loading) { 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 index 2f5b6f6bc..ea3b9f680 100644 --- 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 @@ -12,6 +12,7 @@ import uy.kohesive.injekt.api.get class MangaNotesScreenModel( val manga: Manga, + editing: Boolean, private val setMangaNotes: SetMangaNotes = Injekt.get(), ) : StateScreenModel(MangaNotesScreenState.Loading) { @@ -23,6 +24,7 @@ class MangaNotesScreenModel( MangaNotesScreenState.Success( manga = manga, notes = manga.notes, + editing = editing, ) } } From 5a42eb0d5f874b3ab68eedcda1399fc0e1822344 Mon Sep 17 00:00:00 2001 From: imkunet Date: Mon, 19 Feb 2024 15:22:29 -0500 Subject: [PATCH 06/18] Added fade transition between states --- .../presentation/manga/MangaNotesScreen.kt | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt index b2968d8a4..317354224 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt @@ -68,7 +68,7 @@ fun MangaNotesScreen( text = { Text( text = stringResource( - if (state.editing) MR.strings.action_apply else MR.strings.action_edit + if (state.editing) MR.strings.action_apply else MR.strings.action_edit, ), ) }, @@ -85,7 +85,11 @@ fun MangaNotesScreen( }, modifier = modifier, ) { paddingValues -> - if (state.editing) { + AnimatedVisibility( + state.editing, + enter = fadeIn(), + exit = fadeOut(), + ) { MangaNotesTextArea( state = state, onSave = onSave, @@ -100,35 +104,40 @@ fun MangaNotesScreen( .only(WindowInsetsSides.Bottom), ), ) - - return@Scaffold } - if (state.notes.isNullOrBlank()) { + AnimatedVisibility( + !state.editing && state.notes.isNullOrBlank(), + enter = fadeIn(), + exit = fadeOut(), + ) { EmptyScreen( stringRes = MR.strings.information_no_notes, modifier = Modifier.padding(paddingValues), ) - return@Scaffold } - 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), - ), - ), + AnimatedVisibility( + !state.editing && !state.notes.isNullOrBlank(), + enter = fadeIn(), + exit = fadeOut(), ) { - Markdown(content = state.notes) + 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()) + } } - - return@Scaffold } } From 44cffcfcf7d61cd3747f87abee487dfdb57db866 Mon Sep 17 00:00:00 2001 From: imkunet Date: Mon, 19 Feb 2024 15:22:43 -0500 Subject: [PATCH 07/18] Make cursor jump to end by default --- .../presentation/manga/components/MangaNotesTextArea.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 index 03eb3732f..f90ae1e0c 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt @@ -15,6 +15,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier 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 @Composable @@ -23,7 +25,9 @@ fun MangaNotesTextArea( onSave: (String) -> Unit, modifier: Modifier = Modifier, ) { - var text by remember { mutableStateOf(state.notes.orEmpty()) } + var text by remember { + mutableStateOf(TextFieldValue(state.notes.orEmpty(), TextRange(Int.MAX_VALUE))) + } val focusRequester = remember { FocusRequester() } Column( @@ -47,7 +51,7 @@ fun MangaNotesTextArea( DisposableEffect(Unit) { onDispose { - onSave(text) + onSave(text.text) } } } From f262f68ea70b92fd3b2ac6512ba3fc3191293403 Mon Sep 17 00:00:00 2001 From: imkunet Date: Mon, 19 Feb 2024 18:26:06 -0500 Subject: [PATCH 08/18] Fixed padding --- .../eu/kanade/presentation/manga/MangaNotesScreen.kt | 4 +++- .../manga/components/MangaNotesTextArea.kt | 12 ++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt index 317354224..b97ab3ba6 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt @@ -5,6 +5,7 @@ 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.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBars @@ -83,7 +84,8 @@ fun MangaNotesScreen( ) } }, - modifier = modifier, + modifier = modifier + .imePadding(), ) { paddingValues -> AnimatedVisibility( state.editing, 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 index f90ae1e0c..86245f2fa 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt @@ -1,9 +1,7 @@ package eu.kanade.presentation.manga.components -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding import androidx.compose.material3.OutlinedTextField import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -30,17 +28,15 @@ fun MangaNotesTextArea( } val focusRequester = remember { FocusRequester() } - Column( + Box( modifier = modifier - .fillMaxSize() - .imePadding(), + .fillMaxSize(), ) { OutlinedTextField( value = text, onValueChange = { text = it }, modifier = Modifier - .fillMaxWidth() - .weight(1f) + .fillMaxSize() .focusRequester(focusRequester), ) } From 05fc1d7b4f31cd7a0413f102d87c3a9345501ca9 Mon Sep 17 00:00:00 2001 From: imkunet Date: Tue, 20 Feb 2024 02:43:16 -0500 Subject: [PATCH 09/18] Impose note editor length limit --- .../manga/components/MangaNotesTextArea.kt | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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 index 86245f2fa..7737f08ac 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt @@ -1,8 +1,12 @@ 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 @@ -11,12 +15,15 @@ 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, @@ -34,10 +41,28 @@ fun MangaNotesTextArea( ) { OutlinedTextField( value = text, - onValueChange = { text = it }, + 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", + ) + } + }, ) } From 5c690cc6c271464b84e03c7b921bbd2315d64823 Mon Sep 17 00:00:00 2001 From: imkunet Date: Tue, 5 Mar 2024 04:12:05 -0500 Subject: [PATCH 10/18] Fixes to pass relevant testing --- .../java/eu/kanade/presentation/manga/MangaNotesScreen.kt | 1 - .../presentation/manga/components/MangaNotesTextArea.kt | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt index b97ab3ba6..4d36d4e0f 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt @@ -5,7 +5,6 @@ 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.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBars 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 index 7737f08ac..43bb1b8cf 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt @@ -22,7 +22,7 @@ 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 +private const val MaxLength = 10_000 @Composable fun MangaNotesTextArea( @@ -41,12 +41,12 @@ fun MangaNotesTextArea( ) { OutlinedTextField( value = text, - onValueChange = { if (it.text.length <= MAX_LENGTH) text = it }, + onValueChange = { if (it.text.length <= MaxLength) text = it }, modifier = Modifier .fillMaxSize() .focusRequester(focusRequester), supportingText = { - val displayWarning = text.text.length > MAX_LENGTH / 10 * 9 + val displayWarning = text.text.length > MaxLength / 10 * 9 if (!displayWarning) { Text( text = "0", @@ -59,7 +59,7 @@ fun MangaNotesTextArea( exit = fadeOut(), ) { Text( - text = "${text.text.length} / $MAX_LENGTH", + text = "${text.text.length} / $MaxLength", ) } }, From 7a50521ad7b869c3189b6989e73256081da204ef Mon Sep 17 00:00:00 2001 From: imkunet Date: Thu, 19 Sep 2024 03:05:45 -0400 Subject: [PATCH 11/18] Add migration to add notes to manga --- data/src/main/sqldelight/tachiyomi/migrations/4.sqm | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 data/src/main/sqldelight/tachiyomi/migrations/4.sqm 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; From 0e0a9aac5b54ca2d9bcd09d997d7bba5bfcdbc3d Mon Sep 17 00:00:00 2001 From: imkunet Date: Thu, 19 Sep 2024 03:12:00 -0400 Subject: [PATCH 12/18] Make spotless --- .../presentation/manga/components/MangaNotesTextArea.kt | 8 ++++---- .../kanade/presentation/manga/components/MangaToolbar.kt | 2 +- .../kanade/tachiyomi/ui/manga/notes/MangaNotesScreen.kt | 2 +- .../main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) 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 index 43bb1b8cf..7737f08ac 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreenState -private const val MaxLength = 10_000 +private const val MAX_LENGTH = 10_000 @Composable fun MangaNotesTextArea( @@ -41,12 +41,12 @@ fun MangaNotesTextArea( ) { OutlinedTextField( value = text, - onValueChange = { if (it.text.length <= MaxLength) text = it }, + onValueChange = { if (it.text.length <= MAX_LENGTH) text = it }, modifier = Modifier .fillMaxSize() .focusRequester(focusRequester), supportingText = { - val displayWarning = text.text.length > MaxLength / 10 * 9 + val displayWarning = text.text.length > MAX_LENGTH / 10 * 9 if (!displayWarning) { Text( text = "0", @@ -59,7 +59,7 @@ fun MangaNotesTextArea( exit = fadeOut(), ) { Text( - text = "${text.text.length} / $MaxLength", + text = "${text.text.length} / $MAX_LENGTH", ) } }, 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 919e15957..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 @@ -155,7 +155,7 @@ fun MangaToolbar( AppBar.OverflowAction( title = stringResource(MR.strings.action_notes), onClick = onClickNotes, - ) + ), ) } .build(), 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 index 1c3fce925..ea7f88db4 100644 --- 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 @@ -51,7 +51,7 @@ class MangaNotesScreen( navigateUp = navigator::pop, beginEditing = { screenModel.beginEditing() }, endEditing = { screenModel.endEditing() }, - onSave = { screenModel.saveText(it) } + onSave = { screenModel.saveText(it) }, ) } } diff --git a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt index 98cd877ae..0e804dd52 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt @@ -167,7 +167,7 @@ class MangaRepositoryImpl( updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode), version = value.version, isSyncing = 0, - notes = value.notes + notes = value.notes, ) } } From 170c000d65f05f743e2c883f4f720471eea44272 Mon Sep 17 00:00:00 2001 From: imkunet Date: Thu, 19 Sep 2024 06:16:22 -0400 Subject: [PATCH 13/18] Design revision see the Discord for more details --- .../presentation/manga/MangaNotesScreen.kt | 31 +++-- .../kanade/presentation/manga/MangaScreen.kt | 125 +++++++++++------- .../manga/MangaScreenConstants.kt | 1 - .../manga/components/MangaInfoHeader.kt | 46 +++++-- .../manga/components/MangaNotesSection.kt | 81 +++++++----- 5 files changed, 176 insertions(+), 108 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt index 4d36d4e0f..03a7702ec 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check @@ -123,21 +124,23 @@ fun MangaNotesScreen( enter = fadeIn(), exit = fadeOut(), ) { - RichText( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .fillMaxWidth() - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = paddingValues.calculateTopPadding() + MaterialTheme.padding.medium, + SelectionContainer { + RichText( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxWidth() + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = paddingValues.calculateTopPadding() + MaterialTheme.padding.medium, + ), + style = RichTextStyle( + stringStyle = RichTextStringStyle( + linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary), + ), ), - style = RichTextStyle( - stringStyle = RichTextStringStyle( - linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary), - ), - ), - ) { - Markdown(content = state.notes.orEmpty()) + ) { + 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 2d8ac8f49..f5bddc966 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -25,6 +26,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Icon import androidx.compose.material3.SnackbarHost @@ -44,6 +46,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastMap @@ -55,7 +58,6 @@ 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 @@ -336,27 +338,47 @@ private fun MangaScreenSmallImpl( }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = { - val isFABVisible = remember(chapters) { - chapters.fastAny { !it.chapter.read } && !isAnySelected - } - AnimatedVisibility( - visible = isFABVisible, - enter = fadeIn(), - exit = fadeOut(), + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(4.dp), ) { - ExtendedFloatingActionButton( - text = { - val isReading = remember(state.chapters) { - state.chapters.fastAny { it.chapter.read } - } - Text( - text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start), - ) - }, - icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, - onClick = onContinueReading, - expanded = chapterListState.shouldExpandFAB(), - ) + AnimatedVisibility( + visible = !isAnySelected, + enter = fadeIn(), + exit = fadeOut(), + ) { + ExtendedFloatingActionButton( + text = { Text(stringResource(MR.strings.action_notes)) }, + icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = null) }, + onClick = onNotesEditClicked, + expanded = chapterListState.shouldExpandFAB(), + ) + } + + val isFABVisible = remember(chapters) { + chapters.fastAny { !it.chapter.read } && !isAnySelected + } + AnimatedVisibility( + visible = isFABVisible, + enter = fadeIn(), + exit = fadeOut(), + ) { + ExtendedFloatingActionButton( + text = { + val isReading = remember(state.chapters) { + state.chapters.fastAny { it.chapter.read } + } + Text( + text = stringResource( + if (isReading) MR.strings.action_resume else MR.strings.action_start, + ), + ) + }, + icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, + onClick = onContinueReading, + expanded = chapterListState.shouldExpandFAB(), + ) + } } }, ) { contentPadding -> @@ -424,18 +446,10 @@ private fun MangaScreenSmallImpl( defaultExpandState = state.isFromSource, description = state.manga.description, tagsProvider = { state.manga.genre }, + noteContent = state.manga.notes, onTagSearch = onTagSearch, onCopyTagToClipboard = onCopyTagToClipboard, - ) - } - - item( - key = MangaScreenItem.NOTES_SECTION, - contentType = MangaScreenItem.NOTES_SECTION, - ) { - MangaNotesSection( onClickNotes = onNotesEditClicked, - content = state.manga.notes, ) } @@ -599,21 +613,39 @@ fun MangaScreenLargeImpl( enter = fadeIn(), exit = fadeOut(), ) { - ExtendedFloatingActionButton( - text = { - val isReading = remember(state.chapters) { - state.chapters.fastAny { it.chapter.read } - } - Text( - text = stringResource( - if (isReading) MR.strings.action_resume else MR.strings.action_start, - ), + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + AnimatedVisibility( + visible = !isAnySelected, + enter = fadeIn(), + exit = fadeOut(), + ) { + ExtendedFloatingActionButton( + text = { Text(stringResource(MR.strings.action_notes)) }, + icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = null) }, + onClick = onNotesEditClicked, + expanded = chapterListState.shouldExpandFAB(), ) - }, - icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, - onClick = onContinueReading, - expanded = chapterListState.shouldExpandFAB(), - ) + } + + ExtendedFloatingActionButton( + text = { + val isReading = remember(state.chapters) { + state.chapters.fastAny { it.chapter.read } + } + Text( + text = stringResource( + if (isReading) MR.strings.action_resume else MR.strings.action_start, + ), + ) + }, + icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, + onClick = onContinueReading, + expanded = chapterListState.shouldExpandFAB(), + ) + } } }, ) { contentPadding -> @@ -663,12 +695,10 @@ fun MangaScreenLargeImpl( defaultExpandState = true, description = state.manga.description, tagsProvider = { state.manga.genre }, + noteContent = state.manga.notes, onTagSearch = onTagSearch, onCopyTagToClipboard = onCopyTagToClipboard, - ) - MangaNotesSection( onClickNotes = onNotesEditClicked, - content = state.manga.notes, ) } }, @@ -788,6 +818,7 @@ private fun LazyListScope.sharedChapterItems( is ChapterList.MissingCount -> { MissingChapterCountListItem(count = item.count) } + is ChapterList.Item -> { MangaChapterListItem( title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { 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 785038c78..22a6664d0 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt @@ -17,7 +17,6 @@ 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/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index 6a1376463..7031b8052 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -236,8 +236,10 @@ fun ExpandableMangaDescription( defaultExpandState: Boolean, description: String?, tagsProvider: () -> List?, + noteContent: String?, onTagSearch: (String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit, + onClickNotes: () -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -255,6 +257,8 @@ fun ExpandableMangaDescription( expandedDescription = desc, shrunkDescription = trimmedDescription, expanded = expanded, + noteContent = noteContent, + onNotesEditClicked = onClickNotes, modifier = Modifier .padding(top = 8.dp) .padding(horizontal = 16.dp) @@ -559,7 +563,9 @@ private fun ColumnScope.MangaContentInfo( private fun MangaSummary( expandedDescription: String, shrunkDescription: String, + noteContent: String?, expanded: Boolean, + onNotesEditClicked: () -> Unit, modifier: Modifier = Modifier, ) { val animProgress by animateFloatAsState( @@ -571,25 +577,41 @@ private fun MangaSummary( contents = listOf( { Text( - text = "\n\n", // Shows at least 3 lines + // Shows at least 3 lines if no notes + // when there are notes show 6 + text = if (noteContent.isNullOrBlank()) "\n\n" else "\n\n\n\n\n", style = MaterialTheme.typography.bodyMedium, ) }, { - Text( - text = expandedDescription, - style = MaterialTheme.typography.bodyMedium, - ) + Column { + MangaNotesSection( + content = noteContent, + expanded = true, + onClickNotes = onNotesEditClicked, + ) + Text( + text = expandedDescription, + style = MaterialTheme.typography.bodyMedium, + ) + } }, { SelectionContainer { - Text( - text = if (expanded) expandedDescription else shrunkDescription, - maxLines = Int.MAX_VALUE, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.secondaryItemAlpha(), - ) + Column { + MangaNotesSection( + content = noteContent, + expanded = expanded, + onClickNotes = onNotesEditClicked, + ) + Text( + text = if (expanded) expandedDescription else shrunkDescription, + maxLines = Int.MAX_VALUE, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.secondaryItemAlpha(), + ) + } } }, { 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 index a20931187..aab99be98 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt @@ -1,5 +1,7 @@ package eu.kanade.presentation.manga.components +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.spring import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -9,6 +11,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -31,8 +34,9 @@ import tachiyomi.presentation.core.i18n.stringResource @Composable fun MangaNotesSection( - onClickNotes: () -> Unit, content: String?, + expanded: Boolean, + onClickNotes: () -> Unit, modifier: Modifier = Modifier, ) { Column(modifier.fillMaxWidth()) { @@ -42,10 +46,45 @@ fun MangaNotesSection( .fillMaxWidth(), ) { if (!content.isNullOrBlank()) { + Column( + modifier = Modifier + .animateContentSize( + animationSpec = spring(), + alignment = Alignment.Center, + ), + ) { + if (expanded) { + Button( + onClick = onClickNotes, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.primary, + ), + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 4.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Outlined.Edit, + contentDescription = null, + modifier = Modifier + .size(16.dp), + ) + Text( + stringResource(MR.strings.action_edit_notes), + ) + } + } + } + } + RichText( modifier = Modifier - .fillMaxWidth() - .padding(MaterialTheme.padding.medium), + .fillMaxWidth(), style = RichTextStyle( stringStyle = RichTextStringStyle( linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary), @@ -54,38 +93,11 @@ fun MangaNotesSection( ) { 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 - }, - ), - ) - } + HorizontalDivider( + modifier = Modifier + .padding(vertical = 16.dp), + ) } } } @@ -96,6 +108,7 @@ fun MangaNotesSection( private fun MangaNotesSectionPreview() { MangaNotesSection( onClickNotes = {}, + expanded = true, content = "# Hello world\ntest1234 hi there!", ) } From 2c2c5cc5ef79a60fa5d99048bc5d3dedd51bf074 Mon Sep 17 00:00:00 2001 From: imkunet Date: Thu, 19 Sep 2024 19:02:28 -0400 Subject: [PATCH 14/18] Design revision 2 --- .../presentation/manga/MangaNotesScreen.kt | 10 ++ .../kanade/presentation/manga/MangaScreen.kt | 111 ++++++------------ .../manga/components/MangaInfoHeader.kt | 15 +-- .../manga/components/MangaNotesSection.kt | 101 +++++++++------- 4 files changed, 110 insertions(+), 127 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt index 03a7702ec..2c35b5a72 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt @@ -17,6 +17,7 @@ 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.material.icons.filled.EditNote import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -31,12 +32,14 @@ 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 kotlinx.collections.immutable.persistentListOf 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 +import tachiyomi.presentation.core.screens.EmptyScreenAction @Composable fun MangaNotesScreen( @@ -116,6 +119,13 @@ fun MangaNotesScreen( EmptyScreen( stringRes = MR.strings.information_no_notes, modifier = Modifier.padding(paddingValues), + actions = persistentListOf( + EmptyScreenAction( + MR.strings.action_add_notes, + Icons.Filled.EditNote, + beginEditing, + ), + ), ) } 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 f5bddc966..0cd952be9 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -26,7 +25,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Icon import androidx.compose.material3.SnackbarHost @@ -46,7 +44,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastMap @@ -338,47 +335,29 @@ private fun MangaScreenSmallImpl( }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = { - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(4.dp), + val isFABVisible = remember(chapters) { + chapters.fastAny { !it.chapter.read } && !isAnySelected + } + AnimatedVisibility( + visible = isFABVisible, + enter = fadeIn(), + exit = fadeOut(), ) { - AnimatedVisibility( - visible = !isAnySelected, - enter = fadeIn(), - exit = fadeOut(), - ) { - ExtendedFloatingActionButton( - text = { Text(stringResource(MR.strings.action_notes)) }, - icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = null) }, - onClick = onNotesEditClicked, - expanded = chapterListState.shouldExpandFAB(), - ) - } - - val isFABVisible = remember(chapters) { - chapters.fastAny { !it.chapter.read } && !isAnySelected - } - AnimatedVisibility( - visible = isFABVisible, - enter = fadeIn(), - exit = fadeOut(), - ) { - ExtendedFloatingActionButton( - text = { - val isReading = remember(state.chapters) { - state.chapters.fastAny { it.chapter.read } - } - Text( - text = stringResource( - if (isReading) MR.strings.action_resume else MR.strings.action_start, - ), - ) - }, - icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, - onClick = onContinueReading, - expanded = chapterListState.shouldExpandFAB(), - ) - } + ExtendedFloatingActionButton( + text = { + val isReading = remember(state.chapters) { + state.chapters.fastAny { it.chapter.read } + } + Text( + text = stringResource( + if (isReading) MR.strings.action_resume else MR.strings.action_start, + ), + ) + }, + icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, + onClick = onContinueReading, + expanded = chapterListState.shouldExpandFAB(), + ) } }, ) { contentPadding -> @@ -613,39 +592,21 @@ fun MangaScreenLargeImpl( enter = fadeIn(), exit = fadeOut(), ) { - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - AnimatedVisibility( - visible = !isAnySelected, - enter = fadeIn(), - exit = fadeOut(), - ) { - ExtendedFloatingActionButton( - text = { Text(stringResource(MR.strings.action_notes)) }, - icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = null) }, - onClick = onNotesEditClicked, - expanded = chapterListState.shouldExpandFAB(), + ExtendedFloatingActionButton( + text = { + val isReading = remember(state.chapters) { + state.chapters.fastAny { it.chapter.read } + } + Text( + text = stringResource( + if (isReading) MR.strings.action_resume else MR.strings.action_start, + ), ) - } - - ExtendedFloatingActionButton( - text = { - val isReading = remember(state.chapters) { - state.chapters.fastAny { it.chapter.read } - } - Text( - text = stringResource( - if (isReading) MR.strings.action_resume else MR.strings.action_start, - ), - ) - }, - icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, - onClick = onContinueReading, - expanded = chapterListState.shouldExpandFAB(), - ) - } + }, + icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, + onClick = onContinueReading, + expanded = chapterListState.shouldExpandFAB(), + ) } }, ) { contentPadding -> diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index 7031b8052..fe5e8ad84 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -597,13 +597,14 @@ private fun MangaSummary( } }, { - SelectionContainer { - Column { - MangaNotesSection( - content = noteContent, - expanded = expanded, - onClickNotes = onNotesEditClicked, - ) + + Column { + MangaNotesSection( + content = noteContent, + expanded = expanded, + onClickNotes = onNotesEditClicked, + ) + SelectionContainer { Text( text = if (expanded) expandedDescription else shrunkDescription, maxLines = Int.MAX_VALUE, 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 index aab99be98..d665c433a 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt @@ -1,7 +1,12 @@ 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 @@ -9,8 +14,9 @@ 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.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.filled.EditNote import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -39,49 +45,12 @@ fun MangaNotesSection( onClickNotes: () -> Unit, modifier: Modifier = Modifier, ) { - Column(modifier.fillMaxWidth()) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth(), - ) { - if (!content.isNullOrBlank()) { - Column( - modifier = Modifier - .animateContentSize( - animationSpec = spring(), - alignment = Alignment.Center, - ), - ) { - if (expanded) { - Button( - onClick = onClickNotes, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.primary, - ), - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 4.dp), - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Outlined.Edit, - contentDescription = null, - modifier = Modifier - .size(16.dp), - ) - Text( - stringResource(MR.strings.action_edit_notes), - ) - } - } - } - } - + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (!content.isNullOrBlank()) { + SelectionContainer { RichText( modifier = Modifier .fillMaxWidth(), @@ -93,10 +62,52 @@ fun MangaNotesSection( ) { Markdown(content = content) } + } + 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(vertical = 16.dp), + .padding( + top = if (expanded) 0.dp else 12.dp, + bottom = if (expanded) 16.dp else 12.dp, + ), ) } } From 7f7177f597669e1e0b30cb9df63d7e023a9323cc Mon Sep 17 00:00:00 2001 From: imkunet Date: Thu, 19 Sep 2024 19:03:02 -0400 Subject: [PATCH 15/18] Spotless --- .../eu/kanade/presentation/manga/components/MangaInfoHeader.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index fe5e8ad84..ee9adb737 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -597,7 +597,6 @@ private fun MangaSummary( } }, { - Column { MangaNotesSection( content = noteContent, From 6dbc9efa173e62b434e498bac8dfbba44acc3eac Mon Sep 17 00:00:00 2001 From: imkunet Date: Fri, 20 Sep 2024 04:31:06 -0400 Subject: [PATCH 16/18] 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) --- app/build.gradle.kts | 1 + .../presentation/manga/MangaNotesScreen.kt | 126 ++---------- .../kanade/presentation/manga/MangaScreen.kt | 9 +- .../manga/components/MangaNotesDisplay.kt | 28 +++ .../manga/components/MangaNotesSection.kt | 23 +-- .../manga/components/MangaNotesTextArea.kt | 186 ++++++++++++++---- .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 1 - .../ui/manga/notes/MangaNotesScreen.kt | 16 -- .../ui/manga/notes/MangaNotesScreenModel.kt | 22 --- gradle/libs.versions.toml | 2 + .../moko-resources/base/strings.xml | 11 +- 11 files changed, 206 insertions(+), 219 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesDisplay.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2e252b07e..b31f3abd7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -233,6 +233,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) diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt index 2c35b5a72..77c678f7c 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaNotesScreen.kt @@ -1,52 +1,28 @@ package eu.kanade.presentation.manga -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.EditNote -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 kotlinx.collections.immutable.persistentListOf 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 -import tachiyomi.presentation.core.screens.EmptyScreenAction @Composable fun MangaNotesScreen( state: MangaNotesScreenState.Success, navigateUp: () -> Unit, - beginEditing: () -> Unit, - endEditing: () -> Unit, onSave: (String) -> Unit, modifier: Modifier = Modifier, ) { @@ -60,98 +36,22 @@ fun MangaNotesScreen( 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), - actions = persistentListOf( - EmptyScreenAction( - MR.strings.action_add_notes, - Icons.Filled.EditNote, - beginEditing, - ), + 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(), - ) { - SelectionContainer { - RichText( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .fillMaxWidth() - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = paddingValues.calculateTopPadding() + MaterialTheme.padding.medium, - ), - style = RichTextStyle( - stringStyle = RichTextStringStyle( - linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary), - ), - ), - ) { - Markdown(content = state.notes.orEmpty()) - } - } - } + ) } } 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 0cd952be9..0ddfc267e 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -113,7 +113,6 @@ fun MangaScreen( onEditFetchIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, onNotesClicked: () -> Unit, - onNotesEditClicked: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -163,7 +162,6 @@ fun MangaScreen( onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, onNotesClicked = onNotesClicked, - onNotesEditClicked = onNotesEditClicked, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, @@ -200,7 +198,6 @@ fun MangaScreen( onEditIntervalClicked = onEditFetchIntervalClicked, onMigrateClicked = onMigrateClicked, onNotesClicked = onNotesClicked, - onNotesEditClicked = onNotesEditClicked, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, @@ -247,7 +244,6 @@ private fun MangaScreenSmallImpl( onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, onNotesClicked: () -> Unit, - onNotesEditClicked: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -428,7 +424,7 @@ private fun MangaScreenSmallImpl( noteContent = state.manga.notes, onTagSearch = onTagSearch, onCopyTagToClipboard = onCopyTagToClipboard, - onClickNotes = onNotesEditClicked, + onClickNotes = onNotesClicked, ) } @@ -498,7 +494,6 @@ fun MangaScreenLargeImpl( onEditIntervalClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, onNotesClicked: () -> Unit, - onNotesEditClicked: () -> Unit, // For bottom action menu onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, @@ -659,7 +654,7 @@ fun MangaScreenLargeImpl( noteContent = state.manga.notes, onTagSearch = onTagSearch, onCopyTagToClipboard = onCopyTagToClipboard, - onClickNotes = onNotesEditClicked, + onClickNotes = onNotesClicked, ) } }, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesDisplay.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesDisplay.kt new file mode 100644 index 000000000..7f3325b1e --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesDisplay.kt @@ -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().setHtml(html = content) + richTextState.config.linkColor = MaterialTheme.colorScheme.primary + richTextState.config.listIndent = 15 + + SelectionContainer { + RichText( + modifier = modifier, + style = MaterialTheme.typography.bodyMedium, + state = richTextState, + ) + } +} 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 index d665c433a..68e3d4e86 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesSection.kt @@ -14,7 +14,6 @@ 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.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.EditNote import androidx.compose.material3.HorizontalDivider @@ -25,13 +24,8 @@ 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 @@ -50,19 +44,10 @@ fun MangaNotesSection( horizontalAlignment = Alignment.CenterHorizontally, ) { if (!content.isNullOrBlank()) { - SelectionContainer { - RichText( - modifier = Modifier - .fillMaxWidth(), - style = RichTextStyle( - stringStyle = RichTextStringStyle( - linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary), - ), - ), - ) { - Markdown(content = content) - } - } + MangaNotesDisplay( + content = content, + modifier = modifier.fillMaxWidth(), + ) AnimatedVisibility( visible = expanded, 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 index 7737f08ac..564b7606b 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt @@ -1,26 +1,52 @@ 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.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.material3.OutlinedTextField +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.FormatSize +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.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip 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 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.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 = 10_000 @@ -30,49 +56,135 @@ fun MangaNotesTextArea( onSave: (String) -> Unit, modifier: Modifier = Modifier, ) { - var text by remember { - mutableStateOf(TextFieldValue(state.notes.orEmpty(), TextRange(Int.MAX_VALUE))) - } + val richTextState = rememberRichTextState() + richTextState.config.linkColor = MaterialTheme.colorScheme.primary + richTextState.config.listIndent = 15 val focusRequester = remember { FocusRequester() } - Box( + val largeFontSize = MaterialTheme.typography.headlineMedium.fontSize + + Column( 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", - ) - } + RichTextEditor( + state = richTextState, + textStyle = MaterialTheme.typography.bodyMedium, + maxLength = MAX_LENGTH, + placeholder = { + Text(text = stringResource(MR.strings.notes_placeholder)) }, + 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 { + SlackDemoPanelButton( + onClick = { richTextState.toggleSpanStyle(SpanStyle(fontWeight = FontWeight.Bold)) }, + isSelected = richTextState.currentSpanStyle.fontWeight == FontWeight.Bold, + icon = Icons.Outlined.FormatBold, + ) + } + item { + SlackDemoPanelButton( + onClick = { richTextState.toggleSpanStyle(SpanStyle(fontStyle = FontStyle.Italic)) }, + isSelected = richTextState.currentSpanStyle.fontStyle == FontStyle.Italic, + icon = Icons.Outlined.FormatItalic, + ) + } + item { + SlackDemoPanelButton( + 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 { + SlackDemoPanelButton( + onClick = { richTextState.toggleUnorderedList() }, + isSelected = richTextState.isUnorderedList, + icon = Icons.AutoMirrored.Outlined.FormatListBulleted, + ) + } + item { + SlackDemoPanelButton( + onClick = { richTextState.toggleOrderedList() }, + isSelected = richTextState.isOrderedList, + icon = Icons.Outlined.FormatListNumbered, + ) + } + item { + VerticalDivider( + modifier = Modifier + .height(24.dp), + ) + } + item { + SlackDemoPanelButton( + onClick = { richTextState.toggleSpanStyle(SpanStyle(fontSize = largeFontSize)) }, + isSelected = richTextState.currentSpanStyle.fontSize == largeFontSize, + icon = Icons.Outlined.FormatSize, + ) + } + } + } } LaunchedEffect(focusRequester) { + state.notes?.let { richTextState.setHtml(it) } focusRequester.requestFocus() } DisposableEffect(Unit) { onDispose { - onSave(text.text) + onSave(richTextState.toHtml()) } } } + +@Composable +fun SlackDemoPanelButton( + 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), + ) + } +} 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 3106cfdb0..46b7ea4c5 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 @@ -166,7 +166,6 @@ class MangaScreen( 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 index ea7f88db4..d97325b8e 100644 --- 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 @@ -1,6 +1,5 @@ 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 @@ -14,7 +13,6 @@ import tachiyomi.presentation.core.screens.LoadingScreen class MangaNotesScreen( private val manga: Manga, - private val editing: Boolean = false, ) : Screen() { @Composable override fun Content() { @@ -23,7 +21,6 @@ class MangaNotesScreen( val screenModel = rememberScreenModel { MangaNotesScreenModel( manga = manga, - editing = editing, ) } val state by screenModel.state.collectAsState() @@ -35,22 +32,9 @@ class MangaNotesScreen( 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 index ea3b9f680..af38396d9 100644 --- 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 @@ -12,7 +12,6 @@ import uy.kohesive.injekt.api.get class MangaNotesScreenModel( val manga: Manga, - editing: Boolean, private val setMangaNotes: SetMangaNotes = Injekt.get(), ) : StateScreenModel(MangaNotesScreenState.Loading) { @@ -24,29 +23,10 @@ class MangaNotesScreenModel( 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 @@ -75,7 +55,5 @@ sealed interface MangaNotesScreenState { data class Success( val manga: Manga, val notes: String?, - - val editing: Boolean = false, ) : MangaNotesScreenState } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7fd313312..f160218fa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 65bd59ffc..5ea7267d1 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -146,9 +146,9 @@ Move series to top Move to bottom Move series to bottom - Notes - Add Notes - Edit Notes + Note + Add Note + Edit Note Install Share Save @@ -915,7 +915,7 @@ Failed to bypass Cloudflare Tap here for help with Cloudflare *required - There are no notes here yet! + No notes WebView is required for the app to function @@ -956,4 +956,7 @@ HTTP %d, check website in WebView No Internet connection Couldn\'t reach %s + + + My analysis of the story begins with ontological antirealism, that is to say... From 2159eb37a060903d590af45bdec9e7dd46edd9d0 Mon Sep 17 00:00:00 2001 From: imkunet Date: Fri, 20 Sep 2024 04:48:32 -0400 Subject: [PATCH 17/18] Rename composable from example name --- .../manga/components/MangaNotesTextArea.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 index 564b7606b..37e304ea3 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt @@ -89,21 +89,21 @@ fun MangaNotesTextArea( .padding(top = 4.dp), ) { item { - SlackDemoPanelButton( + MangaNotesTextAreaButton( onClick = { richTextState.toggleSpanStyle(SpanStyle(fontWeight = FontWeight.Bold)) }, isSelected = richTextState.currentSpanStyle.fontWeight == FontWeight.Bold, icon = Icons.Outlined.FormatBold, ) } item { - SlackDemoPanelButton( + MangaNotesTextAreaButton( onClick = { richTextState.toggleSpanStyle(SpanStyle(fontStyle = FontStyle.Italic)) }, isSelected = richTextState.currentSpanStyle.fontStyle == FontStyle.Italic, icon = Icons.Outlined.FormatItalic, ) } item { - SlackDemoPanelButton( + MangaNotesTextAreaButton( onClick = { richTextState.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.Underline)) }, @@ -119,14 +119,14 @@ fun MangaNotesTextArea( ) } item { - SlackDemoPanelButton( + MangaNotesTextAreaButton( onClick = { richTextState.toggleUnorderedList() }, isSelected = richTextState.isUnorderedList, icon = Icons.AutoMirrored.Outlined.FormatListBulleted, ) } item { - SlackDemoPanelButton( + MangaNotesTextAreaButton( onClick = { richTextState.toggleOrderedList() }, isSelected = richTextState.isOrderedList, icon = Icons.Outlined.FormatListNumbered, @@ -139,7 +139,7 @@ fun MangaNotesTextArea( ) } item { - SlackDemoPanelButton( + MangaNotesTextAreaButton( onClick = { richTextState.toggleSpanStyle(SpanStyle(fontSize = largeFontSize)) }, isSelected = richTextState.currentSpanStyle.fontSize == largeFontSize, icon = Icons.Outlined.FormatSize, @@ -162,7 +162,7 @@ fun MangaNotesTextArea( } @Composable -fun SlackDemoPanelButton( +fun MangaNotesTextAreaButton( onClick: () -> Unit, icon: ImageVector, isSelected: Boolean, From 1b3981039741d6f3886fc65b5fc7af0d1f98d742 Mon Sep 17 00:00:00 2001 From: imkunet Date: Fri, 20 Sep 2024 16:17:48 -0400 Subject: [PATCH 18/18] Reduce char limit, switch encoding --- .../manga/components/MangaNotesDisplay.kt | 2 +- .../manga/components/MangaNotesTextArea.kt | 50 +++++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesDisplay.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesDisplay.kt index 7f3325b1e..3a594c34c 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesDisplay.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesDisplay.kt @@ -14,7 +14,7 @@ fun MangaNotesDisplay( content: String, modifier: Modifier, ) { - val richTextState = rememberRichTextState().setHtml(html = content) + val richTextState = rememberRichTextState().setMarkdown(markdown = content) richTextState.config.linkColor = MaterialTheme.colorScheme.primary richTextState.config.listIndent = 15 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 index 37e304ea3..968a7a728 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaNotesTextArea.kt @@ -19,7 +19,6 @@ 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.FormatSize import androidx.compose.material.icons.outlined.FormatUnderlined import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -42,13 +41,27 @@ 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 = 10_000 +private const val MAX_LENGTH = 250 + +private fun RichTextState.render(): String { + var current: String + var mutated = this.toMarkdown().replace("\n
\n
", "") + + do { + current = mutated + mutated = mutated.trim { it.isWhitespace() || it == '\n' } + mutated = mutated.removeSuffix("
").removePrefix("
") + } while (mutated != current) + + return current +} @Composable fun MangaNotesTextArea( @@ -61,19 +74,29 @@ fun MangaNotesTextArea( richTextState.config.listIndent = 15 val focusRequester = remember { FocusRequester() } - val largeFontSize = MaterialTheme.typography.headlineMedium.fontSize - Column( modifier = modifier .fillMaxSize(), ) { RichTextEditor( state = richTextState, - textStyle = MaterialTheme.typography.bodyMedium, + 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() @@ -132,31 +155,18 @@ fun MangaNotesTextArea( icon = Icons.Outlined.FormatListNumbered, ) } - item { - VerticalDivider( - modifier = Modifier - .height(24.dp), - ) - } - item { - MangaNotesTextAreaButton( - onClick = { richTextState.toggleSpanStyle(SpanStyle(fontSize = largeFontSize)) }, - isSelected = richTextState.currentSpanStyle.fontSize == largeFontSize, - icon = Icons.Outlined.FormatSize, - ) - } } } } LaunchedEffect(focusRequester) { - state.notes?.let { richTextState.setHtml(it) } + state.notes?.let { richTextState.setMarkdown(it) } focusRequester.requestFocus() } DisposableEffect(Unit) { onDispose { - onSave(richTextState.toHtml()) + onSave(richTextState.render()) } } }