Page previews for Exh/E-H and NH
- Still needs click image to open chapter
This commit is contained in:
parent
36461b52c0
commit
67e190bffd
@ -25,6 +25,7 @@ import eu.kanade.domain.manga.interactor.GetMergedManga
|
|||||||
import eu.kanade.domain.manga.interactor.GetMergedMangaById
|
import eu.kanade.domain.manga.interactor.GetMergedMangaById
|
||||||
import eu.kanade.domain.manga.interactor.GetMergedMangaForDownloading
|
import eu.kanade.domain.manga.interactor.GetMergedMangaForDownloading
|
||||||
import eu.kanade.domain.manga.interactor.GetMergedReferencesById
|
import eu.kanade.domain.manga.interactor.GetMergedReferencesById
|
||||||
|
import eu.kanade.domain.manga.interactor.GetPagePreviews
|
||||||
import eu.kanade.domain.manga.interactor.GetSearchMetadata
|
import eu.kanade.domain.manga.interactor.GetSearchMetadata
|
||||||
import eu.kanade.domain.manga.interactor.GetSearchTags
|
import eu.kanade.domain.manga.interactor.GetSearchTags
|
||||||
import eu.kanade.domain.manga.interactor.GetSearchTitles
|
import eu.kanade.domain.manga.interactor.GetSearchTitles
|
||||||
@ -97,6 +98,7 @@ class SYDomainModule : InjektModule {
|
|||||||
addFactory { CreateSortTag(get(), get()) }
|
addFactory { CreateSortTag(get(), get()) }
|
||||||
addFactory { DeleteSortTag(get(), get()) }
|
addFactory { DeleteSortTag(get(), get()) }
|
||||||
addFactory { ReorderSortTag(get(), get()) }
|
addFactory { ReorderSortTag(get(), get()) }
|
||||||
|
addFactory { GetPagePreviews(get()) }
|
||||||
|
|
||||||
addSingletonFactory<MangaMetadataRepository> { MangaMetadataRepositoryImpl(get()) }
|
addSingletonFactory<MangaMetadataRepository> { MangaMetadataRepositoryImpl(get()) }
|
||||||
addFactory { GetFlatMetadataById(get()) }
|
addFactory { GetFlatMetadataById(get()) }
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
package eu.kanade.domain.manga.interactor
|
||||||
|
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
import eu.kanade.domain.manga.model.PagePreview
|
||||||
|
import eu.kanade.domain.manga.model.toMangaInfo
|
||||||
|
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewSource
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import exh.source.getMainSource
|
||||||
|
|
||||||
|
class GetPagePreviews(
|
||||||
|
private val pagePreviewCache: PagePreviewCache,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun await(manga: Manga, source: Source, page: Int): Result {
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val source = source.getMainSource<PagePreviewSource>() ?: return Result.Unused
|
||||||
|
return try {
|
||||||
|
val pagePreviews = try {
|
||||||
|
pagePreviewCache.getPageListFromCache(manga, page)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
source.getPagePreviewList(manga.toMangaInfo(), page).also {
|
||||||
|
pagePreviewCache.putPageListToCache(manga, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Result.Success(
|
||||||
|
pagePreviews.pagePreviews.map {
|
||||||
|
PagePreview(it.index, it.imageUrl, source.id)
|
||||||
|
},
|
||||||
|
pagePreviews.hasNextPage,
|
||||||
|
pagePreviews.pagePreviewPages,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Result {
|
||||||
|
object Unused : Result()
|
||||||
|
data class Success(
|
||||||
|
val pagePreviews: List<PagePreview>,
|
||||||
|
val hasNextPage: Boolean,
|
||||||
|
val pageCount: Int?,
|
||||||
|
) : Result()
|
||||||
|
data class Error(val error: Throwable) : Result()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package eu.kanade.domain.manga.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewInfo
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PagePreview(
|
||||||
|
val index: Int,
|
||||||
|
val imageUrl: String,
|
||||||
|
val source: Long,
|
||||||
|
) {
|
||||||
|
@Transient
|
||||||
|
private val _progress: MutableStateFlow<Int> = MutableStateFlow(-1)
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val progress = _progress.asStateFlow()
|
||||||
|
|
||||||
|
fun getPagePreviewInfo() = PagePreviewInfo(index, imageUrl, _progress)
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import androidx.compose.ui.util.fastMap
|
||||||
|
import androidx.compose.ui.util.fastMaxBy
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AroundLayout(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
startLayout: @Composable () -> Unit,
|
||||||
|
endLayout: @Composable () -> Unit,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
SubcomposeLayout(modifier) { constraints ->
|
||||||
|
val layoutWidth = constraints.maxWidth
|
||||||
|
|
||||||
|
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||||
|
|
||||||
|
val startLayoutPlaceables = subcompose(AroundLayoutContent.Start, startLayout).fastMap {
|
||||||
|
it.measure(looseConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
val startLayoutWidth = startLayoutPlaceables.fastMaxBy { it.width }?.width ?: 0
|
||||||
|
|
||||||
|
val endLayoutPlaceables = subcompose(AroundLayoutContent.End, endLayout).fastMap {
|
||||||
|
it.measure(looseConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
val endLayoutWidth = endLayoutPlaceables.fastMaxBy { it.width }?.width ?: 0
|
||||||
|
|
||||||
|
val bodyContentWidth = layoutWidth - startLayoutWidth
|
||||||
|
|
||||||
|
val bodyContentPlaceables = subcompose(AroundLayoutContent.MainContent) {
|
||||||
|
Box(Modifier.padding(end = endLayoutWidth.toDp())) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}.fastMap { it.measure(looseConstraints.copy(maxWidth = bodyContentWidth)) }
|
||||||
|
|
||||||
|
val height = (startLayoutPlaceables + endLayoutPlaceables + bodyContentPlaceables).maxOfOrNull { it.height } ?: 0
|
||||||
|
|
||||||
|
layout(constraints.maxWidth, height) {
|
||||||
|
// Placing to control drawing order to match default elevation of each placeable
|
||||||
|
|
||||||
|
bodyContentPlaceables.fastForEach {
|
||||||
|
it.place(startLayoutWidth, 0)
|
||||||
|
}
|
||||||
|
startLayoutPlaceables.fastForEach {
|
||||||
|
it.place(0, 0)
|
||||||
|
}
|
||||||
|
// The bottom bar is always at the bottom of the layout
|
||||||
|
endLayoutPlaceables.fastForEach {
|
||||||
|
it.place(layoutWidth - endLayoutWidth, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class AroundLayoutContent { Start, MainContent, End }
|
@ -63,6 +63,7 @@ import eu.kanade.presentation.manga.components.MangaChapterListItem
|
|||||||
import eu.kanade.presentation.manga.components.MangaInfoBox
|
import eu.kanade.presentation.manga.components.MangaInfoBox
|
||||||
import eu.kanade.presentation.manga.components.MangaInfoButtons
|
import eu.kanade.presentation.manga.components.MangaInfoButtons
|
||||||
import eu.kanade.presentation.manga.components.MangaSmallAppBar
|
import eu.kanade.presentation.manga.components.MangaSmallAppBar
|
||||||
|
import eu.kanade.presentation.manga.components.PagePreviews
|
||||||
import eu.kanade.presentation.manga.components.SearchMetadataChips
|
import eu.kanade.presentation.manga.components.SearchMetadataChips
|
||||||
import eu.kanade.presentation.util.isScrolledToEnd
|
import eu.kanade.presentation.util.isScrolledToEnd
|
||||||
import eu.kanade.presentation.util.isScrollingUp
|
import eu.kanade.presentation.util.isScrollingUp
|
||||||
@ -74,6 +75,7 @@ import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
|||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.ChapterItem
|
import eu.kanade.tachiyomi.ui.manga.ChapterItem
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.PagePreviewState
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import exh.source.getMainSource
|
import exh.source.getMainSource
|
||||||
|
|
||||||
@ -108,6 +110,7 @@ fun MangaScreen(
|
|||||||
onMergedSettingsClicked: () -> Unit,
|
onMergedSettingsClicked: () -> Unit,
|
||||||
onMergeClicked: () -> Unit,
|
onMergeClicked: () -> Unit,
|
||||||
onMergeWithAnotherClicked: () -> Unit,
|
onMergeWithAnotherClicked: () -> Unit,
|
||||||
|
onMorePreviewsClicked: () -> Unit,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||||
@ -141,6 +144,7 @@ fun MangaScreen(
|
|||||||
onMergedSettingsClicked = onMergedSettingsClicked,
|
onMergedSettingsClicked = onMergedSettingsClicked,
|
||||||
onMergeClicked = onMergeClicked,
|
onMergeClicked = onMergeClicked,
|
||||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
||||||
|
onMorePreviewsClicked = onMorePreviewsClicked,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||||
@ -173,6 +177,7 @@ fun MangaScreen(
|
|||||||
onMergedSettingsClicked = onMergedSettingsClicked,
|
onMergedSettingsClicked = onMergedSettingsClicked,
|
||||||
onMergeClicked = onMergeClicked,
|
onMergeClicked = onMergeClicked,
|
||||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
||||||
|
onMorePreviewsClicked = onMorePreviewsClicked,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||||
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
|
||||||
@ -211,6 +216,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
onMergedSettingsClicked: () -> Unit,
|
onMergedSettingsClicked: () -> Unit,
|
||||||
onMergeClicked: () -> Unit,
|
onMergeClicked: () -> Unit,
|
||||||
onMergeWithAnotherClicked: () -> Unit,
|
onMergeWithAnotherClicked: () -> Unit,
|
||||||
|
onMorePreviewsClicked: () -> Unit,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||||
@ -432,6 +438,15 @@ private fun MangaScreenSmallImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.pagePreviewsState !is PagePreviewState.Unused) {
|
||||||
|
item(
|
||||||
|
key = MangaScreenItem.CHAPTER_PREVIEW,
|
||||||
|
contentType = MangaScreenItem.CHAPTER_PREVIEW,
|
||||||
|
) {
|
||||||
|
PagePreviews(state.pagePreviewsState, onMorePreviewsClicked)
|
||||||
|
}
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
item(
|
item(
|
||||||
@ -489,6 +504,7 @@ fun MangaScreenLargeImpl(
|
|||||||
onMergedSettingsClicked: () -> Unit,
|
onMergedSettingsClicked: () -> Unit,
|
||||||
onMergeClicked: () -> Unit,
|
onMergeClicked: () -> Unit,
|
||||||
onMergeWithAnotherClicked: () -> Unit,
|
onMergeWithAnotherClicked: () -> Unit,
|
||||||
|
onMorePreviewsClicked: () -> Unit,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
|
||||||
@ -670,6 +686,9 @@ fun MangaScreenLargeImpl(
|
|||||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (state.pagePreviewsState !is PagePreviewState.Unused) {
|
||||||
|
PagePreviews(state.pagePreviewsState, onMorePreviewsClicked)
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ enum class MangaScreenItem {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
INFO_BUTTONS,
|
INFO_BUTTONS,
|
||||||
|
CHAPTER_PREVIEW,
|
||||||
|
|
||||||
// SY <--
|
// SY <--
|
||||||
CHAPTER_HEADER,
|
CHAPTER_HEADER,
|
||||||
|
@ -0,0 +1,103 @@
|
|||||||
|
package eu.kanade.presentation.manga.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
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.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.SubcomposeAsyncImage
|
||||||
|
import coil.compose.SubcomposeAsyncImageContent
|
||||||
|
import eu.kanade.domain.manga.model.PagePreview
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.PagePreviewState
|
||||||
|
import exh.util.floor
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PagePreviews(pagePreviewState: PagePreviewState, onMorePreviewsClicked: () -> Unit) {
|
||||||
|
when (pagePreviewState) {
|
||||||
|
PagePreviewState.Loading -> {
|
||||||
|
Box(modifier = Modifier.height(60.dp).fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is PagePreviewState.Success -> {
|
||||||
|
BoxWithConstraints(Modifier.fillMaxWidth()) {
|
||||||
|
val itemPerRowCount = (maxWidth / 120.dp).floor()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
pagePreviewState.pagePreviews.take(4 * itemPerRowCount).chunked(itemPerRowCount).forEach {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
it.forEach { page ->
|
||||||
|
PagePreview(
|
||||||
|
modifier = Modifier.weight(1F),
|
||||||
|
page = page,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextButton(onClick = onMorePreviewsClicked) {
|
||||||
|
Text(stringResource(R.string.more_previews))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PagePreview(
|
||||||
|
modifier: Modifier,
|
||||||
|
page: PagePreview,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
SubcomposeAsyncImage(
|
||||||
|
model = page,
|
||||||
|
contentDescription = null,
|
||||||
|
loading = {
|
||||||
|
val progress by page.progress.collectAsState()
|
||||||
|
if (progress != -1) {
|
||||||
|
CircularProgressIndicator(progress / 0.01F)
|
||||||
|
} else {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
success = {
|
||||||
|
SubcomposeAsyncImageContent(
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(120.dp)
|
||||||
|
.heightIn(max = 200.dp),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Text(page.index.toString())
|
||||||
|
}
|
||||||
|
}
|
@ -44,6 +44,8 @@ import eu.kanade.tachiyomi.data.coil.DomainMangaKeyer
|
|||||||
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.MangaKeyer
|
import eu.kanade.tachiyomi.data.coil.MangaKeyer
|
||||||
|
import eu.kanade.tachiyomi.data.coil.PagePreviewFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.coil.PagePreviewKeyer
|
||||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
@ -178,6 +180,10 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
add(MangaKeyer())
|
add(MangaKeyer())
|
||||||
add(DomainMangaKeyer())
|
add(DomainMangaKeyer())
|
||||||
add(MangaCoverKeyer())
|
add(MangaCoverKeyer())
|
||||||
|
// SY -->
|
||||||
|
add(PagePreviewKeyer())
|
||||||
|
add(PagePreviewFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
callFactory(callFactoryInit)
|
callFactory(callFactoryInit)
|
||||||
diskCache(diskCacheInit)
|
diskCache(diskCacheInit)
|
||||||
|
@ -18,6 +18,7 @@ import eu.kanade.data.listOfStringsAdapter
|
|||||||
import eu.kanade.data.listOfStringsAndAdapter
|
import eu.kanade.data.listOfStringsAndAdapter
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@ -116,6 +117,8 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
addSingletonFactory { CustomMangaManager(app) }
|
addSingletonFactory { CustomMangaManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { EHentaiUpdateHelper(app) }
|
addSingletonFactory { EHentaiUpdateHelper(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { PagePreviewCache(app) }
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
|
217
app/src/main/java/eu/kanade/tachiyomi/data/cache/PagePreviewCache.kt
vendored
Normal file
217
app/src/main/java/eu/kanade/tachiyomi/data/cache/PagePreviewCache.kt
vendored
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import com.jakewharton.disklrucache.DiskLruCache
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewPage
|
||||||
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Response
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to create page preview cache
|
||||||
|
* For each page in a page preview list a file is created
|
||||||
|
* For each page preview page a Json list is created and converted to a file.
|
||||||
|
* The files are in format *md5key*.0
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @constructor creates an instance of the page preview cache.
|
||||||
|
*/
|
||||||
|
class PagePreviewCache(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Name of cache directory. */
|
||||||
|
const val PARAMETER_CACHE_DIRECTORY = "page_preview_disk_cache"
|
||||||
|
|
||||||
|
/** Application cache version. */
|
||||||
|
const val PARAMETER_APP_VERSION = 1
|
||||||
|
|
||||||
|
/** The number of values per cache entry. Must be positive. */
|
||||||
|
const val PARAMETER_VALUE_COUNT = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Google Json class used for parsing JSON files. */
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
/** Cache class used for cache management. */
|
||||||
|
private var diskCache = setupDiskCache(75)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns directory of cache.
|
||||||
|
*/
|
||||||
|
private val cacheDir: File
|
||||||
|
get() = diskCache.directory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns real size of directory.
|
||||||
|
*/
|
||||||
|
private val realSize: Long
|
||||||
|
get() = DiskUtil.getDirectorySize(cacheDir)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns real size of directory in human readable format.
|
||||||
|
*/
|
||||||
|
val readableSize: String
|
||||||
|
get() = Formatter.formatFileSize(context, realSize)
|
||||||
|
|
||||||
|
// --> EH
|
||||||
|
// Cache size is in MB
|
||||||
|
private fun setupDiskCache(cacheSize: Long): DiskLruCache {
|
||||||
|
return DiskLruCache.open(
|
||||||
|
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||||
|
PARAMETER_APP_VERSION,
|
||||||
|
PARAMETER_VALUE_COUNT,
|
||||||
|
cacheSize * 1024 * 1024,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// <-- EH
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get page list from cache.
|
||||||
|
*
|
||||||
|
* @param manga the manga.
|
||||||
|
* @return the list of pages.
|
||||||
|
*/
|
||||||
|
fun getPageListFromCache(manga: Manga, page: Int): PagePreviewPage {
|
||||||
|
// Get the key for the manga.
|
||||||
|
val key = DiskUtil.hashKeyForDisk(getKey(manga, page))
|
||||||
|
|
||||||
|
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||||
|
return diskCache.get(key).use {
|
||||||
|
json.decodeFromString(it.getString(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add page list to disk cache.
|
||||||
|
*
|
||||||
|
* @param manga the manga.
|
||||||
|
* @param pages list of pages.
|
||||||
|
*/
|
||||||
|
fun putPageListToCache(manga: Manga, pages: PagePreviewPage) {
|
||||||
|
// Convert list of pages to json string.
|
||||||
|
val cachedValue = json.encodeToString(pages)
|
||||||
|
|
||||||
|
// Initialize the editor (edits the values for an entry).
|
||||||
|
var editor: DiskLruCache.Editor? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get editor from md5 key.
|
||||||
|
val key = DiskUtil.hashKeyForDisk(getKey(manga, pages.page))
|
||||||
|
editor = diskCache.edit(key) ?: return
|
||||||
|
|
||||||
|
// Write page preview urls to cache.
|
||||||
|
editor.newOutputStream(0).sink().buffer().use {
|
||||||
|
it.write(cachedValue.toByteArray())
|
||||||
|
it.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
diskCache.flush()
|
||||||
|
editor.commit()
|
||||||
|
editor.abortUnlessCommitted()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Ignore.
|
||||||
|
} finally {
|
||||||
|
editor?.abortUnlessCommitted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if page is in cache.
|
||||||
|
*
|
||||||
|
* @param imageUrl url of page.
|
||||||
|
* @return true if in cache otherwise false.
|
||||||
|
*/
|
||||||
|
fun isImageInCache(imageUrl: String): Boolean {
|
||||||
|
return try {
|
||||||
|
diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null
|
||||||
|
} catch (e: IOException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get page file from url.
|
||||||
|
*
|
||||||
|
* @param imageUrl url of page.
|
||||||
|
* @return path of page.
|
||||||
|
*/
|
||||||
|
fun getImageFile(imageUrl: String): File {
|
||||||
|
// Get file from md5 key.
|
||||||
|
val imageName = DiskUtil.hashKeyForDisk(imageUrl) + ".0"
|
||||||
|
return File(diskCache.directory, imageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add page to cache.
|
||||||
|
*
|
||||||
|
* @param imageUrl url of page.
|
||||||
|
* @param response http response from page.
|
||||||
|
* @throws IOException page error.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun putImageToCache(imageUrl: String, response: Response) {
|
||||||
|
// Initialize editor (edits the values for an entry).
|
||||||
|
var editor: DiskLruCache.Editor? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get editor from md5 key.
|
||||||
|
val key = DiskUtil.hashKeyForDisk(imageUrl)
|
||||||
|
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
||||||
|
|
||||||
|
// Get OutputStream and write page with Okio.
|
||||||
|
response.body!!.source().saveTo(editor.newOutputStream(0))
|
||||||
|
|
||||||
|
diskCache.flush()
|
||||||
|
editor.commit()
|
||||||
|
} finally {
|
||||||
|
response.body?.close()
|
||||||
|
editor?.abortUnlessCommitted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear(): Int {
|
||||||
|
var deletedFiles = 0
|
||||||
|
cacheDir.listFiles()?.forEach {
|
||||||
|
if (removeFileFromCache(it.name)) {
|
||||||
|
deletedFiles++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deletedFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove file from cache.
|
||||||
|
*
|
||||||
|
* @param file name of file "md5.0".
|
||||||
|
* @return status of deletion for the file.
|
||||||
|
*/
|
||||||
|
private fun removeFileFromCache(file: String): Boolean {
|
||||||
|
// Make sure we don't delete the journal file (keeps track of cache).
|
||||||
|
if (file == "journal" || file.startsWith("journal.")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
// Remove the extension from the file to get the key of the cache
|
||||||
|
val key = file.substringBeforeLast(".")
|
||||||
|
// Remove file from cache.
|
||||||
|
diskCache.remove(key)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getKey(manga: Manga, page: Int): String {
|
||||||
|
return "${manga.id}_$page"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,268 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.decode.DataSource
|
||||||
|
import coil.decode.ImageSource
|
||||||
|
import coil.disk.DiskCache
|
||||||
|
import coil.fetch.FetchResult
|
||||||
|
import coil.fetch.Fetcher
|
||||||
|
import coil.fetch.SourceResult
|
||||||
|
import coil.network.HttpException
|
||||||
|
import coil.request.Options
|
||||||
|
import coil.request.Parameters
|
||||||
|
import eu.kanade.domain.manga.model.PagePreview
|
||||||
|
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewSource
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.internal.closeQuietly
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
|
import okio.Source
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Fetcher] that fetches page preview image for [PagePreview] object.
|
||||||
|
*
|
||||||
|
* Disk caching is handled by [PagePreviewCache], otherwise
|
||||||
|
* handled by Coil's [DiskCache].
|
||||||
|
*/
|
||||||
|
class PagePreviewFetcher(
|
||||||
|
private val page: PagePreview,
|
||||||
|
private val options: Options,
|
||||||
|
private val pagePreviewFileLazy: Lazy<File>,
|
||||||
|
private val diskCacheKeyLazy: Lazy<String>,
|
||||||
|
private val sourceLazy: Lazy<PagePreviewSource?>,
|
||||||
|
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||||
|
private val diskCacheLazy: Lazy<DiskCache>,
|
||||||
|
) : Fetcher {
|
||||||
|
|
||||||
|
private val diskCacheKey: String
|
||||||
|
get() = diskCacheKeyLazy.value
|
||||||
|
|
||||||
|
override suspend fun fetch(): FetchResult {
|
||||||
|
return httpLoader()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileLoader(file: File): FetchResult {
|
||||||
|
return SourceResult(
|
||||||
|
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = DataSource.DISK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun httpLoader(): FetchResult {
|
||||||
|
if (pagePreviewFileLazy.value.exists() && options.diskCachePolicy.readEnabled) {
|
||||||
|
return fileLoader(pagePreviewFileLazy.value)
|
||||||
|
}
|
||||||
|
var snapshot = readFromDiskCache()
|
||||||
|
try {
|
||||||
|
// Fetch from disk cache
|
||||||
|
if (snapshot != null) {
|
||||||
|
val snapshotPagePreviewCache = moveSnapshotToPagePreviewCache(snapshot, pagePreviewFileLazy.value)
|
||||||
|
if (snapshotPagePreviewCache != null) {
|
||||||
|
// Read from page preview cache
|
||||||
|
return fileLoader(snapshotPagePreviewCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from snapshot
|
||||||
|
return SourceResult(
|
||||||
|
source = snapshot.toImageSource(),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = DataSource.DISK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from network
|
||||||
|
val response = executeNetworkRequest()
|
||||||
|
val responseBody = checkNotNull(response.body) { "Null response source" }
|
||||||
|
try {
|
||||||
|
// Read from page preview cache after page preview updated
|
||||||
|
val responsePagePreviewCache = writeResponseToPagePreviewCache(response, pagePreviewFileLazy.value)
|
||||||
|
if (responsePagePreviewCache != null) {
|
||||||
|
return fileLoader(responsePagePreviewCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from disk cache
|
||||||
|
snapshot = writeToDiskCache(snapshot, response)
|
||||||
|
if (snapshot != null) {
|
||||||
|
return SourceResult(
|
||||||
|
source = snapshot.toImageSource(),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = DataSource.NETWORK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from response if cache is unused or unusable
|
||||||
|
return SourceResult(
|
||||||
|
source = ImageSource(source = responseBody.source(), context = options.context),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
responseBody.closeQuietly()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
snapshot?.closeQuietly()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun executeNetworkRequest(): Response {
|
||||||
|
val response = sourceLazy.value?.fetchPreviewImage(page.getPagePreviewInfo(), getCacheControl()) ?: callFactoryLazy.value.newCall(newRequest()).await()
|
||||||
|
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||||
|
response.body?.closeQuietly()
|
||||||
|
throw HttpException(response)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCacheControl(): CacheControl? {
|
||||||
|
val diskRead = options.diskCachePolicy.readEnabled
|
||||||
|
val networkRead = options.networkCachePolicy.readEnabled
|
||||||
|
return when {
|
||||||
|
!networkRead && diskRead -> {
|
||||||
|
CacheControl.FORCE_CACHE
|
||||||
|
}
|
||||||
|
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
||||||
|
CacheControl.FORCE_NETWORK
|
||||||
|
} else {
|
||||||
|
CACHE_CONTROL_FORCE_NETWORK_NO_CACHE
|
||||||
|
}
|
||||||
|
!networkRead && !diskRead -> {
|
||||||
|
// This causes the request to fail with a 504 Unsatisfiable Request.
|
||||||
|
CACHE_CONTROL_NO_NETWORK_NO_CACHE
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newRequest(): Request {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(page.imageUrl)
|
||||||
|
.headers((sourceLazy.value as? HttpSource)?.headers ?: options.headers)
|
||||||
|
// Support attaching custom data to the network request.
|
||||||
|
.tag(Parameters::class.java, options.parameters)
|
||||||
|
|
||||||
|
val cacheControl = getCacheControl()
|
||||||
|
if (cacheControl != null) {
|
||||||
|
request.cacheControl(cacheControl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveSnapshotToPagePreviewCache(snapshot: DiskCache.Snapshot, cacheFile: File): File? {
|
||||||
|
return try {
|
||||||
|
diskCacheLazy.value.run {
|
||||||
|
fileSystem.source(snapshot.data).use { input ->
|
||||||
|
writeSourceToPagePreviewCache(input, cacheFile)
|
||||||
|
}
|
||||||
|
remove(diskCacheKey)
|
||||||
|
}
|
||||||
|
cacheFile.takeIf { it.exists() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to write snapshot data to page preview cache ${cacheFile.name}" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeResponseToPagePreviewCache(response: Response, cacheFile: File): File? {
|
||||||
|
if (!options.diskCachePolicy.writeEnabled) return null
|
||||||
|
return try {
|
||||||
|
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||||
|
writeSourceToPagePreviewCache(input, cacheFile)
|
||||||
|
}
|
||||||
|
cacheFile.takeIf { it.exists() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to write response data to page preview cache ${cacheFile.name}" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeSourceToPagePreviewCache(input: Source, cacheFile: File) {
|
||||||
|
cacheFile.parentFile?.mkdirs()
|
||||||
|
cacheFile.delete()
|
||||||
|
try {
|
||||||
|
cacheFile.sink().buffer().use { output ->
|
||||||
|
output.writeAll(input)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
cacheFile.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readFromDiskCache(): DiskCache.Snapshot? {
|
||||||
|
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey] else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeToDiskCache(
|
||||||
|
snapshot: DiskCache.Snapshot?,
|
||||||
|
response: Response,
|
||||||
|
): DiskCache.Snapshot? {
|
||||||
|
if (!options.diskCachePolicy.writeEnabled) {
|
||||||
|
snapshot?.closeQuietly()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val editor = if (snapshot != null) {
|
||||||
|
snapshot.closeAndEdit()
|
||||||
|
} else {
|
||||||
|
diskCacheLazy.value.edit(diskCacheKey)
|
||||||
|
} ?: return null
|
||||||
|
try {
|
||||||
|
diskCacheLazy.value.fileSystem.write(editor.data) {
|
||||||
|
response.body!!.source().readAll(this)
|
||||||
|
}
|
||||||
|
return editor.commitAndGet()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
try {
|
||||||
|
editor.abort()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
|
||||||
|
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(
|
||||||
|
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||||
|
private val diskCacheLazy: Lazy<DiskCache>,
|
||||||
|
) : Fetcher.Factory<PagePreview> {
|
||||||
|
|
||||||
|
private val pagePreviewCache: PagePreviewCache by injectLazy()
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
override fun create(data: PagePreview, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||||
|
return PagePreviewFetcher(
|
||||||
|
page = data,
|
||||||
|
options = options,
|
||||||
|
pagePreviewFileLazy = lazy { pagePreviewCache.getImageFile(data.imageUrl) },
|
||||||
|
diskCacheKeyLazy = lazy { PagePreviewKeyer().key(data, options) },
|
||||||
|
sourceLazy = lazy { sourceManager.get(data.source) as? PagePreviewSource },
|
||||||
|
callFactoryLazy = callFactoryLazy,
|
||||||
|
diskCacheLazy = diskCacheLazy,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
|
||||||
|
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import coil.key.Keyer
|
||||||
|
import coil.request.Options
|
||||||
|
import eu.kanade.domain.manga.model.PagePreview
|
||||||
|
|
||||||
|
class PagePreviewKeyer : Keyer<PagePreview> {
|
||||||
|
override fun key(data: PagePreview, options: Options): String {
|
||||||
|
return data.imageUrl
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.ProgressListener
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.Response
|
||||||
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
|
||||||
|
interface PagePreviewSource : Source {
|
||||||
|
|
||||||
|
suspend fun getPagePreviewList(manga: MangaInfo, page: Int): PagePreviewPage
|
||||||
|
|
||||||
|
suspend fun fetchPreviewImage(page: PagePreviewInfo, cacheControl: CacheControl? = null): Response
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PagePreviewPage(
|
||||||
|
val page: Int,
|
||||||
|
val pagePreviews: List<PagePreviewInfo>,
|
||||||
|
val hasNextPage: Boolean,
|
||||||
|
val pagePreviewPages: Int?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PagePreviewInfo(
|
||||||
|
val index: Int,
|
||||||
|
val imageUrl: String,
|
||||||
|
@Transient
|
||||||
|
private val _progress: MutableStateFlow<Int> = MutableStateFlow(-1),
|
||||||
|
) : ProgressListener {
|
||||||
|
@Transient
|
||||||
|
val progress = _progress.asStateFlow()
|
||||||
|
|
||||||
|
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||||
|
_progress.value = if (contentLength > 0) {
|
||||||
|
(100 * bytesRead / contentLength).toInt()
|
||||||
|
} else {
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.all
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@ -10,6 +12,10 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewInfo
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewPage
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
@ -72,11 +78,16 @@ import kotlinx.serialization.json.put
|
|||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.CookieJar
|
import okhttp3.CookieJar
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.nodes.TextNode
|
import org.jsoup.nodes.TextNode
|
||||||
@ -84,6 +95,8 @@ import rx.Observable
|
|||||||
import tachiyomi.source.model.ChapterInfo
|
import tachiyomi.source.model.ChapterInfo
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
// TODO Consider gallery updating when doing tabbed browsing
|
// TODO Consider gallery updating when doing tabbed browsing
|
||||||
@ -94,7 +107,8 @@ class EHentai(
|
|||||||
) : HttpSource(),
|
) : HttpSource(),
|
||||||
MetadataSource<EHentaiSearchMetadata, Document>,
|
MetadataSource<EHentaiSearchMetadata, Document>,
|
||||||
UrlImportableSource,
|
UrlImportableSource,
|
||||||
NamespaceSource {
|
NamespaceSource,
|
||||||
|
PagePreviewSource {
|
||||||
override val metaClass = EHentaiSearchMetadata::class
|
override val metaClass = EHentaiSearchMetadata::class
|
||||||
|
|
||||||
private val domain: String
|
private val domain: String
|
||||||
@ -498,7 +512,7 @@ class EHentai(
|
|||||||
override fun searchMangaParse(response: Response) = genericMangaParse(response)
|
override fun searchMangaParse(response: Response) = genericMangaParse(response)
|
||||||
override fun latestUpdatesParse(response: Response) = genericMangaParse(response)
|
override fun latestUpdatesParse(response: Response) = genericMangaParse(response)
|
||||||
|
|
||||||
private fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true): Request {
|
private fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cacheControl: CacheControl? = null): Request {
|
||||||
return GET(
|
return GET(
|
||||||
if (page != null) {
|
if (page != null) {
|
||||||
addParam(url, "page", (page - 1).toString())
|
addParam(url, "page", (page - 1).toString())
|
||||||
@ -513,10 +527,10 @@ class EHentai(
|
|||||||
headers.build()
|
headers.build()
|
||||||
} else headers,
|
} else headers,
|
||||||
).let {
|
).let {
|
||||||
if (cache) {
|
if (cacheControl == null) {
|
||||||
it
|
it
|
||||||
} else {
|
} else {
|
||||||
it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build()
|
it.newBuilder().cacheControl(cacheControl).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -736,7 +750,7 @@ class EHentai(
|
|||||||
exGet(
|
exGet(
|
||||||
favoriteUrl,
|
favoriteUrl,
|
||||||
page = page,
|
page = page,
|
||||||
cache = false,
|
cacheControl = CacheControl.FORCE_NETWORK,
|
||||||
),
|
),
|
||||||
).awaitResponse()
|
).awaitResponse()
|
||||||
}
|
}
|
||||||
@ -820,7 +834,9 @@ class EHentai(
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
chain.proceed(newReq)
|
chain.proceed(newReq)
|
||||||
}.build()
|
}
|
||||||
|
.addInterceptor(ThumbnailPreviewInterceptor())
|
||||||
|
.build()
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
override fun getFilterList(): FilterList {
|
override fun getFilterList(): FilterList {
|
||||||
@ -1080,11 +1096,154 @@ class EHentai(
|
|||||||
EHentaiDescription(state, openMetadataViewer, search)
|
EHentaiDescription(state, openMetadataViewer, search)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getPagePreviewList(
|
||||||
|
manga: MangaInfo,
|
||||||
|
page: Int,
|
||||||
|
): PagePreviewPage {
|
||||||
|
val doc = client.newCall(
|
||||||
|
exGet(
|
||||||
|
(baseUrl + manga.key)
|
||||||
|
.toHttpUrl()
|
||||||
|
.newBuilder()
|
||||||
|
.removeAllQueryParameters("nw")
|
||||||
|
.addQueryParameter("p", (page - 1).toString())
|
||||||
|
.build()
|
||||||
|
.toString(),
|
||||||
|
),
|
||||||
|
).await().asJsoup()
|
||||||
|
val previews = if (doc.selectFirst("div#gdo4 .ths")!!.attr("onClick").contains("inline_set=ts_l")) {
|
||||||
|
doc.body()
|
||||||
|
.select("#gdt div a")
|
||||||
|
.map {
|
||||||
|
PagePreviewInfo(it.text().toInt(), imageUrl = it.select("img").attr("src"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parseNormalPreviewSet(doc)
|
||||||
|
.map { preview ->
|
||||||
|
PagePreviewInfo(preview.index, imageUrl = preview.toUrl())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PagePreviewPage(
|
||||||
|
page = page,
|
||||||
|
pagePreviews = previews,
|
||||||
|
hasNextPage = doc.select("table.ptt tbody tr td")
|
||||||
|
.last()!!
|
||||||
|
.hasClass("ptdd")
|
||||||
|
.not(),
|
||||||
|
pagePreviewPages = doc.select("table.ptt tbody tr td a").asReversed()
|
||||||
|
.firstNotNullOfOrNull { it.text().toIntOrNull() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchPreviewImage(page: PagePreviewInfo, cacheControl: CacheControl?): Response {
|
||||||
|
return client.newCallWithProgress(exGet(page.imageUrl, cacheControl = cacheControl), page).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse normal previews with regular expressions
|
||||||
|
*/
|
||||||
|
private fun parseNormalPreviewSet(doc: Document): List<EHentaiThumbnailPreview> {
|
||||||
|
return doc.body()
|
||||||
|
.select("#gdt div div")
|
||||||
|
.map { it.selectFirst("img")!!.attr("alt").toInt() to it.attr("style") }
|
||||||
|
.map { (index, style) ->
|
||||||
|
val styles = style.split(";").mapNotNull { it.trimOrNull() }
|
||||||
|
val width = styles.first { it.startsWith("width:") }
|
||||||
|
.removePrefix("width:")
|
||||||
|
.removeSuffix("px")
|
||||||
|
.toInt()
|
||||||
|
|
||||||
|
val height = styles.first { it.startsWith("height:") }
|
||||||
|
.removePrefix("height:")
|
||||||
|
.removeSuffix("px")
|
||||||
|
.toInt()
|
||||||
|
|
||||||
|
val background = styles.first { it.startsWith("background:") }
|
||||||
|
.removePrefix("background:")
|
||||||
|
.split(" ")
|
||||||
|
|
||||||
|
val url = background.first { it.startsWith("url(") }
|
||||||
|
.removePrefix("url(")
|
||||||
|
.removeSuffix(")")
|
||||||
|
|
||||||
|
val widthOffset = background.first { it.startsWith("-") }
|
||||||
|
.removePrefix("-")
|
||||||
|
.removeSuffix("px")
|
||||||
|
.toInt()
|
||||||
|
|
||||||
|
EHentaiThumbnailPreview(url, width, height, widthOffset, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class EHentaiThumbnailPreview(
|
||||||
|
val imageUrl: String,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val widthOffset: Int,
|
||||||
|
val index: Int,
|
||||||
|
) {
|
||||||
|
fun toUrl(): String {
|
||||||
|
return BLANK_PREVIEW_THUMB.toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("imageUrl", imageUrl)
|
||||||
|
.addQueryParameter("width", width.toString())
|
||||||
|
.addQueryParameter("height", height.toString())
|
||||||
|
.addQueryParameter("widthOffset", widthOffset.toString())
|
||||||
|
.build()
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parseFromUrl(url: HttpUrl) = EHentaiThumbnailPreview(
|
||||||
|
imageUrl = url.queryParameter("imageUrl")!!,
|
||||||
|
width = url.queryParameter("width")!!.toInt(),
|
||||||
|
height = url.queryParameter("height")!!.toInt(),
|
||||||
|
widthOffset = url.queryParameter("widthOffset")!!.toInt(),
|
||||||
|
index = -1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ThumbnailPreviewInterceptor : Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
|
||||||
|
if (request.url.host == THUMB_DOMAIN && request.url.pathSegments.contains(BLANK_THUMB)) {
|
||||||
|
val thumbnailPreview = EHentaiThumbnailPreview.parseFromUrl(request.url)
|
||||||
|
val response = chain.proceed(request.newBuilder().url(thumbnailPreview.imageUrl).build())
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val body = ByteArrayOutputStream()
|
||||||
|
.use {
|
||||||
|
val bitmap = BitmapFactory.decodeStream(response.body!!.byteStream())
|
||||||
|
?: throw IOException("Null bitmap($thumbnailPreview)")
|
||||||
|
Bitmap.createBitmap(
|
||||||
|
bitmap,
|
||||||
|
thumbnailPreview.widthOffset,
|
||||||
|
0,
|
||||||
|
thumbnailPreview.width.coerceAtMost(bitmap.width - thumbnailPreview.widthOffset),
|
||||||
|
thumbnailPreview.height.coerceAtMost(bitmap.height),
|
||||||
|
).compress(Bitmap.CompressFormat.JPEG, 100, it)
|
||||||
|
it.toByteArray()
|
||||||
|
}
|
||||||
|
.toResponseBody("image/jpeg".toMediaType())
|
||||||
|
|
||||||
|
return response.newBuilder().body(body).build()
|
||||||
|
} else {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TR_SUFFIX = "TR"
|
private const val TR_SUFFIX = "TR"
|
||||||
private const val REVERSE_PARAM = "TEH_REVERSE"
|
private const val REVERSE_PARAM = "TEH_REVERSE"
|
||||||
private val PAGE_COUNT_REGEX = "[0-9]*".toRegex()
|
private val PAGE_COUNT_REGEX = "[0-9]*".toRegex()
|
||||||
private val RATING_REGEX = "([0-9]*)px".toRegex()
|
private val RATING_REGEX = "([0-9]*)px".toRegex()
|
||||||
|
private const val THUMB_DOMAIN = "ehgt.org"
|
||||||
|
private const val BLANK_THUMB = "blank.gif"
|
||||||
|
private const val BLANK_PREVIEW_THUMB = "https://$THUMB_DOMAIN/g/$BLANK_THUMB"
|
||||||
|
|
||||||
private const val EH_API_BASE = "https://api.e-hentai.org/api.php"
|
private const val EH_API_BASE = "https://api.e-hentai.org/api.php"
|
||||||
private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
|
private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
|
||||||
|
@ -4,7 +4,12 @@ import android.content.Context
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewInfo
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewPage
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@ -23,6 +28,7 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
|
||||||
@ -30,7 +36,8 @@ class NHentai(delegate: HttpSource, val context: Context) :
|
|||||||
DelegatedHttpSource(delegate),
|
DelegatedHttpSource(delegate),
|
||||||
MetadataSource<NHentaiSearchMetadata, Response>,
|
MetadataSource<NHentaiSearchMetadata, Response>,
|
||||||
UrlImportableSource,
|
UrlImportableSource,
|
||||||
NamespaceSource {
|
NamespaceSource,
|
||||||
|
PagePreviewSource {
|
||||||
override val metaClass = NHentaiSearchMetadata::class
|
override val metaClass = NHentaiSearchMetadata::class
|
||||||
override val lang = delegate.lang
|
override val lang = delegate.lang
|
||||||
|
|
||||||
@ -174,6 +181,35 @@ class NHentai(delegate: HttpSource, val context: Context) :
|
|||||||
NHentaiDescription(state, openMetadataViewer)
|
NHentaiDescription(state, openMetadataViewer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getPagePreviewList(manga: MangaInfo, page: Int): PagePreviewPage {
|
||||||
|
val metadata = fetchOrLoadMetadata(manga.id()) {
|
||||||
|
client.newCall(mangaDetailsRequest(manga.toSManga())).await()
|
||||||
|
}
|
||||||
|
return PagePreviewPage(
|
||||||
|
page,
|
||||||
|
metadata.pageImageTypes.mapIndexed { index, s ->
|
||||||
|
PagePreviewInfo(index + 1, imageUrl = thumbnailUrlFromType(metadata.mediaId!!, index + 1, s)!!)
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun thumbnailUrlFromType(mediaId: String, page: Int, t: String) = NHentaiSearchMetadata.typeToExtension(t)?.let {
|
||||||
|
"https://t3.nhentai.net/galleries/$mediaId/${page}t.$it"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchPreviewImage(page: PagePreviewInfo, cacheControl: CacheControl?): Response {
|
||||||
|
return client.newCallWithProgress(
|
||||||
|
if (cacheControl != null) {
|
||||||
|
GET(page.imageUrl, cache = cacheControl)
|
||||||
|
} else {
|
||||||
|
GET(page.imageUrl)
|
||||||
|
},
|
||||||
|
page,
|
||||||
|
).await()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val otherId = 7309872737163460316L
|
const val otherId = 7309872737163460316L
|
||||||
|
|
||||||
|
@ -74,6 +74,7 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||||||
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
|
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
|
||||||
import eu.kanade.tachiyomi.widget.materialdialogs.await
|
import eu.kanade.tachiyomi.widget.materialdialogs.await
|
||||||
import exh.md.similar.MangaDexSimilarController
|
import exh.md.similar.MangaDexSimilarController
|
||||||
|
import exh.pagepreview.PagePreviewController
|
||||||
import exh.recs.RecommendsController
|
import exh.recs.RecommendsController
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import exh.source.getMainSource
|
import exh.source.getMainSource
|
||||||
@ -191,6 +192,7 @@ class MangaController :
|
|||||||
onMergedSettingsClicked = this::openMergedSettingsDialog,
|
onMergedSettingsClicked = this::openMergedSettingsDialog,
|
||||||
onMergeClicked = this::openSmartSearch,
|
onMergeClicked = this::openSmartSearch,
|
||||||
onMergeWithAnotherClicked = this::mergeWithAnother,
|
onMergeWithAnotherClicked = this::mergeWithAnother,
|
||||||
|
onMorePreviewsClicked = this::openMorePagePreviews,
|
||||||
// SY <--
|
// SY <--
|
||||||
onMultiBookmarkClicked = presenter::bookmarkChapters,
|
onMultiBookmarkClicked = presenter::bookmarkChapters,
|
||||||
onMultiMarkAsReadClicked = presenter::markChaptersRead,
|
onMultiMarkAsReadClicked = presenter::markChaptersRead,
|
||||||
@ -338,12 +340,19 @@ class MangaController :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
private fun openMorePagePreviews() {
|
||||||
|
val manga = presenter.manga ?: return
|
||||||
|
router.pushController(PagePreviewController(manga.id))
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
fun openSmartSearch() {
|
private fun openSmartSearch() {
|
||||||
val manga = presenter.manga ?: return
|
val manga = presenter.manga ?: return
|
||||||
val smartSearchConfig = SourcesController.SmartSearchConfig(manga.title, manga.id)
|
val smartSearchConfig = SourcesController.SmartSearchConfig(manga.title, manga.id)
|
||||||
|
|
||||||
router?.pushController(
|
router.pushController(
|
||||||
SourcesController(
|
SourcesController(
|
||||||
bundleOf(
|
bundleOf(
|
||||||
SourcesController.SMART_SEARCH_CONFIG to smartSearchConfig,
|
SourcesController.SMART_SEARCH_CONFIG to smartSearchConfig,
|
||||||
|
@ -22,6 +22,7 @@ import eu.kanade.domain.manga.interactor.GetManga
|
|||||||
import eu.kanade.domain.manga.interactor.GetMangaWithChapters
|
import eu.kanade.domain.manga.interactor.GetMangaWithChapters
|
||||||
import eu.kanade.domain.manga.interactor.GetMergedMangaById
|
import eu.kanade.domain.manga.interactor.GetMergedMangaById
|
||||||
import eu.kanade.domain.manga.interactor.GetMergedReferencesById
|
import eu.kanade.domain.manga.interactor.GetMergedReferencesById
|
||||||
|
import eu.kanade.domain.manga.interactor.GetPagePreviews
|
||||||
import eu.kanade.domain.manga.interactor.InsertManga
|
import eu.kanade.domain.manga.interactor.InsertManga
|
||||||
import eu.kanade.domain.manga.interactor.InsertMergedReference
|
import eu.kanade.domain.manga.interactor.InsertMergedReference
|
||||||
import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
|
import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
|
||||||
@ -30,6 +31,7 @@ import eu.kanade.domain.manga.interactor.UpdateManga
|
|||||||
import eu.kanade.domain.manga.interactor.UpdateMergedSettings
|
import eu.kanade.domain.manga.interactor.UpdateMergedSettings
|
||||||
import eu.kanade.domain.manga.model.MangaUpdate
|
import eu.kanade.domain.manga.model.MangaUpdate
|
||||||
import eu.kanade.domain.manga.model.MergeMangaSettingsUpdate
|
import eu.kanade.domain.manga.model.MergeMangaSettingsUpdate
|
||||||
|
import eu.kanade.domain.manga.model.PagePreview
|
||||||
import eu.kanade.domain.manga.model.TriStateFilter
|
import eu.kanade.domain.manga.model.TriStateFilter
|
||||||
import eu.kanade.domain.manga.model.isLocal
|
import eu.kanade.domain.manga.model.isLocal
|
||||||
import eu.kanade.domain.manga.model.toDbManga
|
import eu.kanade.domain.manga.model.toDbManga
|
||||||
@ -50,6 +52,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||||
@ -139,6 +142,7 @@ class MangaPresenter(
|
|||||||
private val deleteMangaById: DeleteMangaById = Injekt.get(),
|
private val deleteMangaById: DeleteMangaById = Injekt.get(),
|
||||||
private val deleteByMergeId: DeleteByMergeId = Injekt.get(),
|
private val deleteByMergeId: DeleteByMergeId = Injekt.get(),
|
||||||
private val getFlatMetadata: GetFlatMetadataById = Injekt.get(),
|
private val getFlatMetadata: GetFlatMetadataById = Injekt.get(),
|
||||||
|
private val getPagePreviews: GetPagePreviews = Injekt.get(),
|
||||||
// SY <--
|
// SY <--
|
||||||
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
|
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
|
||||||
private val setMangaChapterFlags: SetMangaChapterFlags = Injekt.get(),
|
private val setMangaChapterFlags: SetMangaChapterFlags = Injekt.get(),
|
||||||
@ -216,6 +220,7 @@ class MangaPresenter(
|
|||||||
val chapters: List<DomainChapter>,
|
val chapters: List<DomainChapter>,
|
||||||
val flatMetadata: FlatMetadata?,
|
val flatMetadata: FlatMetadata?,
|
||||||
val mergedData: MergedMangaData? = null,
|
val mergedData: MergedMangaData? = null,
|
||||||
|
val pagePreviewsState: PagePreviewState = PagePreviewState.Loading,
|
||||||
) {
|
) {
|
||||||
constructor(pair: Pair<DomainManga, List<DomainChapter>>, flatMetadata: FlatMetadata?) :
|
constructor(pair: Pair<DomainManga, List<DomainChapter>>, flatMetadata: FlatMetadata?) :
|
||||||
this(pair.first, pair.second, flatMetadata)
|
this(pair.first, pair.second, flatMetadata)
|
||||||
@ -317,10 +322,18 @@ class MangaPresenter(
|
|||||||
isFromSource = isFromSource,
|
isFromSource = isFromSource,
|
||||||
trackingAvailable = trackManager.hasLoggedServices(),
|
trackingAvailable = trackManager.hasLoggedServices(),
|
||||||
chapters = chapterItems,
|
chapters = chapterItems,
|
||||||
|
// SY -->
|
||||||
meta = raiseMetadata(flatMetadata, source),
|
meta = raiseMetadata(flatMetadata, source),
|
||||||
mergedData = mergedData,
|
mergedData = mergedData,
|
||||||
showRecommendationsInOverflow = preferences.recommendsInOverflow().get(),
|
showRecommendationsInOverflow = preferences.recommendsInOverflow().get(),
|
||||||
showMergeWithAnother = smartSearched,
|
showMergeWithAnother = smartSearched,
|
||||||
|
pagePreviewsState = if (source.getMainSource() is PagePreviewSource) {
|
||||||
|
getPagePreviews(manga, source)
|
||||||
|
PagePreviewState.Loading
|
||||||
|
} else {
|
||||||
|
PagePreviewState.Unused
|
||||||
|
},
|
||||||
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -890,10 +903,28 @@ class MangaPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
private fun DomainChapter.toMergedDownloadedChapter() = copy(
|
private fun DomainChapter.toMergedDownloadedChapter() = copy(
|
||||||
scanlator = scanlator?.substringAfter(": "),
|
scanlator = scanlator?.substringAfter(": "),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun getPagePreviews(manga: DomainManga, source: Source) {
|
||||||
|
presenterScope.launchIO {
|
||||||
|
when (val result = getPagePreviews.await(manga, source, 1)) {
|
||||||
|
is GetPagePreviews.Result.Error -> updateSuccessState {
|
||||||
|
it.copy(pagePreviewsState = PagePreviewState.Error(result.error))
|
||||||
|
}
|
||||||
|
is GetPagePreviews.Result.Success -> updateSuccessState {
|
||||||
|
it.copy(pagePreviewsState = PagePreviewState.Success(result.pagePreviews))
|
||||||
|
}
|
||||||
|
GetPagePreviews.Result.Unused -> updateSuccessState {
|
||||||
|
it.copy(pagePreviewsState = PagePreviewState.Unused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests an updated list of chapters from the source.
|
* Requests an updated list of chapters from the source.
|
||||||
*/
|
*/
|
||||||
@ -1382,6 +1413,7 @@ sealed class MangaScreenState {
|
|||||||
val mergedData: MergedMangaData?,
|
val mergedData: MergedMangaData?,
|
||||||
val showRecommendationsInOverflow: Boolean,
|
val showRecommendationsInOverflow: Boolean,
|
||||||
val showMergeWithAnother: Boolean,
|
val showMergeWithAnother: Boolean,
|
||||||
|
val pagePreviewsState: PagePreviewState,
|
||||||
// SY <--
|
// SY <--
|
||||||
) : MangaScreenState() {
|
) : MangaScreenState() {
|
||||||
|
|
||||||
@ -1446,3 +1478,12 @@ private val chapterDecimalFormat = DecimalFormat(
|
|||||||
DecimalFormatSymbols()
|
DecimalFormatSymbols()
|
||||||
.apply { decimalSeparator = '.' },
|
.apply { decimalSeparator = '.' },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
sealed class PagePreviewState {
|
||||||
|
object Unused : PagePreviewState()
|
||||||
|
object Loading : PagePreviewState()
|
||||||
|
data class Success(val pagePreviews: List<PagePreview>) : PagePreviewState()
|
||||||
|
data class Error(val error: Throwable) : PagePreviewState()
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
@ -20,6 +20,7 @@ import eu.kanade.domain.manga.interactor.GetAllManga
|
|||||||
import eu.kanade.domain.manga.repository.MangaRepository
|
import eu.kanade.domain.manga.repository.MangaRepository
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
|
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
|
||||||
@ -86,6 +87,7 @@ class SettingsAdvancedController(
|
|||||||
private val trackManager: TrackManager by injectLazy()
|
private val trackManager: TrackManager by injectLazy()
|
||||||
private val getAllManga: GetAllManga by injectLazy()
|
private val getAllManga: GetAllManga by injectLazy()
|
||||||
private val getChapterByMangaId: GetChapterByMangaId by injectLazy()
|
private val getChapterByMangaId: GetChapterByMangaId by injectLazy()
|
||||||
|
private val pagePreviewCache: PagePreviewCache by injectLazy()
|
||||||
|
|
||||||
@SuppressLint("BatteryLife")
|
@SuppressLint("BatteryLife")
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
|
||||||
@ -160,6 +162,15 @@ class SettingsAdvancedController(
|
|||||||
|
|
||||||
onClick { clearChapterCache() }
|
onClick { clearChapterCache() }
|
||||||
}
|
}
|
||||||
|
// SY -->
|
||||||
|
preference {
|
||||||
|
key = CLEAR_PREVIEW_CACHE_KEY
|
||||||
|
titleRes = R.string.pref_clear_page_preview_cache
|
||||||
|
summary = context.getString(R.string.used_cache, pagePreviewCache.readableSize)
|
||||||
|
|
||||||
|
onClick { clearPagePreviewCache() }
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
switchPreference {
|
switchPreference {
|
||||||
key = Keys.autoClearChapterCache
|
key = Keys.autoClearChapterCache
|
||||||
titleRes = R.string.pref_auto_clear_chapter_cache
|
titleRes = R.string.pref_auto_clear_chapter_cache
|
||||||
@ -514,6 +525,23 @@ class SettingsAdvancedController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun clearPagePreviewCache() {
|
||||||
|
val activity = activity ?: return
|
||||||
|
launchIO {
|
||||||
|
try {
|
||||||
|
val deletedFiles = pagePreviewCache.clear()
|
||||||
|
withUIContext {
|
||||||
|
activity.toast(resources?.getString(R.string.cache_deleted, deletedFiles))
|
||||||
|
findPreference(CLEAR_PREVIEW_CACHE_KEY)?.summary =
|
||||||
|
resources?.getString(R.string.used_cache, pagePreviewCache.readableSize)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
withUIContext { activity.toast(R.string.cache_delete_error) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun clearChapterCache() {
|
private fun clearChapterCache() {
|
||||||
@ -574,3 +602,7 @@ class SettingsAdvancedController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val CLEAR_CACHE_KEY = "pref_clear_cache_key"
|
private const val CLEAR_CACHE_KEY = "pref_clear_cache_key"
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
private const val CLEAR_PREVIEW_CACHE_KEY = "pref_clear_preview_cache_key"
|
||||||
|
// SY <--
|
||||||
|
36
app/src/main/java/exh/pagepreview/PagePreviewController.kt
Normal file
36
app/src/main/java/exh/pagepreview/PagePreviewController.kt
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package exh.pagepreview
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||||
|
import exh.pagepreview.components.PagePreviewScreen
|
||||||
|
|
||||||
|
class PagePreviewController : FullComposeController<PagePreviewPresenter> {
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
constructor(bundle: Bundle? = null) : super(bundle)
|
||||||
|
|
||||||
|
constructor(mangaId: Long) : super(
|
||||||
|
bundleOf(MANGA_ID to mangaId),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun createPresenter() = PagePreviewPresenter(args.getLong(MANGA_ID, -1))
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun ComposeContent() {
|
||||||
|
PagePreviewScreen(
|
||||||
|
state = presenter.state.collectAsState().value,
|
||||||
|
pageDialogOpen = presenter.pageDialogOpen,
|
||||||
|
onPageSelected = presenter::moveToPage,
|
||||||
|
onOpenPageDialog = { presenter.pageDialogOpen = true },
|
||||||
|
onDismissPageDialog = { presenter.pageDialogOpen = false },
|
||||||
|
navigateUp = router::popCurrentController,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MANGA_ID = "manga_id"
|
||||||
|
}
|
||||||
|
}
|
103
app/src/main/java/exh/pagepreview/PagePreviewPresenter.kt
Normal file
103
app/src/main/java/exh/pagepreview/PagePreviewPresenter.kt
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package exh.pagepreview
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import eu.kanade.domain.manga.interactor.GetManga
|
||||||
|
import eu.kanade.domain.manga.interactor.GetPagePreviews
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
import eu.kanade.domain.manga.model.PagePreview
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class PagePreviewPresenter(
|
||||||
|
private val mangaId: Long,
|
||||||
|
private val getPagePreviews: GetPagePreviews = Injekt.get(),
|
||||||
|
private val getManga: GetManga = Injekt.get(),
|
||||||
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
|
) : BasePresenter<PagePreviewController>() {
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow<PagePreviewState>(PagePreviewState.Loading)
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val page = MutableStateFlow(1)
|
||||||
|
|
||||||
|
var pageDialogOpen by mutableStateOf(false)
|
||||||
|
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
presenterScope.launchIO {
|
||||||
|
val manga = getManga.await(mangaId)!!
|
||||||
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
page
|
||||||
|
.onEach { page ->
|
||||||
|
when (
|
||||||
|
val previews = getPagePreviews.await(manga, source, page)
|
||||||
|
) {
|
||||||
|
is GetPagePreviews.Result.Error -> _state.update {
|
||||||
|
PagePreviewState.Error(previews.error)
|
||||||
|
}
|
||||||
|
is GetPagePreviews.Result.Success -> _state.update {
|
||||||
|
when (it) {
|
||||||
|
PagePreviewState.Loading, is PagePreviewState.Error -> {
|
||||||
|
PagePreviewState.Success(
|
||||||
|
page,
|
||||||
|
previews.pagePreviews,
|
||||||
|
previews.hasNextPage,
|
||||||
|
previews.pageCount,
|
||||||
|
manga,
|
||||||
|
source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PagePreviewState.Success -> it.copy(
|
||||||
|
page = page,
|
||||||
|
pagePreviews = previews.pagePreviews,
|
||||||
|
hasNextPage = previews.hasNextPage,
|
||||||
|
pageCount = previews.pageCount,
|
||||||
|
).also { logcat { page.toString() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GetPagePreviews.Result.Unused -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.catch { e ->
|
||||||
|
_state.update {
|
||||||
|
PagePreviewState.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveToPage(page: Int) {
|
||||||
|
this.page.value = page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class PagePreviewState {
|
||||||
|
object Loading : PagePreviewState()
|
||||||
|
|
||||||
|
data class Success(
|
||||||
|
val page: Int,
|
||||||
|
val pagePreviews: List<PagePreview>,
|
||||||
|
val hasNextPage: Boolean,
|
||||||
|
val pageCount: Int?,
|
||||||
|
val manga: Manga,
|
||||||
|
val source: Source,
|
||||||
|
) : PagePreviewState()
|
||||||
|
|
||||||
|
data class Error(val error: Throwable) : PagePreviewState()
|
||||||
|
}
|
@ -0,0 +1,209 @@
|
|||||||
|
package exh.pagepreview.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.AnimationState
|
||||||
|
import androidx.compose.animation.core.animateTo
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.UTurnRight
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SmallTopAppBar
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.material3.rememberTopAppBarScrollState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.components.AroundLayout
|
||||||
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
|
import eu.kanade.presentation.components.Scaffold
|
||||||
|
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||||
|
import eu.kanade.presentation.manga.components.PagePreview
|
||||||
|
import eu.kanade.presentation.util.plus
|
||||||
|
import eu.kanade.presentation.util.topPaddingValues
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import exh.pagepreview.PagePreviewState
|
||||||
|
import exh.util.floor
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PagePreviewScreen(
|
||||||
|
state: PagePreviewState,
|
||||||
|
pageDialogOpen: Boolean,
|
||||||
|
onPageSelected: (Int) -> Unit,
|
||||||
|
onOpenPageDialog: () -> Unit,
|
||||||
|
onDismissPageDialog: () -> Unit,
|
||||||
|
navigateUp: () -> Unit,
|
||||||
|
) {
|
||||||
|
val topAppBarScrollState = rememberTopAppBarScrollState()
|
||||||
|
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarScrollState)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.statusBarsPadding()
|
||||||
|
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
PagePreviewTopAppBar(
|
||||||
|
topAppBarScrollBehavior = topAppBarScrollBehavior,
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
title = stringResource(id = R.string.page_previews),
|
||||||
|
onOpenPageDialog = onOpenPageDialog,
|
||||||
|
showOpenPageDialog = state is PagePreviewState.Success &&
|
||||||
|
(state.pageCount != null && state.pageCount > 1 /* TODO support unknown pageCount || state.hasNextPage*/),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { paddingValues ->
|
||||||
|
when (state) {
|
||||||
|
is PagePreviewState.Error -> EmptyScreen(state.error.message.orEmpty())
|
||||||
|
PagePreviewState.Loading -> LoadingScreen()
|
||||||
|
is PagePreviewState.Success -> {
|
||||||
|
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||||
|
val itemPerRowCount by derivedStateOf { (maxWidth / 120.dp).floor() }
|
||||||
|
val items by derivedStateOf { state.pagePreviews.chunked(itemPerRowCount) }
|
||||||
|
SideEffect {
|
||||||
|
logcat { (items.hashCode() to state.page).toString() }
|
||||||
|
}
|
||||||
|
ScrollbarLazyColumn(
|
||||||
|
modifier = Modifier,
|
||||||
|
contentPadding = paddingValues + topPaddingValues,
|
||||||
|
) {
|
||||||
|
items.forEach {
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
it.forEach { page ->
|
||||||
|
PagePreview(
|
||||||
|
modifier = Modifier.weight(1F),
|
||||||
|
page = page,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pageDialogOpen && state is PagePreviewState.Success) {
|
||||||
|
PagePreviewPageDialog(
|
||||||
|
currentPage = state.page,
|
||||||
|
pageCount = state.pageCount!!,
|
||||||
|
onDismissPageDialog = onDismissPageDialog,
|
||||||
|
onPageSelected = onPageSelected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PagePreviewPageDialog(
|
||||||
|
currentPage: Int,
|
||||||
|
pageCount: Int,
|
||||||
|
onDismissPageDialog: () -> Unit,
|
||||||
|
onPageSelected: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
var page by remember(currentPage) {
|
||||||
|
mutableStateOf(currentPage.toFloat())
|
||||||
|
}
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissPageDialog,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onPageSelected(page.roundToInt())
|
||||||
|
onDismissPageDialog()
|
||||||
|
},) {
|
||||||
|
Text(stringResource(android.R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissPageDialog) {
|
||||||
|
Text(stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(stringResource(R.string.page_preview_page_go_to))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
AroundLayout(
|
||||||
|
startLayout = { Text(text = page.roundToInt().toString()) },
|
||||||
|
endLayout = { Text(text = pageCount.toString()) },
|
||||||
|
) {
|
||||||
|
Slider(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = page,
|
||||||
|
onValueChange = { page = it },
|
||||||
|
onValueChangeFinished = {
|
||||||
|
scope.launch {
|
||||||
|
val newPage = page
|
||||||
|
AnimationState(
|
||||||
|
newPage,
|
||||||
|
).animateTo(newPage.roundToInt().toFloat()) {
|
||||||
|
page = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
valueRange = 1F..pageCount.toFloat(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PagePreviewTopAppBar(
|
||||||
|
topAppBarScrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
navigateUp: () -> Unit,
|
||||||
|
title: String,
|
||||||
|
onOpenPageDialog: () -> Unit,
|
||||||
|
showOpenPageDialog: Boolean,
|
||||||
|
) {
|
||||||
|
SmallTopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = navigateUp) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.abc_action_bar_up_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = title)
|
||||||
|
},
|
||||||
|
scrollBehavior = topAppBarScrollBehavior,
|
||||||
|
actions = {
|
||||||
|
if (showOpenPageDialog) {
|
||||||
|
IconButton(onClick = onOpenPageDialog) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.UTurnRight,
|
||||||
|
contentDescription = stringResource(R.string.page_preview_page_go_to),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
@ -531,6 +531,12 @@
|
|||||||
<string name="manga_id_is_null">Manga ID is null!</string>
|
<string name="manga_id_is_null">Manga ID is null!</string>
|
||||||
<string name="loading_manga">Loading manga…</string>
|
<string name="loading_manga">Loading manga…</string>
|
||||||
|
|
||||||
|
<!-- Page previews -->
|
||||||
|
<string name="page_previews">Page previews</string>
|
||||||
|
<string name="more_previews">More previews</string>
|
||||||
|
<string name="pref_clear_page_preview_cache">Clear page preview cache</string>
|
||||||
|
<string name="page_preview_page_go_to">Go to</string>
|
||||||
|
|
||||||
<!-- Rating 0-10 (0, 0.5, 1, 1.5 and so fourth) -->
|
<!-- Rating 0-10 (0, 0.5, 1, 1.5 and so fourth) -->
|
||||||
<string name="rating10">Masterpiece</string>
|
<string name="rating10">Masterpiece</string>
|
||||||
<string name="rating9">Amazing</string>
|
<string name="rating9">Amazing</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user