From c475acd1eaf9f1fd9394f126a8a6f3c0daf52048 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sun, 17 Apr 2022 16:36:22 +0200 Subject: [PATCH] Migrate History screen to Compose (#6922) * Migrate History screen to Compose - Migrate screen - Strip logic from presenter into use cases and repository - Setup for other screen being able to migrate to Compose with Theme * Changes from review comments --- app/build.gradle.kts | 18 ++ .../data/history/local/HistoryPagingSource.kt | 43 +++ .../repository/HistoryRepositoryImpl.kt | 137 ++++++++ .../java/eu/kanade/domain/DomainModule.kt | 26 ++ .../history/interactor/DeleteHistoryTable.kt | 12 + .../domain/history/interactor/GetHistory.kt | 22 ++ .../interactor/GetNextChapterForManga.kt | 14 + .../history/interactor/RemoveHistoryById.kt | 21 ++ .../interactor/RemoveHistoryByMangaId.kt | 12 + .../history/repository/HistoryRepository.kt | 22 ++ .../presentation/components/EmptyScreen.kt | 49 +++ .../presentation/components/MangaCover.kt | 35 ++ .../presentation/history/HistoryScreen.kt | 298 ++++++++++++++++++ .../presentation/theme/TachiyomiTheme.kt | 20 ++ .../eu/kanade/presentation/util/Constants.kt | 5 + .../kanade/presentation/util/LazyListState.kt | 5 + app/src/main/java/eu/kanade/tachiyomi/App.kt | 2 + .../data/backup/full/FullBackupManager.kt | 2 +- .../data/backup/legacy/LegacyBackupManager.kt | 2 +- .../data/database/queries/HistoryQueries.kt | 40 ++- ...utResolver.kt => HistoryUpsertResolver.kt} | 2 +- .../tachiyomi/ui/reader/ReaderActivity.kt | 11 +- .../tachiyomi/ui/reader/ReaderPresenter.kt | 2 +- .../history/ClearHistoryDialogController.kt | 21 ++ .../ui/recent/history/HistoryAdapter.kt | 51 --- .../ui/recent/history/HistoryController.kt | 228 +++----------- .../ui/recent/history/HistoryHolder.kt | 71 ----- .../ui/recent/history/HistoryItem.kt | 42 --- .../ui/recent/history/HistoryPresenter.kt | 240 +++++++------- .../ui/recent/history/RemoveHistoryDialog.kt | 54 ---- .../main/res/layout/compose_controller.xml | 4 + .../main/res/layout/history_controller.xml | 33 -- app/src/main/res/layout/history_item.xml | 85 ----- app/src/main/res/values/strings.xml | 1 + .../tachiyomi/data/backup/BackupTest.kt | 2 +- gradle/androidx.versions.toml | 3 + gradle/compose.versions.toml | 9 + gradle/kotlinx.versions.toml | 2 +- gradle/libs.versions.toml | 5 +- settings.gradle.kts | 3 + 40 files changed, 986 insertions(+), 668 deletions(-) create mode 100644 app/src/main/java/eu/kanade/data/history/local/HistoryPagingSource.kt create mode 100644 app/src/main/java/eu/kanade/data/history/repository/HistoryRepositoryImpl.kt create mode 100644 app/src/main/java/eu/kanade/domain/DomainModule.kt create mode 100644 app/src/main/java/eu/kanade/domain/history/interactor/DeleteHistoryTable.kt create mode 100644 app/src/main/java/eu/kanade/domain/history/interactor/GetHistory.kt create mode 100644 app/src/main/java/eu/kanade/domain/history/interactor/GetNextChapterForManga.kt create mode 100644 app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt create mode 100644 app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryByMangaId.kt create mode 100644 app/src/main/java/eu/kanade/domain/history/repository/HistoryRepository.kt create mode 100644 app/src/main/java/eu/kanade/presentation/components/EmptyScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/components/MangaCover.kt create mode 100644 app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt create mode 100644 app/src/main/java/eu/kanade/presentation/util/Constants.kt create mode 100644 app/src/main/java/eu/kanade/presentation/util/LazyListState.kt rename app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/{HistoryLastReadPutResolver.kt => HistoryUpsertResolver.kt} (97%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/ClearHistoryDialogController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryAdapter.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryItem.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/RemoveHistoryDialog.kt create mode 100644 app/src/main/res/layout/compose_controller.xml delete mode 100644 app/src/main/res/layout/history_controller.xml delete mode 100644 app/src/main/res/layout/history_item.xml create mode 100644 gradle/compose.versions.toml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 04f9367d3..352171174 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -109,6 +109,7 @@ android { buildFeatures { viewBinding = true + compose = true // Disable some unused things aidl = false @@ -122,6 +123,10 @@ android { checkReleaseBuilds = false } + composeOptions { + kotlinCompilerExtensionVersion = compose.versions.compose.get() + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -133,6 +138,16 @@ android { } dependencies { + implementation(compose.foundation) + implementation(compose.material3.core) + implementation(compose.material3.adapter) + implementation(compose.animation) + implementation(compose.ui.tooling) + + implementation(androidx.paging.runtime) + implementation(androidx.paging.compose) + + implementation(kotlinx.reflect) implementation(kotlinx.bundles.coroutines) @@ -262,6 +277,9 @@ tasks { "-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi", "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi", "-Xopt-in=coil.annotation.ExperimentalCoilApi", + "-Xopt-in=androidx.compose.material3.ExperimentalMaterial3Api", + "-Xopt-in=androidx.compose.ui.ExperimentalComposeUiApi", + "-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi" ) } diff --git a/app/src/main/java/eu/kanade/data/history/local/HistoryPagingSource.kt b/app/src/main/java/eu/kanade/data/history/local/HistoryPagingSource.kt new file mode 100644 index 000000000..95a0d6e52 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/history/local/HistoryPagingSource.kt @@ -0,0 +1,43 @@ +package eu.kanade.data.history.local + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import eu.kanade.domain.history.repository.HistoryRepository +import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory +import logcat.logcat + +class HistoryPagingSource( + private val repository: HistoryRepository, + private val query: String +) : PagingSource() { + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) + } + } + + override suspend fun load(params: LoadParams): LoadResult.Page { + val nextPageNumber = params.key ?: 0 + logcat { "Loading page $nextPageNumber" } + + val response = repository.getHistory(PAGE_SIZE, nextPageNumber, query) + + val nextKey = if (response.size == 25) { + nextPageNumber + 1 + } else { + null + } + + return LoadResult.Page( + data = response, + prevKey = null, + nextKey = nextKey + ) + } + + companion object { + const val PAGE_SIZE = 25 + } +} diff --git a/app/src/main/java/eu/kanade/data/history/repository/HistoryRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/history/repository/HistoryRepositoryImpl.kt new file mode 100644 index 000000000..af610d684 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/history/repository/HistoryRepositoryImpl.kt @@ -0,0 +1,137 @@ +package eu.kanade.data.history.repository + +import eu.kanade.data.history.local.HistoryPagingSource +import eu.kanade.domain.history.repository.HistoryRepository +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.History +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.tables.HistoryTable +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import rx.Subscription +import rx.schedulers.Schedulers +import java.util.* + +class HistoryRepositoryImpl( + private val db: DatabaseHelper +) : HistoryRepository { + + /** + * Used to observe changes in the History table + * as RxJava isn't supported in Paging 3 + */ + private var subscription: Subscription? = null + + /** + * Paging Source for history table + */ + override fun getHistory(query: String): HistoryPagingSource { + subscription?.unsubscribe() + val pagingSource = HistoryPagingSource(this, query) + subscription = db.db + .observeChangesInTable(HistoryTable.TABLE) + .observeOn(Schedulers.io()) + .subscribe { + pagingSource.invalidate() + } + return pagingSource + } + + override suspend fun getHistory(limit: Int, page: Int, query: String) = coroutineScope { + withContext(Dispatchers.IO) { + // Set date limit for recent manga + val calendar = Calendar.getInstance().apply { + time = Date() + add(Calendar.YEAR, -50) + } + + db.getRecentManga(calendar.time, limit, page * limit, query) + .executeAsBlocking() + } + } + + override suspend fun getNextChapterForManga(manga: Manga, chapter: Chapter): Chapter? = coroutineScope { + withContext(Dispatchers.IO) { + if (!chapter.read) { + return@withContext chapter + } + + val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { + Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } + Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } + Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) } + else -> throw NotImplementedError("Unknown sorting method") + } + + val chapters = db.getChapters(manga) + .executeAsBlocking() + .sortedWith { c1, c2 -> sortFunction(c1, c2) } + + val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id } + return@withContext when (manga.sorting) { + Manga.CHAPTER_SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1) + Manga.CHAPTER_SORTING_NUMBER -> { + val chapterNumber = chapter.chapter_number + + ((currChapterIndex + 1) until chapters.size) + .map { chapters[it] } + .firstOrNull { + it.chapter_number > chapterNumber && + it.chapter_number <= chapterNumber + 1 + } + } + Manga.CHAPTER_SORTING_UPLOAD_DATE -> { + chapters.drop(currChapterIndex + 1) + .firstOrNull { it.date_upload >= chapter.date_upload } + } + else -> throw NotImplementedError("Unknown sorting method") + } + } + } + + override suspend fun resetHistory(history: History): Boolean = coroutineScope { + withContext(Dispatchers.IO) { + try { + history.last_read = 0 + db.upsertHistoryLastRead(history) + .executeAsBlocking() + true + } catch (e: Throwable) { + logcat(throwable = e) + false + } + } + } + + override suspend fun resetHistoryByMangaId(mangaId: Long): Boolean = coroutineScope { + withContext(Dispatchers.IO) { + try { + val history = db.getHistoryByMangaId(mangaId) + .executeAsBlocking() + history.forEach { it.last_read = 0 } + db.upsertHistoryLastRead(history) + .executeAsBlocking() + true + } catch (e: Throwable) { + logcat(throwable = e) + false + } + } + } + + override suspend fun deleteAllHistory(): Boolean = coroutineScope { + withContext(Dispatchers.IO) { + try { + db.dropHistoryTable() + .executeAsBlocking() + true + } catch (e: Throwable) { + logcat(throwable = e) + false + } + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt new file mode 100644 index 000000000..d54c52c2a --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -0,0 +1,26 @@ +package eu.kanade.domain + +import eu.kanade.data.history.repository.HistoryRepositoryImpl +import eu.kanade.domain.history.interactor.DeleteHistoryTable +import eu.kanade.domain.history.interactor.GetHistory +import eu.kanade.domain.history.interactor.GetNextChapterForManga +import eu.kanade.domain.history.interactor.RemoveHistoryById +import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId +import eu.kanade.domain.history.repository.HistoryRepository +import uy.kohesive.injekt.api.InjektModule +import uy.kohesive.injekt.api.InjektRegistrar +import uy.kohesive.injekt.api.addFactory +import uy.kohesive.injekt.api.addSingletonFactory +import uy.kohesive.injekt.api.get + +class DomainModule : InjektModule { + + override fun InjektRegistrar.registerInjectables() { + addSingletonFactory { HistoryRepositoryImpl(get()) } + addFactory { DeleteHistoryTable(get()) } + addFactory { GetHistory(get()) } + addFactory { GetNextChapterForManga(get()) } + addFactory { RemoveHistoryById(get()) } + addFactory { RemoveHistoryByMangaId(get()) } + } +} diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/DeleteHistoryTable.kt b/app/src/main/java/eu/kanade/domain/history/interactor/DeleteHistoryTable.kt new file mode 100644 index 000000000..bebf1209d --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/interactor/DeleteHistoryTable.kt @@ -0,0 +1,12 @@ +package eu.kanade.domain.history.interactor + +import eu.kanade.domain.history.repository.HistoryRepository + +class DeleteHistoryTable( + private val repository: HistoryRepository +) { + + suspend fun await(): Boolean { + return repository.deleteAllHistory() + } +} diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/GetHistory.kt b/app/src/main/java/eu/kanade/domain/history/interactor/GetHistory.kt new file mode 100644 index 000000000..d376e0a1b --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/interactor/GetHistory.kt @@ -0,0 +1,22 @@ +package eu.kanade.domain.history.interactor + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import eu.kanade.data.history.local.HistoryPagingSource +import eu.kanade.domain.history.repository.HistoryRepository +import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory +import kotlinx.coroutines.flow.Flow + +class GetHistory( + private val repository: HistoryRepository +) { + + fun subscribe(query: String): Flow> { + return Pager( + PagingConfig(pageSize = HistoryPagingSource.PAGE_SIZE) + ) { + repository.getHistory(query) + }.flow + } +} diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/GetNextChapterForManga.kt b/app/src/main/java/eu/kanade/domain/history/interactor/GetNextChapterForManga.kt new file mode 100644 index 000000000..ecaf53af7 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/interactor/GetNextChapterForManga.kt @@ -0,0 +1,14 @@ +package eu.kanade.domain.history.interactor + +import eu.kanade.domain.history.repository.HistoryRepository +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga + +class GetNextChapterForManga( + private val repository: HistoryRepository +) { + + suspend fun await(manga: Manga, chapter: Chapter): Chapter? { + return repository.getNextChapterForManga(manga, chapter) + } +} diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt new file mode 100644 index 000000000..a0f022fd6 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt @@ -0,0 +1,21 @@ +package eu.kanade.domain.history.interactor + +import eu.kanade.domain.history.repository.HistoryRepository +import eu.kanade.tachiyomi.data.database.models.History +import eu.kanade.tachiyomi.data.database.models.HistoryImpl + +class RemoveHistoryById( + private val repository: HistoryRepository +) { + + suspend fun await(history: History): Boolean { + // Workaround for list not freaking out when changing reference varaible + val history = HistoryImpl().apply { + id = history.id + chapter_id = history.chapter_id + last_read = history.last_read + time_read = history.time_read + } + return repository.resetHistory(history) + } +} diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryByMangaId.kt b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryByMangaId.kt new file mode 100644 index 000000000..1868a1ba2 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryByMangaId.kt @@ -0,0 +1,12 @@ +package eu.kanade.domain.history.interactor + +import eu.kanade.domain.history.repository.HistoryRepository + +class RemoveHistoryByMangaId( + private val repository: HistoryRepository +) { + + suspend fun await(mangaId: Long): Boolean { + return repository.resetHistoryByMangaId(mangaId) + } +} diff --git a/app/src/main/java/eu/kanade/domain/history/repository/HistoryRepository.kt b/app/src/main/java/eu/kanade/domain/history/repository/HistoryRepository.kt new file mode 100644 index 000000000..5846b2e16 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/repository/HistoryRepository.kt @@ -0,0 +1,22 @@ +package eu.kanade.domain.history.repository + +import eu.kanade.data.history.local.HistoryPagingSource +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.History +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory + +interface HistoryRepository { + + fun getHistory(query: String): HistoryPagingSource + + suspend fun getHistory(limit: Int, page: Int, query: String): List + + suspend fun getNextChapterForManga(manga: Manga, chapter: Chapter): Chapter? + + suspend fun resetHistory(history: History): Boolean + + suspend fun resetHistoryByMangaId(mangaId: Long): Boolean + + suspend fun deleteAllHistory(): Boolean +} diff --git a/app/src/main/java/eu/kanade/presentation/components/EmptyScreen.kt b/app/src/main/java/eu/kanade/presentation/components/EmptyScreen.kt new file mode 100644 index 000000000..e94bef827 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/EmptyScreen.kt @@ -0,0 +1,49 @@ +package eu.kanade.presentation.components + +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidView +import eu.kanade.tachiyomi.widget.EmptyView + +@Composable +fun EmptyScreen( + @StringRes textResource: Int, + actions: List? = null, +) { + EmptyScreen( + message = stringResource(id = textResource), + actions = actions, + ) +} + +@Composable +fun EmptyScreen( + message: String, + actions: List? = null, +) { + Box( + modifier = Modifier + .fillMaxSize() + ) { + AndroidView( + factory = { context -> + EmptyView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + } + }, + modifier = Modifier + .align(Alignment.Center), + ) { view -> + view.show(message, actions) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/components/MangaCover.kt b/app/src/main/java/eu/kanade/presentation/components/MangaCover.kt new file mode 100644 index 000000000..524b5744f --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/MangaCover.kt @@ -0,0 +1,35 @@ +package eu.kanade.presentation.components + +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import eu.kanade.tachiyomi.data.database.models.Manga + +enum class MangaCoverAspect(val ratio: Float) { + SQUARE(1f / 1f), + COVER(2f / 3f) +} + +@Composable +fun MangaCover( + modifier: Modifier = Modifier, + manga: Manga, + aspect: MangaCoverAspect, + contentDescription: String = "", + shape: Shape = RoundedCornerShape(4.dp) +) { + AsyncImage( + model = manga, + contentDescription = contentDescription, + modifier = modifier + .aspectRatio(aspect.ratio) + .clip(shape), + contentScale = ContentScale.Crop + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt new file mode 100644 index 000000000..a0d00cd65 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -0,0 +1,298 @@ +package eu.kanade.presentation.history + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.core.text.buildSpannedString +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.items +import eu.kanade.presentation.components.EmptyScreen +import eu.kanade.presentation.components.MangaCover +import eu.kanade.presentation.components.MangaCoverAspect +import eu.kanade.presentation.theme.TachiyomiTheme +import eu.kanade.presentation.util.horizontalPadding +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter +import eu.kanade.tachiyomi.ui.recent.history.UiModel +import eu.kanade.tachiyomi.util.lang.toRelativeString +import eu.kanade.tachiyomi.util.lang.toTimestampString +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.text.DateFormat +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.* + +val chapterFormatter = DecimalFormat( + "#.###", + DecimalFormatSymbols() + .apply { decimalSeparator = '.' }, +) + +@Composable +fun HistoryScreen( + composeView: ComposeView, + presenter: HistoryPresenter, + onClickItem: (MangaChapterHistory) -> Unit, + onClickResume: (MangaChapterHistory) -> Unit, + onClickDelete: (MangaChapterHistory, Boolean) -> Unit, +) { + val nestedSrollInterop = rememberNestedScrollInteropConnection(composeView) + TachiyomiTheme { + val state by presenter.state.collectAsState() + val history = state.list?.collectAsLazyPagingItems() + when { + history == null -> { + CircularProgressIndicator() + } + history.itemCount == 0 -> { + EmptyScreen( + textResource = R.string.information_no_recent_manga + ) + } + else -> { + HistoryContent( + nestedScroll = nestedSrollInterop, + history = history, + onClickItem = onClickItem, + onClickResume = onClickResume, + onClickDelete = onClickDelete, + ) + } + } + } +} + +@Composable +fun HistoryContent( + history: LazyPagingItems, + onClickItem: (MangaChapterHistory) -> Unit, + onClickResume: (MangaChapterHistory) -> Unit, + onClickDelete: (MangaChapterHistory, Boolean) -> Unit, + preferences: PreferencesHelper = Injekt.get(), + nestedScroll: NestedScrollConnection +) { + val relativeTime: Int = remember { preferences.relativeTime().get() } + val dateFormat: DateFormat = remember { preferences.dateFormat() } + + val (removeState, setRemoveState) = remember { mutableStateOf(null) } + + val scrollState = rememberLazyListState() + LazyColumn( + modifier = Modifier + .nestedScroll(nestedScroll), + state = scrollState, + ) { + items(history) { item -> + when (item) { + is UiModel.Header -> { + HistoryHeader( + modifier = Modifier + .animateItemPlacement(), + date = item.date, + relativeTime = relativeTime, + dateFormat = dateFormat + ) + } + is UiModel.History -> { + val value = item.item + HistoryItem( + modifier = Modifier.animateItemPlacement(), + history = value, + onClickItem = { onClickItem(value) }, + onClickResume = { onClickResume(value) }, + onClickDelete = { setRemoveState(value) }, + ) + } + null -> {} + } + } + item { + Spacer( + modifier = Modifier + .navigationBarsPadding() + ) + } + } + + if (removeState != null) { + RemoveHistoryDialog( + onPositive = { all -> + onClickDelete(removeState, all) + setRemoveState(null) + }, + onNegative = { setRemoveState(null) } + ) + } +} + +@Composable +fun HistoryHeader( + modifier: Modifier = Modifier, + date: Date, + relativeTime: Int, + dateFormat: DateFormat, +) { + Text( + modifier = modifier + .padding(horizontal = horizontalPadding, vertical = 8.dp), + text = date.toRelativeString( + LocalContext.current, + relativeTime, + dateFormat + ), + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.SemiBold, + ) + ) +} + +@Composable +fun HistoryItem( + modifier: Modifier = Modifier, + history: MangaChapterHistory, + onClickItem: () -> Unit, + onClickResume: () -> Unit, + onClickDelete: () -> Unit, +) { + Row( + modifier = modifier + .clickable(onClick = onClickItem) + .height(96.dp) + .padding(horizontal = horizontalPadding, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + MangaCover( + modifier = Modifier.fillMaxHeight(), + manga = history.manga, + aspect = MangaCoverAspect.COVER + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = horizontalPadding, end = 8.dp), + ) { + val textStyle = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = history.manga.title, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = textStyle.copy(fontWeight = FontWeight.SemiBold) + ) + Row { + Text( + text = buildSpannedString { + if (history.chapter.chapter_number > -1) { + append( + stringResource( + R.string.history_prefix, + chapterFormatter.format(history.chapter.chapter_number) + ) + ) + } + append(Date(history.history.last_read).toTimestampString()) + }.toString(), + modifier = Modifier.padding(top = 2.dp), + style = textStyle + ) + } + } + IconButton(onClick = onClickDelete) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = stringResource(id = R.string.action_delete), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + IconButton(onClick = onClickResume) { + Icon( + imageVector = Icons.Filled.PlayArrow, + contentDescription = stringResource(id = R.string.action_resume), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } +} + +@Composable +fun RemoveHistoryDialog( + onPositive: (Boolean) -> Unit, + onNegative: () -> Unit +) { + val (removeEverything, removeEverythingState) = remember { mutableStateOf(false) } + + AlertDialog( + title = { + Text(text = stringResource(id = R.string.action_remove)) + }, + text = { + Column { + Text(text = stringResource(id = R.string.dialog_with_checkbox_remove_description)) + Row( + modifier = Modifier.toggleable(value = removeEverything, onValueChange = removeEverythingState), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = removeEverything, + onCheckedChange = removeEverythingState, + ) + Text( + text = stringResource(id = R.string.dialog_with_checkbox_reset) + ) + } + } + }, + onDismissRequest = onNegative, + confirmButton = { + TextButton(onClick = { onPositive(removeEverything) }) { + Text(text = stringResource(id = R.string.action_remove)) + } + }, + dismissButton = { + TextButton(onClick = onNegative) { + Text(text = stringResource(id = R.string.action_cancel)) + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt b/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt new file mode 100644 index 000000000..c01bc1e3b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt @@ -0,0 +1,20 @@ +package eu.kanade.presentation.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.google.android.material.composethemeadapter3.createMdc3Theme + +@Composable +fun TachiyomiTheme(content: @Composable () -> Unit) { + val context = LocalContext.current + var (colorScheme, typography) = createMdc3Theme( + context = context + ) + + MaterialTheme( + colorScheme = colorScheme!!, + typography = typography!!, + content = content + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/util/Constants.kt b/app/src/main/java/eu/kanade/presentation/util/Constants.kt new file mode 100644 index 000000000..fcf64d77b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/util/Constants.kt @@ -0,0 +1,5 @@ +package eu.kanade.presentation.util + +import androidx.compose.ui.unit.dp + +val horizontalPadding = 16.dp diff --git a/app/src/main/java/eu/kanade/presentation/util/LazyListState.kt b/app/src/main/java/eu/kanade/presentation/util/LazyListState.kt new file mode 100644 index 000000000..adf7cd80c --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/util/LazyListState.kt @@ -0,0 +1,5 @@ +package eu.kanade.presentation.util + +import androidx.compose.foundation.lazy.LazyListState + +fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1 diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index f4ea91d01..b37721ce6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -24,6 +24,7 @@ import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.disk.DiskCache import coil.util.DebugLogger +import eu.kanade.domain.DomainModule import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder @@ -74,6 +75,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { } Injekt.importModule(AppModule(this)) + Injekt.importModule(DomainModule()) setupAcra() setupNotificationChannels() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt index ca15125ae..c1807cdee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt @@ -294,7 +294,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { } } } - databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking() + databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt index 4984b242c..8d42245e0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt @@ -168,7 +168,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab } } } - databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking() + databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt index 24b57f48e..b1fe42904 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt @@ -5,7 +5,7 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory -import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver +import eu.kanade.tachiyomi.data.database.resolvers.HistoryUpsertResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver import eu.kanade.tachiyomi.data.database.tables.HistoryTable import java.util.Date @@ -64,9 +64,9 @@ interface HistoryQueries : DbProvider { * Inserts history object if not yet in database * @param history history object */ - fun updateHistoryLastRead(history: History) = db.put() + fun upsertHistoryLastRead(history: History) = db.put() .`object`(history) - .withPutResolver(HistoryLastReadPutResolver()) + .withPutResolver(HistoryUpsertResolver()) .prepare() /** @@ -74,12 +74,40 @@ interface HistoryQueries : DbProvider { * Inserts history object if not yet in database * @param historyList history object list */ - fun updateHistoryLastRead(historyList: List) = db.put() + fun upsertHistoryLastRead(historyList: List) = db.put() .objects(historyList) - .withPutResolver(HistoryLastReadPutResolver()) + .withPutResolver(HistoryUpsertResolver()) .prepare() - fun deleteHistory() = db.delete() + fun resetHistoryLastRead(historyId: Long) = db.executeSQL() + .withQuery( + RawQuery.builder() + .query( + """ + UPDATE ${HistoryTable.TABLE} + SET history_last_read = 0 + WHERE ${HistoryTable.COL_ID} = $historyId + """.trimIndent() + ) + .build() + ) + .prepare() + + fun resetHistoryLastRead(historyIds: List) = db.executeSQL() + .withQuery( + RawQuery.builder() + .query( + """ + UPDATE ${HistoryTable.TABLE} + SET history_last_read = 0 + WHERE ${HistoryTable.COL_ID} in ${historyIds.joinToString(",", "(", ")")} + """.trimIndent() + ) + .build() + ) + .prepare() + + fun dropHistoryTable() = db.delete() .byQuery( DeleteQuery.builder() .table(HistoryTable.TABLE) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryLastReadPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryUpsertResolver.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryLastReadPutResolver.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryUpsertResolver.kt index 7bcba97f7..908aca16d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryLastReadPutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryUpsertResolver.kt @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.database.mappers.HistoryPutResolver import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.tables.HistoryTable -class HistoryLastReadPutResolver : HistoryPutResolver() { +class HistoryUpsertResolver : HistoryPutResolver() { /** * Updates last_read time of chapter diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 51fa73cc1..9053864d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -97,14 +97,19 @@ import kotlin.math.max class ReaderActivity : BaseRxActivity() { companion object { - fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent { + + fun newIntent(context: Context, mangaId: Long?, chapterId: Long?): Intent { return Intent(context, ReaderActivity::class.java).apply { - putExtra("manga", manga.id) - putExtra("chapter", chapter.id) + putExtra("manga", mangaId) + putExtra("chapter", chapterId) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } } + fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent { + return newIntent(context, manga.id, chapter.id) + } + private const val ENABLED_BUTTON_IMAGE_ALPHA = 255 private const val DISABLED_BUTTON_IMAGE_ALPHA = 64 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index fb954bab7..8debf6f3c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -449,7 +449,7 @@ class ReaderPresenter( private fun saveChapterHistory(chapter: ReaderChapter) { if (!incognitoMode) { val history = History.create(chapter.chapter).apply { last_read = Date().time } - db.updateHistoryLastRead(history).asRxCompletable() + db.upsertHistoryLastRead(history).asRxCompletable() .onErrorComplete() .subscribeOn(Schedulers.io()) .subscribe() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/ClearHistoryDialogController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/ClearHistoryDialogController.kt new file mode 100644 index 000000000..a4080a01d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/ClearHistoryDialogController.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.ui.recent.history + +import android.app.Dialog +import android.os.Bundle +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class ClearHistoryDialogController : DialogController() { + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(activity!!) + .setMessage(R.string.clear_history_confirmation) + .setPositiveButton(android.R.string.ok) { _, _ -> + (targetController as? HistoryController) + ?.presenter + ?.deleteAllHistory() + } + .setNegativeButton(android.R.string.cancel, null) + .create() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryAdapter.kt deleted file mode 100644 index 8c1a88ec8..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryAdapter.kt +++ /dev/null @@ -1,51 +0,0 @@ -package eu.kanade.tachiyomi.ui.recent.history - -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.source.SourceManager -import uy.kohesive.injekt.injectLazy -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols - -/** - * Adapter of HistoryHolder. - * Connection between Fragment and Holder - * Holder updates should be called from here. - * - * @param controller a HistoryController object - * @constructor creates an instance of the adapter. - */ -class HistoryAdapter(controller: HistoryController) : - FlexibleAdapter>(null, controller, true) { - - val sourceManager: SourceManager by injectLazy() - - val resumeClickListener: OnResumeClickListener = controller - val removeClickListener: OnRemoveClickListener = controller - val itemClickListener: OnItemClickListener = controller - - /** - * DecimalFormat used to display correct chapter number - */ - val decimalFormat = DecimalFormat( - "#.###", - DecimalFormatSymbols() - .apply { decimalSeparator = '.' }, - ) - - init { - setDisplayHeadersAtStartUp(true) - } - - interface OnResumeClickListener { - fun onResumeClick(position: Int) - } - - interface OnRemoveClickListener { - fun onRemoveClick(position: Int) - } - - interface OnItemClickListener { - fun onItemClick(position: Int) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt index 333dea3d4..70d5aefbe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt @@ -1,192 +1,65 @@ package eu.kanade.tachiyomi.ui.recent.history -import android.app.Dialog -import android.os.Bundle import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.appcompat.widget.SearchView -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dev.chrisbanes.insetter.applyInsetter -import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.presentation.history.HistoryScreen import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.backup.BackupRestoreService -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.History -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.databinding.HistoryControllerBinding -import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.databinding.ComposeControllerBinding import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.browse.source.browse.ProgressItem -import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.onAnimationsFinished -import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import logcat.LogPriority import reactivecircus.flowbinding.appcompat.queryTextChanges -import uy.kohesive.injekt.injectLazy /** * Fragment that shows recently read manga. */ class HistoryController : - NucleusController(), - RootController, - FlexibleAdapter.OnUpdateListener, - FlexibleAdapter.EndlessScrollListener, - HistoryAdapter.OnRemoveClickListener, - HistoryAdapter.OnResumeClickListener, - HistoryAdapter.OnItemClickListener, - RemoveHistoryDialog.Listener { + NucleusController(), + RootController { - private val db: DatabaseHelper by injectLazy() - - /** - * Adapter containing the recent manga. - */ - var adapter: HistoryAdapter? = null - private set - - /** - * Endless loading item. - */ - private var progressItem: ProgressItem? = null - - /** - * Search query. - */ private var query = "" - override fun getTitle(): String? { - return resources?.getString(R.string.label_recent_manga) - } + override fun getTitle(): String? = resources?.getString(R.string.label_recent_manga) - override fun createPresenter(): HistoryPresenter { - return HistoryPresenter() - } + override fun createPresenter(): HistoryPresenter = HistoryPresenter() - override fun createBinding(inflater: LayoutInflater) = HistoryControllerBinding.inflate(inflater) + override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding = + ComposeControllerBinding.inflate(inflater) override fun onViewCreated(view: View) { super.onViewCreated(view) - binding.recycler.applyInsetter { - type(navigationBars = true) { - padding() - } - } - - // Initialize adapter - binding.recycler.layoutManager = LinearLayoutManager(view.context) - adapter = HistoryAdapter(this@HistoryController) - binding.recycler.setHasFixedSize(true) - binding.recycler.adapter = adapter - adapter?.fastScroller = binding.fastScroller - } - - override fun onDestroyView(view: View) { - adapter = null - super.onDestroyView(view) - } - - /** - * Populate adapter with chapters - * - * @param mangaHistory list of manga history - */ - fun onNextManga(mangaHistory: List, cleanBatch: Boolean = false) { - if (adapter?.itemCount ?: 0 == 0) { - resetProgressItem() - } - if (cleanBatch) { - adapter?.updateDataSet(mangaHistory) - } else { - adapter?.onLoadMoreComplete(mangaHistory) - } - binding.recycler.onAnimationsFinished { - (activity as? MainActivity)?.ready = true - } - } - - /** - * Safely error if next page load fails - */ - fun onAddPageError(error: Throwable) { - adapter?.onLoadMoreComplete(null) - adapter?.endlessTargetCount = 1 - logcat(LogPriority.ERROR, error) - } - - override fun onUpdateEmptyView(size: Int) { - if (size > 0) { - binding.emptyView.hide() - } else { - binding.emptyView.show(R.string.information_no_recent_manga) - } - } - - /** - * Sets a new progress item and reenables the scroll listener. - */ - private fun resetProgressItem() { - progressItem = ProgressItem() - adapter?.endlessTargetCount = 0 - adapter?.setEndlessScrollListener(this, progressItem!!) - } - - override fun onLoadMore(lastPosition: Int, currentPage: Int) { - val view = view ?: return - if (BackupRestoreService.isRunning(view.context.applicationContext)) { - onAddPageError(Throwable()) - return - } - val adapter = adapter ?: return - presenter.requestNext(adapter.itemCount - adapter.headerItems.size, query) - } - - override fun noMoreLoad(newItemsSize: Int) {} - - override fun onResumeClick(position: Int) { - val activity = activity ?: return - val (manga, chapter, _) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return - - val nextChapter = presenter.getNextChapter(chapter, manga) - if (nextChapter != null) { - val intent = ReaderActivity.newIntent(activity, manga, nextChapter) - startActivity(intent) - } else { - activity.toast(R.string.no_next_chapter) - } - } - - override fun onRemoveClick(position: Int) { - val (manga, _, history) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return - RemoveHistoryDialog(this, manga, history).showDialog(router) - } - - override fun onItemClick(position: Int) { - val manga = (adapter?.getItem(position) as? HistoryItem)?.mch?.manga ?: return - router.pushController(MangaController(manga).withFadeTransaction()) - } - - override fun removeHistory(manga: Manga, history: History, all: Boolean) { - if (all) { - // Reset last read of chapter to 0L - presenter.removeAllFromHistory(manga.id!!) - } else { - // Remove all chapters belonging to manga from library - presenter.removeFromHistory(history) + binding.root.setContent { + HistoryScreen( + composeView = binding.root, + presenter = presenter, + onClickItem = { (manga, _, _) -> + router.pushController(MangaController(manga).withFadeTransaction()) + }, + onClickResume = { (manga, chapter, _) -> + presenter.getNextChapterForManga(manga, chapter) + }, + onClickDelete = { (manga, _, history), all -> + if (all) { + // Reset last read of chapter to 0L + presenter.removeAllFromHistory(manga.id!!) + } else { + // Remove all chapters belonging to manga from library + presenter.removeFromHistory(history) + } + }, + ) } } @@ -201,46 +74,33 @@ class HistoryController : searchView.clearFocus() } searchView.queryTextChanges() - .drop(1) // Drop first event after subscribed .filter { router.backstack.lastOrNull()?.controller == this } .onEach { query = it.toString() - presenter.updateList(query) + presenter.search(query) } .launchIn(viewScope) - - // Fixes problem with the overflow icon showing up in lieu of search - searchItem.fixExpand( - onExpand = { invalidateMenuOnExpand() }, - ) } override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { + return when (item.itemId) { R.id.action_clear_history -> { - val ctrl = ClearHistoryDialogController() - ctrl.targetController = this@HistoryController - ctrl.showDialog(router) + val dialog = ClearHistoryDialogController() + dialog.targetController = this@HistoryController + dialog.showDialog(router) + true } - } - - return super.onOptionsItemSelected(item) - } - - class ClearHistoryDialogController : DialogController() { - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(activity!!) - .setMessage(R.string.clear_history_confirmation) - .setPositiveButton(android.R.string.ok) { _, _ -> - (targetController as? HistoryController)?.clearHistory() - } - .setNegativeButton(android.R.string.cancel, null) - .create() + else -> super.onOptionsItemSelected(item) } } - private fun clearHistory() { - db.deleteHistory().executeAsBlocking() - activity?.toast(R.string.clear_history_completed) + fun openChapter(chapter: Chapter?) { + val activity = activity ?: return + if (chapter != null) { + val intent = ReaderActivity.newIntent(activity, chapter.manga_id, chapter.id) + startActivity(intent) + } else { + activity.toast(R.string.no_next_chapter) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt deleted file mode 100644 index 8164e5cc8..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt +++ /dev/null @@ -1,71 +0,0 @@ -package eu.kanade.tachiyomi.ui.recent.history - -import android.view.View -import coil.dispose -import coil.load -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory -import eu.kanade.tachiyomi.databinding.HistoryItemBinding -import eu.kanade.tachiyomi.util.lang.toTimestampString -import java.util.Date - -/** - * Holder that contains recent manga item - * Uses R.layout.item_recently_read. - * UI related actions should be called from here. - * - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @constructor creates a new recent chapter holder. - */ -class HistoryHolder( - view: View, - val adapter: HistoryAdapter, -) : FlexibleViewHolder(view, adapter) { - - private val binding = HistoryItemBinding.bind(view) - - init { - binding.holder.setOnClickListener { - adapter.itemClickListener.onItemClick(bindingAdapterPosition) - } - - binding.remove.setOnClickListener { - adapter.removeClickListener.onRemoveClick(bindingAdapterPosition) - } - - binding.resume.setOnClickListener { - adapter.resumeClickListener.onResumeClick(bindingAdapterPosition) - } - } - - /** - * Set values of view - * - * @param item item containing history information - */ - fun bind(item: MangaChapterHistory) { - // Retrieve objects - val (manga, chapter, history) = item - - // Set manga title - binding.mangaTitle.text = manga.title - - // Set chapter number + timestamp - if (chapter.chapter_number > -1f) { - val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) - binding.mangaSubtitle.text = itemView.context.getString( - R.string.recent_manga_time, - formattedNumber, - Date(history.last_read).toTimestampString(), - ) - } else { - binding.mangaSubtitle.text = Date(history.last_read).toTimestampString() - } - - // Set cover - binding.cover.dispose() - binding.cover.load(item.manga) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryItem.kt deleted file mode 100644 index 58f9e0cc2..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryItem.kt +++ /dev/null @@ -1,42 +0,0 @@ -package eu.kanade.tachiyomi.ui.recent.history - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractSectionableItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory -import eu.kanade.tachiyomi.ui.recent.DateSectionItem - -class HistoryItem(val mch: MangaChapterHistory, header: DateSectionItem) : - AbstractSectionableItem(header) { - - override fun getLayoutRes(): Int { - return R.layout.history_item - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): HistoryHolder { - return HistoryHolder(view, adapter as HistoryAdapter) - } - - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: HistoryHolder, - position: Int, - payloads: List?, - ) { - holder.bind(mch) - } - - override fun equals(other: Any?): Boolean { - if (other is HistoryItem) { - return mch.manga.id == other.mch.manga.id - } - return false - } - - override fun hashCode(): Int { - return mch.manga.id!!.hashCode() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt index e8feb084d..aa726176c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt @@ -1,157 +1,135 @@ package eu.kanade.tachiyomi.ui.recent.history import android.os.Bundle -import eu.kanade.tachiyomi.data.database.DatabaseHelper +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.insertSeparators +import androidx.paging.map +import eu.kanade.domain.history.interactor.DeleteHistoryTable +import eu.kanade.domain.history.interactor.GetHistory +import eu.kanade.domain.history.interactor.GetNextChapterForManga +import eu.kanade.domain.history.interactor.RemoveHistoryById +import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory -import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.recent.DateSectionItem +import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.toDateKey -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import uy.kohesive.injekt.injectLazy -import java.text.DateFormat -import java.util.Calendar -import java.util.Date -import java.util.TreeMap +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.* /** * Presenter of HistoryFragment. * Contains information and data for fragment. * Observable updates should be called from here. */ -class HistoryPresenter : BasePresenter() { +class HistoryPresenter( + private val getHistory: GetHistory = Injekt.get(), + private val getNextChapterForManga: GetNextChapterForManga = Injekt.get(), + private val deleteHistoryTable: DeleteHistoryTable = Injekt.get(), + private val removeHistoryById: RemoveHistoryById = Injekt.get(), + private val removeHistoryByMangaId: RemoveHistoryByMangaId = Injekt.get(), +) : BasePresenter() { - private val db: DatabaseHelper by injectLazy() - private val preferences: PreferencesHelper by injectLazy() - - private val relativeTime: Int = preferences.relativeTime().get() - private val dateFormat: DateFormat = preferences.dateFormat() - - private var recentMangaSubscription: Subscription? = null + private var _query: MutableStateFlow = MutableStateFlow("") + private var _state: MutableStateFlow = MutableStateFlow(HistoryState.EMPTY) + val state: StateFlow = _state override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - // Used to get a list of recently read manga - updateList() - } - - fun requestNext(offset: Int, search: String = "") { - getRecentMangaObservable(offset = offset, search = search) - .subscribeLatestCache( - { view, mangas -> - view.onNextManga(mangas) - }, - HistoryController::onAddPageError, - ) - } - - /** - * Get recent manga observable - * @return list of history - */ - private fun getRecentMangaObservable(limit: Int = 25, offset: Int = 0, search: String = ""): Observable> { - // Set date limit for recent manga - val cal = Calendar.getInstance().apply { - time = Date() - add(Calendar.YEAR, -50) - } - - return db.getRecentManga(cal.time, limit, offset, search).asRxObservable() - .map { recents -> - val map = TreeMap> { d1, d2 -> d2.compareTo(d1) } - val byDay = recents - .groupByTo(map) { it.history.last_read.toDateKey() } - byDay.flatMap { entry -> - val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat) - entry.value.map { HistoryItem(it, dateItem) } - } - } - .observeOn(AndroidSchedulers.mainThread()) - } - - /** - * Reset last read of chapter to 0L - * @param history history belonging to chapter - */ - fun removeFromHistory(history: History) { - history.last_read = 0L - db.updateHistoryLastRead(history).asRxObservable() - .subscribe() - } - - /** - * Pull a list of history from the db - * @param search a search query to use for filtering - */ - fun updateList(search: String = "") { - recentMangaSubscription?.unsubscribe() - recentMangaSubscription = getRecentMangaObservable(search = search) - .subscribeLatestCache( - { view, mangas -> - view.onNextManga(mangas, true) - }, - HistoryController::onAddPageError, - ) - } - - /** - * Removes all chapters belonging to manga from history. - * @param mangaId id of manga - */ - fun removeAllFromHistory(mangaId: Long) { - db.getHistoryByMangaId(mangaId).asRxSingle() - .map { list -> - list.forEach { it.last_read = 0L } - db.updateHistoryLastRead(list).executeAsBlocking() - } - .subscribe() - } - - /** - * Retrieves the next chapter of the given one. - * - * @param chapter the chapter of the history object. - * @param manga the manga of the chapter. - */ - fun getNextChapter(chapter: Chapter, manga: Manga): Chapter? { - if (!chapter.read) { - return chapter - } - - val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { - Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } - Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } - Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) } - else -> throw NotImplementedError("Unknown sorting method") - } - - val chapters = db.getChapters(manga).executeAsBlocking() - .sortedWith { c1, c2 -> sortFunction(c1, c2) } - - val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id } - return when (manga.sorting) { - Manga.CHAPTER_SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1) - Manga.CHAPTER_SORTING_NUMBER -> { - val chapterNumber = chapter.chapter_number - - ((currChapterIndex + 1) until chapters.size) - .map { chapters[it] } - .firstOrNull { - it.chapter_number > chapterNumber && - it.chapter_number <= chapterNumber + 1 + presenterScope.launchIO { + _state.update { state -> + state.copy( + list = _query.flatMapLatest { query -> + getHistory.subscribe(query) + .map { pagingData -> + pagingData + .map { + UiModel.History(it) + } + .insertSeparators { before, after -> + val beforeDate = + before?.item?.history?.last_read?.toDateKey() + val afterDate = + after?.item?.history?.last_read?.toDateKey() + when { + beforeDate == null && afterDate != null -> UiModel.Header( + afterDate, + ) + beforeDate != null && afterDate != null -> UiModel.Header( + afterDate, + ) + // Return null to avoid adding a separator between two items. + else -> null + } + } + } } + .cachedIn(presenterScope), + ) } - Manga.CHAPTER_SORTING_UPLOAD_DATE -> { - chapters.drop(currChapterIndex + 1) - .firstOrNull { it.date_upload >= chapter.date_upload } + } + } + + fun search(query: String) { + presenterScope.launchIO { + _query.emit(query) + } + } + + fun removeFromHistory(history: History) { + presenterScope.launchIO { + removeHistoryById.await(history) + } + } + + fun removeAllFromHistory(mangaId: Long) { + presenterScope.launchIO { + removeHistoryByMangaId.await(mangaId) + } + } + + fun getNextChapterForManga(manga: Manga, chapter: Chapter) { + presenterScope.launchIO { + val chapter = getNextChapterForManga.await(manga, chapter) + view?.openChapter(chapter) + } + } + + fun deleteAllHistory() { + presenterScope.launchIO { + val result = deleteHistoryTable.await() + if (!result) return@launchIO + launchUI { + view?.activity?.toast(R.string.clear_history_completed) } - else -> throw NotImplementedError("Unknown sorting method") } } } + +sealed class UiModel { + data class History(val item: MangaChapterHistory) : UiModel() + data class Header(val date: Date) : UiModel() +} + +data class HistoryState( + val list: Flow>? = null, +) { + + companion object { + val EMPTY = HistoryState(null) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/RemoveHistoryDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/RemoveHistoryDialog.kt deleted file mode 100644 index 6243ed1d8..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/RemoveHistoryDialog.kt +++ /dev/null @@ -1,54 +0,0 @@ -package eu.kanade.tachiyomi.ui.recent.history - -import android.app.Dialog -import android.os.Bundle -import com.bluelinelabs.conductor.Controller -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.History -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.widget.DialogCheckboxView - -class RemoveHistoryDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : RemoveHistoryDialog.Listener { - - private var manga: Manga? = null - - private var history: History? = null - - constructor(target: T, manga: Manga, history: History) : this() { - this.manga = manga - this.history = history - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val activity = activity!! - - // Create custom view - val dialogCheckboxView = DialogCheckboxView(activity).apply { - setDescription(R.string.dialog_with_checkbox_remove_description) - setOptionDescription(R.string.dialog_with_checkbox_reset) - } - - return MaterialAlertDialogBuilder(activity) - .setTitle(R.string.action_remove) - .setView(dialogCheckboxView) - .setPositiveButton(R.string.action_remove) { _, _ -> onPositive(dialogCheckboxView.isChecked()) } - .setNegativeButton(android.R.string.cancel, null) - .create() - } - - private fun onPositive(checked: Boolean) { - val target = targetController as? Listener ?: return - val manga = manga ?: return - val history = history ?: return - - target.removeHistory(manga, history, checked) - } - - interface Listener { - fun removeHistory(manga: Manga, history: History, all: Boolean) - } -} diff --git a/app/src/main/res/layout/compose_controller.xml b/app/src/main/res/layout/compose_controller.xml new file mode 100644 index 000000000..617287296 --- /dev/null +++ b/app/src/main/res/layout/compose_controller.xml @@ -0,0 +1,4 @@ + + diff --git a/app/src/main/res/layout/history_controller.xml b/app/src/main/res/layout/history_controller.xml deleted file mode 100644 index d33aa20ed..000000000 --- a/app/src/main/res/layout/history_controller.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/history_item.xml b/app/src/main/res/layout/history_item.xml deleted file mode 100644 index ab407a01b..000000000 --- a/app/src/main/res/layout/history_item.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b4f4fac49..53d22ee6f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -702,6 +702,7 @@ Updating library + Ch. %1$s - Ch. %1$s - %2$s Clear history History deleted diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt b/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt index 75fe1320c..881533404 100644 --- a/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt +++ b/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt @@ -344,7 +344,7 @@ class BackupTest { private fun clearDatabase() { db.deleteMangas().executeAsBlocking() - db.deleteHistory().executeAsBlocking() + db.dropHistoryTable().executeAsBlocking() } private fun getSingleHistory(chapter: Chapter): DHistory { diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index b051c785b..6c49236bb 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -21,6 +21,9 @@ lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", ve work-runtime = "androidx.work:work-runtime-ktx:2.6.0" guava = "com.google.guava:guava:31.1-android" +paging-runtime = "androidx.paging:paging-runtime:3.1.1" +paging-compose = "androidx.paging:paging-compose:1.0.0-alpha14" + [bundles] lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"] workmanager = ["work-runtime", "guava"] diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml new file mode 100644 index 000000000..c7f1de71e --- /dev/null +++ b/gradle/compose.versions.toml @@ -0,0 +1,9 @@ +[versions] +compose = "1.2.0-alpha07" + +[libraries] +foundation = { module = "androidx.compose.foundation:foundation", version.ref="compose" } +material3-core = "androidx.compose.material3:material3:1.0.0-alpha09" +material3-adapter = "com.google.android.material:compose-theme-adapter-3:1.0.6" +animation = { module = "androidx.compose.animation:animation", version.ref="compose" } +ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref="compose" } \ No newline at end of file diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 268e66b5f..74448c71b 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin_version = "1.6.20" +kotlin_version = "1.6.10" coroutines_version = "1.6.1" serialization_version = "1.3.2" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 969ce5202..768e2af45 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,6 +49,7 @@ injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440" coil-core = { module = "io.coil-kt:coil", version.ref = "coil_version" } coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil_version" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil_version" } subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:846abe0" image-decoder = "com.github.tachiyomiorg:image-decoder:7481a4a" @@ -100,12 +101,12 @@ okhttp = ["okhttp-core","okhttp-logging","okhttp-dnsoverhttps"] js-engine = ["quickjs-android", "duktape-android"] sqlite = ["sqlitektx", "sqlite-android"] nucleus = ["nucleus-core","nucleus-supportv7"] -coil = ["coil-core","coil-gif",] +coil = ["coil-core","coil-gif","coil-compose"] flowbinding = ["flowbinding-android","flowbinding-appcompat","flowbinding-recyclerview","flowbinding-swiperefreshlayout","flowbinding-viewpager"] conductor = ["conductor-core","conductor-viewpager","conductor-support-preference"] shizuku = ["shizuku-api","shizuku-provider"] robolectric = ["robolectric-core","robolectric-playservices"] [plugins] -kotlinter = { id = "org.jmailen.kotlinter", version = "3.10.0"} +kotlinter = { id = "org.jmailen.kotlinter", version = "3.6.0"} versionsx = { id = "com.github.ben-manes.versions", version = "0.42.0"} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 8c3fdeac3..170ba7daf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,6 +22,9 @@ dependencyResolutionManagement { create("androidx") { from(files("gradle/androidx.versions.toml")) } + create("compose") { + from(files("gradle/compose.versions.toml")) + } } repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories {