mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-22 18:11:50 +01:00
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
This commit is contained in:
parent
7d50d7ff52
commit
c475acd1ea
@ -109,6 +109,7 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
|
compose = true
|
||||||
|
|
||||||
// Disable some unused things
|
// Disable some unused things
|
||||||
aidl = false
|
aidl = false
|
||||||
@ -122,6 +123,10 @@ android {
|
|||||||
checkReleaseBuilds = false
|
checkReleaseBuilds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = compose.versions.compose.get()
|
||||||
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
@ -133,6 +138,16 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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.reflect)
|
||||||
|
|
||||||
implementation(kotlinx.bundles.coroutines)
|
implementation(kotlinx.bundles.coroutines)
|
||||||
@ -262,6 +277,9 @@ tasks {
|
|||||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
|
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
|
||||||
|
"-Xopt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
|
"-Xopt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||||
|
"-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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<Int, MangaChapterHistory>() {
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, MangaChapterHistory>): 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<Int>): LoadResult.Page<Int, MangaChapterHistory> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
app/src/main/java/eu/kanade/domain/DomainModule.kt
Normal file
26
app/src/main/java/eu/kanade/domain/DomainModule.kt
Normal file
@ -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<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||||
|
addFactory { DeleteHistoryTable(get()) }
|
||||||
|
addFactory { GetHistory(get()) }
|
||||||
|
addFactory { GetNextChapterForManga(get()) }
|
||||||
|
addFactory { RemoveHistoryById(get()) }
|
||||||
|
addFactory { RemoveHistoryByMangaId(get()) }
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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<PagingData<MangaChapterHistory>> {
|
||||||
|
return Pager(
|
||||||
|
PagingConfig(pageSize = HistoryPagingSource.PAGE_SIZE)
|
||||||
|
) {
|
||||||
|
repository.getHistory(query)
|
||||||
|
}.flow
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<MangaChapterHistory>
|
||||||
|
|
||||||
|
suspend fun getNextChapterForManga(manga: Manga, chapter: Chapter): Chapter?
|
||||||
|
|
||||||
|
suspend fun resetHistory(history: History): Boolean
|
||||||
|
|
||||||
|
suspend fun resetHistoryByMangaId(mangaId: Long): Boolean
|
||||||
|
|
||||||
|
suspend fun deleteAllHistory(): Boolean
|
||||||
|
}
|
@ -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<EmptyView.Action>? = null,
|
||||||
|
) {
|
||||||
|
EmptyScreen(
|
||||||
|
message = stringResource(id = textResource),
|
||||||
|
actions = actions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmptyScreen(
|
||||||
|
message: String,
|
||||||
|
actions: List<EmptyView.Action>? = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -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<UiModel>,
|
||||||
|
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<MangaChapterHistory?>(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))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package eu.kanade.presentation.util
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
val horizontalPadding = 16.dp
|
@ -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
|
@ -24,6 +24,7 @@ import coil.decode.GifDecoder
|
|||||||
import coil.decode.ImageDecoderDecoder
|
import coil.decode.ImageDecoderDecoder
|
||||||
import coil.disk.DiskCache
|
import coil.disk.DiskCache
|
||||||
import coil.util.DebugLogger
|
import coil.util.DebugLogger
|
||||||
|
import eu.kanade.domain.DomainModule
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
||||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||||
@ -74,6 +75,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
|
Injekt.importModule(DomainModule())
|
||||||
|
|
||||||
setupAcra()
|
setupAcra()
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
|
@ -294,7 +294,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -168,7 +168,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,7 +5,7 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
|||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
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.resolvers.MangaChapterHistoryGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@ -64,9 +64,9 @@ interface HistoryQueries : DbProvider {
|
|||||||
* Inserts history object if not yet in database
|
* Inserts history object if not yet in database
|
||||||
* @param history history object
|
* @param history history object
|
||||||
*/
|
*/
|
||||||
fun updateHistoryLastRead(history: History) = db.put()
|
fun upsertHistoryLastRead(history: History) = db.put()
|
||||||
.`object`(history)
|
.`object`(history)
|
||||||
.withPutResolver(HistoryLastReadPutResolver())
|
.withPutResolver(HistoryUpsertResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,12 +74,40 @@ interface HistoryQueries : DbProvider {
|
|||||||
* Inserts history object if not yet in database
|
* Inserts history object if not yet in database
|
||||||
* @param historyList history object list
|
* @param historyList history object list
|
||||||
*/
|
*/
|
||||||
fun updateHistoryLastRead(historyList: List<History>) = db.put()
|
fun upsertHistoryLastRead(historyList: List<History>) = db.put()
|
||||||
.objects(historyList)
|
.objects(historyList)
|
||||||
.withPutResolver(HistoryLastReadPutResolver())
|
.withPutResolver(HistoryUpsertResolver())
|
||||||
.prepare()
|
.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<Long>) = 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(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(HistoryTable.TABLE)
|
.table(HistoryTable.TABLE)
|
||||||
|
@ -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.models.History
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
|
|
||||||
class HistoryLastReadPutResolver : HistoryPutResolver() {
|
class HistoryUpsertResolver : HistoryPutResolver() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates last_read time of chapter
|
* Updates last_read time of chapter
|
@ -97,14 +97,19 @@ import kotlin.math.max
|
|||||||
class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||||
|
|
||||||
companion object {
|
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 {
|
return Intent(context, ReaderActivity::class.java).apply {
|
||||||
putExtra("manga", manga.id)
|
putExtra("manga", mangaId)
|
||||||
putExtra("chapter", chapter.id)
|
putExtra("chapter", chapterId)
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
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 ENABLED_BUTTON_IMAGE_ALPHA = 255
|
||||||
private const val DISABLED_BUTTON_IMAGE_ALPHA = 64
|
private const val DISABLED_BUTTON_IMAGE_ALPHA = 64
|
||||||
|
|
||||||
|
@ -449,7 +449,7 @@ class ReaderPresenter(
|
|||||||
private fun saveChapterHistory(chapter: ReaderChapter) {
|
private fun saveChapterHistory(chapter: ReaderChapter) {
|
||||||
if (!incognitoMode) {
|
if (!incognitoMode) {
|
||||||
val history = History.create(chapter.chapter).apply { last_read = Date().time }
|
val history = History.create(chapter.chapter).apply { last_read = Date().time }
|
||||||
db.updateHistoryLastRead(history).asRxCompletable()
|
db.upsertHistoryLastRead(history).asRxCompletable()
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe()
|
.subscribe()
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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<IFlexible<*>>(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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,186 +1,56 @@
|
|||||||
package eu.kanade.tachiyomi.ui.recent.history
|
package eu.kanade.tachiyomi.ui.recent.history
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import eu.kanade.presentation.history.HistoryScreen
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
|
||||||
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.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
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.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
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.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
|
||||||
import kotlinx.coroutines.flow.drop
|
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import logcat.LogPriority
|
|
||||||
import reactivecircus.flowbinding.appcompat.queryTextChanges
|
import reactivecircus.flowbinding.appcompat.queryTextChanges
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment that shows recently read manga.
|
* Fragment that shows recently read manga.
|
||||||
*/
|
*/
|
||||||
class HistoryController :
|
class HistoryController :
|
||||||
NucleusController<HistoryControllerBinding, HistoryPresenter>(),
|
NucleusController<ComposeControllerBinding, HistoryPresenter>(),
|
||||||
RootController,
|
RootController {
|
||||||
FlexibleAdapter.OnUpdateListener,
|
|
||||||
FlexibleAdapter.EndlessScrollListener,
|
|
||||||
HistoryAdapter.OnRemoveClickListener,
|
|
||||||
HistoryAdapter.OnResumeClickListener,
|
|
||||||
HistoryAdapter.OnItemClickListener,
|
|
||||||
RemoveHistoryDialog.Listener {
|
|
||||||
|
|
||||||
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 = ""
|
private var query = ""
|
||||||
|
|
||||||
override fun getTitle(): String? {
|
override fun getTitle(): String? = resources?.getString(R.string.label_recent_manga)
|
||||||
return resources?.getString(R.string.label_recent_manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createPresenter(): HistoryPresenter {
|
override fun createPresenter(): HistoryPresenter = HistoryPresenter()
|
||||||
return HistoryPresenter()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createBinding(inflater: LayoutInflater) = HistoryControllerBinding.inflate(inflater)
|
override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding =
|
||||||
|
ComposeControllerBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onViewCreated(view: View) {
|
override fun onViewCreated(view: View) {
|
||||||
super.onViewCreated(view)
|
super.onViewCreated(view)
|
||||||
|
|
||||||
binding.recycler.applyInsetter {
|
binding.root.setContent {
|
||||||
type(navigationBars = true) {
|
HistoryScreen(
|
||||||
padding()
|
composeView = binding.root,
|
||||||
}
|
presenter = presenter,
|
||||||
}
|
onClickItem = { (manga, _, _) ->
|
||||||
|
|
||||||
// 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<HistoryItem>, 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())
|
router.pushController(MangaController(manga).withFadeTransaction())
|
||||||
}
|
},
|
||||||
|
onClickResume = { (manga, chapter, _) ->
|
||||||
override fun removeHistory(manga: Manga, history: History, all: Boolean) {
|
presenter.getNextChapterForManga(manga, chapter)
|
||||||
|
},
|
||||||
|
onClickDelete = { (manga, _, history), all ->
|
||||||
if (all) {
|
if (all) {
|
||||||
// Reset last read of chapter to 0L
|
// Reset last read of chapter to 0L
|
||||||
presenter.removeAllFromHistory(manga.id!!)
|
presenter.removeAllFromHistory(manga.id!!)
|
||||||
@ -188,6 +58,9 @@ class HistoryController :
|
|||||||
// Remove all chapters belonging to manga from library
|
// Remove all chapters belonging to manga from library
|
||||||
presenter.removeFromHistory(history)
|
presenter.removeFromHistory(history)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
@ -201,46 +74,33 @@ class HistoryController :
|
|||||||
searchView.clearFocus()
|
searchView.clearFocus()
|
||||||
}
|
}
|
||||||
searchView.queryTextChanges()
|
searchView.queryTextChanges()
|
||||||
.drop(1) // Drop first event after subscribed
|
|
||||||
.filter { router.backstack.lastOrNull()?.controller == this }
|
.filter { router.backstack.lastOrNull()?.controller == this }
|
||||||
.onEach {
|
.onEach {
|
||||||
query = it.toString()
|
query = it.toString()
|
||||||
presenter.updateList(query)
|
presenter.search(query)
|
||||||
}
|
}
|
||||||
.launchIn(viewScope)
|
.launchIn(viewScope)
|
||||||
|
|
||||||
// Fixes problem with the overflow icon showing up in lieu of search
|
|
||||||
searchItem.fixExpand(
|
|
||||||
onExpand = { invalidateMenuOnExpand() },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.action_clear_history -> {
|
R.id.action_clear_history -> {
|
||||||
val ctrl = ClearHistoryDialogController()
|
val dialog = ClearHistoryDialogController()
|
||||||
ctrl.targetController = this@HistoryController
|
dialog.targetController = this@HistoryController
|
||||||
ctrl.showDialog(router)
|
dialog.showDialog(router)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
fun openChapter(chapter: Chapter?) {
|
||||||
}
|
val activity = activity ?: return
|
||||||
|
if (chapter != null) {
|
||||||
class ClearHistoryDialogController : DialogController() {
|
val intent = ReaderActivity.newIntent(activity, chapter.manga_id, chapter.id)
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
startActivity(intent)
|
||||||
return MaterialAlertDialogBuilder(activity!!)
|
} else {
|
||||||
.setMessage(R.string.clear_history_confirmation)
|
activity.toast(R.string.no_next_chapter)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
(targetController as? HistoryController)?.clearHistory()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.create()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearHistory() {
|
|
||||||
db.deleteHistory().executeAsBlocking()
|
|
||||||
activity?.toast(R.string.clear_history_completed)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<HistoryHolder, DateSectionItem>(header) {
|
|
||||||
|
|
||||||
override fun getLayoutRes(): Int {
|
|
||||||
return R.layout.history_item
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): HistoryHolder {
|
|
||||||
return HistoryHolder(view, adapter as HistoryAdapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bindViewHolder(
|
|
||||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
|
||||||
holder: HistoryHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: List<Any?>?,
|
|
||||||
) {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,157 +1,135 @@
|
|||||||
package eu.kanade.tachiyomi.ui.recent.history
|
package eu.kanade.tachiyomi.ui.recent.history
|
||||||
|
|
||||||
import android.os.Bundle
|
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.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
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.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 eu.kanade.tachiyomi.util.lang.toDateKey
|
||||||
import rx.Observable
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import rx.Subscription
|
import kotlinx.coroutines.flow.Flow
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import uy.kohesive.injekt.injectLazy
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.text.DateFormat
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import java.util.Calendar
|
import kotlinx.coroutines.flow.map
|
||||||
import java.util.Date
|
import kotlinx.coroutines.flow.update
|
||||||
import java.util.TreeMap
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of HistoryFragment.
|
* Presenter of HistoryFragment.
|
||||||
* Contains information and data for fragment.
|
* Contains information and data for fragment.
|
||||||
* Observable updates should be called from here.
|
* Observable updates should be called from here.
|
||||||
*/
|
*/
|
||||||
class HistoryPresenter : BasePresenter<HistoryController>() {
|
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<HistoryController>() {
|
||||||
|
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private var _query: MutableStateFlow<String> = MutableStateFlow("")
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private var _state: MutableStateFlow<HistoryState> = MutableStateFlow(HistoryState.EMPTY)
|
||||||
|
val state: StateFlow<HistoryState> = _state
|
||||||
private val relativeTime: Int = preferences.relativeTime().get()
|
|
||||||
private val dateFormat: DateFormat = preferences.dateFormat()
|
|
||||||
|
|
||||||
private var recentMangaSubscription: Subscription? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
// Used to get a list of recently read manga
|
presenterScope.launchIO {
|
||||||
updateList()
|
_state.update { state ->
|
||||||
|
state.copy(
|
||||||
|
list = _query.flatMapLatest { query ->
|
||||||
|
getHistory.subscribe(query)
|
||||||
|
.map { pagingData ->
|
||||||
|
pagingData
|
||||||
|
.map {
|
||||||
|
UiModel.History(it)
|
||||||
}
|
}
|
||||||
|
.insertSeparators { before, after ->
|
||||||
fun requestNext(offset: Int, search: String = "") {
|
val beforeDate =
|
||||||
getRecentMangaObservable(offset = offset, search = search)
|
before?.item?.history?.last_read?.toDateKey()
|
||||||
.subscribeLatestCache(
|
val afterDate =
|
||||||
{ view, mangas ->
|
after?.item?.history?.last_read?.toDateKey()
|
||||||
view.onNextManga(mangas)
|
when {
|
||||||
},
|
beforeDate == null && afterDate != null -> UiModel.Header(
|
||||||
HistoryController::onAddPageError,
|
afterDate,
|
||||||
|
)
|
||||||
|
beforeDate != null && afterDate != null -> UiModel.Header(
|
||||||
|
afterDate,
|
||||||
|
)
|
||||||
|
// Return null to avoid adding a separator between two items.
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cachedIn(presenterScope),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Get recent manga observable
|
|
||||||
* @return list of history
|
|
||||||
*/
|
|
||||||
private fun getRecentMangaObservable(limit: Int = 25, offset: Int = 0, search: String = ""): Observable<List<HistoryItem>> {
|
|
||||||
// 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()
|
fun search(query: String) {
|
||||||
.map { recents ->
|
presenterScope.launchIO {
|
||||||
val map = TreeMap<Date, MutableList<MangaChapterHistory>> { d1, d2 -> d2.compareTo(d1) }
|
_query.emit(query)
|
||||||
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) {
|
fun removeFromHistory(history: History) {
|
||||||
history.last_read = 0L
|
presenterScope.launchIO {
|
||||||
db.updateHistoryLastRead(history).asRxObservable()
|
removeHistoryById.await(history)
|
||||||
.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) {
|
fun removeAllFromHistory(mangaId: Long) {
|
||||||
db.getHistoryByMangaId(mangaId).asRxSingle()
|
presenterScope.launchIO {
|
||||||
.map { list ->
|
removeHistoryByMangaId.await(mangaId)
|
||||||
list.forEach { it.last_read = 0L }
|
|
||||||
db.updateHistoryLastRead(list).executeAsBlocking()
|
|
||||||
}
|
}
|
||||||
.subscribe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun getNextChapterForManga(manga: Manga, chapter: Chapter) {
|
||||||
* Retrieves the next chapter of the given one.
|
presenterScope.launchIO {
|
||||||
*
|
val chapter = getNextChapterForManga.await(manga, chapter)
|
||||||
* @param chapter the chapter of the history object.
|
view?.openChapter(chapter)
|
||||||
* @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) {
|
fun deleteAllHistory() {
|
||||||
Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
presenterScope.launchIO {
|
||||||
Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
|
val result = deleteHistoryTable.await()
|
||||||
Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
|
if (!result) return@launchIO
|
||||||
else -> throw NotImplementedError("Unknown sorting method")
|
launchUI {
|
||||||
|
view?.activity?.toast(R.string.clear_history_completed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class UiModel {
|
||||||
|
data class History(val item: MangaChapterHistory) : UiModel()
|
||||||
|
data class Header(val date: Date) : UiModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
data class HistoryState(
|
||||||
.sortedWith { c1, c2 -> sortFunction(c1, c2) }
|
val list: Flow<PagingData<UiModel>>? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
|
companion object {
|
||||||
return when (manga.sorting) {
|
val EMPTY = HistoryState(null)
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<T>(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)
|
|
||||||
}
|
|
||||||
}
|
|
4
app/src/main/res/layout/compose_controller.xml
Normal file
4
app/src/main/res/layout/compose_controller.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.compose.ui.platform.ComposeView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recycler"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:paddingTop="4dp"
|
|
||||||
android:paddingBottom="@dimen/action_toolbar_list_padding"
|
|
||||||
tools:listitem="@layout/history_item" />
|
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.widget.MaterialFastScroll
|
|
||||||
android:id="@+id/fast_scroller"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
app:fastScrollerBubbleEnabled="false"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.widget.EmptyView
|
|
||||||
android:id="@+id/empty_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
@ -1,85 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/holder"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="96dp"
|
|
||||||
android:paddingStart="16dp"
|
|
||||||
android:paddingTop="8dp"
|
|
||||||
android:paddingEnd="8dp"
|
|
||||||
android:paddingBottom="8dp"
|
|
||||||
android:background="?attr/selectableItemBackground"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:id="@+id/cover"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:contentDescription="@string/description_cover"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintDimensionRatio="h,3:2"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Cover"
|
|
||||||
tools:src="@mipmap/ic_launcher" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/remove"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/cover"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="2"
|
|
||||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
|
||||||
tools:text="Title" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/manga_subtitle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="2dp"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
|
||||||
tools:text="Subtitle" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/remove"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/action_resume"
|
|
||||||
android:padding="8dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/resume"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:srcCompat="@drawable/ic_delete_24dp"
|
|
||||||
app:tint="?android:attr/textColorPrimary" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/resume"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/action_resume"
|
|
||||||
android:padding="8dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:srcCompat="@drawable/ic_play_arrow_24dp"
|
|
||||||
app:tint="?android:attr/textColorPrimary" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -702,6 +702,7 @@
|
|||||||
<string name="updating_library">Updating library</string>
|
<string name="updating_library">Updating library</string>
|
||||||
|
|
||||||
<!-- History fragment -->
|
<!-- History fragment -->
|
||||||
|
<string name="history_prefix">Ch. %1$s - </string>
|
||||||
<string name="recent_manga_time">Ch. %1$s - %2$s</string>
|
<string name="recent_manga_time">Ch. %1$s - %2$s</string>
|
||||||
<string name="pref_clear_history">Clear history</string>
|
<string name="pref_clear_history">Clear history</string>
|
||||||
<string name="clear_history_completed">History deleted</string>
|
<string name="clear_history_completed">History deleted</string>
|
||||||
|
@ -344,7 +344,7 @@ class BackupTest {
|
|||||||
|
|
||||||
private fun clearDatabase() {
|
private fun clearDatabase() {
|
||||||
db.deleteMangas().executeAsBlocking()
|
db.deleteMangas().executeAsBlocking()
|
||||||
db.deleteHistory().executeAsBlocking()
|
db.dropHistoryTable().executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSingleHistory(chapter: Chapter): DHistory {
|
private fun getSingleHistory(chapter: Chapter): DHistory {
|
||||||
|
@ -21,6 +21,9 @@ lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", ve
|
|||||||
work-runtime = "androidx.work:work-runtime-ktx:2.6.0"
|
work-runtime = "androidx.work:work-runtime-ktx:2.6.0"
|
||||||
guava = "com.google.guava:guava:31.1-android"
|
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]
|
[bundles]
|
||||||
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]
|
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]
|
||||||
workmanager = ["work-runtime", "guava"]
|
workmanager = ["work-runtime", "guava"]
|
||||||
|
9
gradle/compose.versions.toml
Normal file
9
gradle/compose.versions.toml
Normal file
@ -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" }
|
@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin_version = "1.6.20"
|
kotlin_version = "1.6.10"
|
||||||
coroutines_version = "1.6.1"
|
coroutines_version = "1.6.1"
|
||||||
serialization_version = "1.3.2"
|
serialization_version = "1.3.2"
|
||||||
|
|
||||||
|
@ -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-core = { module = "io.coil-kt:coil", version.ref = "coil_version" }
|
||||||
coil-gif = { module = "io.coil-kt:coil-gif", 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"
|
subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:846abe0"
|
||||||
image-decoder = "com.github.tachiyomiorg:image-decoder:7481a4a"
|
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"]
|
js-engine = ["quickjs-android", "duktape-android"]
|
||||||
sqlite = ["sqlitektx", "sqlite-android"]
|
sqlite = ["sqlitektx", "sqlite-android"]
|
||||||
nucleus = ["nucleus-core","nucleus-supportv7"]
|
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"]
|
flowbinding = ["flowbinding-android","flowbinding-appcompat","flowbinding-recyclerview","flowbinding-swiperefreshlayout","flowbinding-viewpager"]
|
||||||
conductor = ["conductor-core","conductor-viewpager","conductor-support-preference"]
|
conductor = ["conductor-core","conductor-viewpager","conductor-support-preference"]
|
||||||
shizuku = ["shizuku-api","shizuku-provider"]
|
shizuku = ["shizuku-api","shizuku-provider"]
|
||||||
robolectric = ["robolectric-core","robolectric-playservices"]
|
robolectric = ["robolectric-core","robolectric-playservices"]
|
||||||
|
|
||||||
[plugins]
|
[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"}
|
versionsx = { id = "com.github.ben-manes.versions", version = "0.42.0"}
|
@ -22,6 +22,9 @@ dependencyResolutionManagement {
|
|||||||
create("androidx") {
|
create("androidx") {
|
||||||
from(files("gradle/androidx.versions.toml"))
|
from(files("gradle/androidx.versions.toml"))
|
||||||
}
|
}
|
||||||
|
create("compose") {
|
||||||
|
from(files("gradle/compose.versions.toml"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
repositories {
|
repositories {
|
||||||
|
Loading…
Reference in New Issue
Block a user