Last Commit Merged: ef863335e6
This commit is contained in:
LuftVerbot 2023-05-31 14:43:40 +02:00
parent af98923989
commit d005475754
30 changed files with 437 additions and 459 deletions

View file

@ -1,17 +1,10 @@
package eu.kanade.core.util
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import rx.Emitter
import rx.Observable
import rx.Observer
import kotlin.coroutines.CoroutineContext
fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
val observer = object : Observer<T> {
@ -30,32 +23,3 @@ fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
val subscription = subscribe(observer)
awaitClose { subscription.unsubscribe() }
}
fun <T : Any> Flow<T>.asObservable(
context: CoroutineContext = Dispatchers.Unconfined,
backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE,
): Observable<T> {
return Observable.create(
{ emitter ->
/*
* ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if
* asObservable is already invoked from unconfined
*/
val job = GlobalScope.launch(context = context, start = CoroutineStart.ATOMIC) {
try {
collect { emitter.onNext(it) }
emitter.onCompleted()
} catch (e: Throwable) {
// Ignore `CancellationException` as error, since it indicates "normal cancellation"
if (e !is CancellationException) {
emitter.onError(e)
} else {
emitter.onCompleted()
}
}
}
emitter.setCancellation { job.cancel() }
},
backpressureMode,
)
}

View file

@ -1,6 +1,7 @@
package eu.kanade.presentation.browse.anime
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.HelpOutline
@ -11,17 +12,21 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import eu.kanade.presentation.browse.anime.components.BrowseAnimeSourceComfortableGrid
import eu.kanade.presentation.browse.anime.components.BrowseAnimeSourceCompactGrid
import eu.kanade.presentation.browse.anime.components.BrowseAnimeSourceList
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.EmptyScreenAction
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.source.anime.AnimeSourceManager
import eu.kanade.tachiyomi.source.anime.LocalAnimeSource
import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.entries.anime.model.Anime
@ -30,7 +35,7 @@ import tachiyomi.domain.library.model.LibraryDisplayMode
@Composable
fun BrowseAnimeSourceContent(
source: AnimeCatalogueSource?,
source: AnimeSource?,
animeList: LazyPagingItems<StateFlow<Anime>>,
columns: GridCells,
displayMode: LibraryDisplayMode,
@ -139,3 +144,24 @@ fun BrowseAnimeSourceContent(
}
}
}
@Composable
fun MissingSourceScreen(
source: AnimeSourceManager.StubAnimeSource,
navigateUp: () -> Unit,
) {
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = source.name,
navigateUp = navigateUp,
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
EmptyScreen(
message = source.getSourceNotInstalledException().message!!,
modifier = Modifier.padding(paddingValues),
)
}
}

View file

@ -20,7 +20,7 @@ import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.RadioMenuItem
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.source.anime.LocalAnimeSource
import tachiyomi.domain.library.model.LibraryDisplayMode
@ -28,7 +28,7 @@ import tachiyomi.domain.library.model.LibraryDisplayMode
fun BrowseAnimeSourceToolbar(
searchQuery: String?,
onSearchQueryChange: (String?) -> Unit,
source: AnimeCatalogueSource?,
source: AnimeSource?,
displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit,

View file

@ -1,6 +1,7 @@
package eu.kanade.presentation.browse.manga
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.HelpOutline
@ -11,18 +12,22 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import eu.kanade.presentation.browse.manga.components.BrowseMangaSourceComfortableGrid
import eu.kanade.presentation.browse.manga.components.BrowseMangaSourceCompactGrid
import eu.kanade.presentation.browse.manga.components.BrowseMangaSourceList
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.EmptyScreenAction
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import eu.kanade.tachiyomi.source.manga.MangaSourceManager
import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.NoChaptersException
@ -30,7 +35,7 @@ import tachiyomi.domain.library.model.LibraryDisplayMode
@Composable
fun BrowseSourceContent(
source: CatalogueSource?,
source: MangaSource?,
mangaList: LazyPagingItems<StateFlow<Manga>>,
columns: GridCells,
displayMode: LibraryDisplayMode,
@ -139,3 +144,24 @@ fun BrowseSourceContent(
}
}
}
@Composable
fun MissingSourceScreen(
source: MangaSourceManager.StubMangaSource,
navigateUp: () -> Unit,
) {
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = source.name,
navigateUp = navigateUp,
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
EmptyScreen(
message = source.getSourceNotInstalledException().message!!,
modifier = Modifier.padding(paddingValues),
)
}
}

View file

@ -20,7 +20,7 @@ import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.RadioMenuItem
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import tachiyomi.domain.library.model.LibraryDisplayMode
@ -28,7 +28,7 @@ import tachiyomi.domain.library.model.LibraryDisplayMode
fun BrowseMangaSourceToolbar(
searchQuery: String?,
onSearchQueryChange: (String?) -> Unit,
source: CatalogueSource?,
source: MangaSource?,
displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit,

View file

@ -76,6 +76,7 @@ import eu.kanade.tachiyomi.ui.entries.anime.EpisodeItem
import eu.kanade.tachiyomi.ui.entries.manga.chapterDecimalFormat
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.model.Episode
import uy.kohesive.injekt.Injekt
@ -98,7 +99,10 @@ fun AnimeScreen(
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?,
onTagClicked: (String) -> Unit,
// For tags menu
onTagSearch: (String) -> Unit,
onFilterButtonClicked: () -> Unit,
onRefresh: () -> Unit,
onContinueWatching: () -> Unit,
@ -125,6 +129,13 @@ fun AnimeScreen(
onAllEpisodeSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
) {
val context = LocalContext.current
val onCopyTagToClipboard: (tag: String) -> Unit = {
if (it.isNotEmpty()) {
context.copyToClipboard(it, it)
}
}
if (!isTabletUi) {
AnimeScreenSmallImpl(
state = state,
@ -138,7 +149,8 @@ fun AnimeScreen(
onWebViewClicked = onWebViewClicked,
onWebViewLongClicked = onWebViewLongClicked,
onTrackingClicked = onTrackingClicked,
onTagClicked = onTagClicked,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
onFilterClicked = onFilterButtonClicked,
onRefresh = onRefresh,
onContinueWatching = onContinueWatching,
@ -170,7 +182,8 @@ fun AnimeScreen(
onWebViewClicked = onWebViewClicked,
onWebViewLongClicked = onWebViewLongClicked,
onTrackingClicked = onTrackingClicked,
onTagClicked = onTagClicked,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
onFilterButtonClicked = onFilterButtonClicked,
onRefresh = onRefresh,
onContinueWatching = onContinueWatching,
@ -206,7 +219,11 @@ private fun AnimeScreenSmallImpl(
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?,
onTagClicked: (String) -> Unit,
// For tags menu
onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit,
onFilterClicked: () -> Unit,
onRefresh: () -> Unit,
onContinueWatching: () -> Unit,
@ -382,7 +399,8 @@ private fun AnimeScreenSmallImpl(
defaultExpandState = state.isFromSource,
description = state.anime.description,
tagsProvider = { state.anime.genre },
onTagClicked = onTagClicked,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
)
}
@ -427,7 +445,11 @@ fun AnimeScreenLargeImpl(
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?,
onTagClicked: (String) -> Unit,
// For tags menu
onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit,
onFilterButtonClicked: () -> Unit,
onRefresh: () -> Unit,
onContinueWatching: () -> Unit,
@ -581,7 +603,8 @@ fun AnimeScreenLargeImpl(
defaultExpandState = true,
description = state.anime.description,
tagsProvider = { state.anime.genre },
onTagClicked = onTagClicked,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
)
}
},

View file

@ -35,6 +35,7 @@ import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
@ -72,6 +73,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.ItemCover
import eu.kanade.presentation.components.TextButton
import eu.kanade.presentation.entries.DotSeparatorText
@ -211,7 +213,8 @@ fun ExpandableAnimeDescription(
defaultExpandState: Boolean,
description: String?,
tagsProvider: () -> List<String>?,
onTagClicked: (String) -> Unit,
onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit,
) {
Column(modifier = modifier) {
val (expanded, onExpanded) = rememberSaveable {
@ -241,6 +244,27 @@ fun ExpandableAnimeDescription(
.padding(vertical = 12.dp)
.animateContentSize(),
) {
var showMenu by remember { mutableStateOf(false) }
var tagSelected by remember { mutableStateOf("") }
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_search)) },
onClick = {
onTagSearch(tagSelected)
showMenu = false
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_copy_to_clipboard)) },
onClick = {
onCopyTagToClipboard(tagSelected)
showMenu = false
},
)
}
if (expanded) {
FlowRow(
modifier = Modifier.padding(horizontal = 16.dp),
@ -250,7 +274,10 @@ fun ExpandableAnimeDescription(
tags.forEach {
TagsChip(
text = it,
onClick = { onTagClicked(it) },
onClick = {
tagSelected = it
showMenu = true
},
)
}
}
@ -262,7 +289,10 @@ fun ExpandableAnimeDescription(
items(items = tags) {
TagsChip(
text = it,
onClick = { onTagClicked(it) },
onClick = {
tagSelected = it
showMenu = true
},
)
}
}

View file

@ -74,6 +74,7 @@ import eu.kanade.tachiyomi.ui.entries.manga.ChapterItem
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreenState
import eu.kanade.tachiyomi.ui.entries.manga.chapterDecimalFormat
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter
import java.text.DateFormat
@ -93,7 +94,10 @@ fun MangaScreen(
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?,
onTagClicked: (String) -> Unit,
// For tags menu
onTagSearch: (String) -> Unit,
onFilterButtonClicked: () -> Unit,
onRefresh: () -> Unit,
onContinueReading: () -> Unit,
@ -119,6 +123,13 @@ fun MangaScreen(
onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
) {
val context = LocalContext.current
val onCopyTagToClipboard: (tag: String) -> Unit = {
if (it.isNotEmpty()) {
context.copyToClipboard(it, it)
}
}
if (!isTabletUi) {
MangaScreenSmallImpl(
state = state,
@ -132,7 +143,8 @@ fun MangaScreen(
onWebViewClicked = onWebViewClicked,
onWebViewLongClicked = onWebViewLongClicked,
onTrackingClicked = onTrackingClicked,
onTagClicked = onTagClicked,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
onFilterClicked = onFilterButtonClicked,
onRefresh = onRefresh,
onContinueReading = onContinueReading,
@ -163,7 +175,8 @@ fun MangaScreen(
onWebViewClicked = onWebViewClicked,
onWebViewLongClicked = onWebViewLongClicked,
onTrackingClicked = onTrackingClicked,
onTagClicked = onTagClicked,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
onFilterButtonClicked = onFilterButtonClicked,
onRefresh = onRefresh,
onContinueReading = onContinueReading,
@ -197,7 +210,11 @@ private fun MangaScreenSmallImpl(
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?,
onTagClicked: (String) -> Unit,
// For tags menu
onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit,
onFilterClicked: () -> Unit,
onRefresh: () -> Unit,
onContinueReading: () -> Unit,
@ -367,7 +384,8 @@ private fun MangaScreenSmallImpl(
defaultExpandState = state.isFromSource,
description = state.manga.description,
tagsProvider = { state.manga.genre },
onTagClicked = onTagClicked,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
)
}
@ -411,7 +429,11 @@ fun MangaScreenLargeImpl(
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?,
onTagClicked: (String) -> Unit,
// For tags menu
onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit,
onFilterButtonClicked: () -> Unit,
onRefresh: () -> Unit,
onContinueReading: () -> Unit,
@ -562,7 +584,8 @@ fun MangaScreenLargeImpl(
defaultExpandState = true,
description = state.manga.description,
tagsProvider = { state.manga.genre },
onTagClicked = onTagClicked,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
)
}
},

View file

@ -35,6 +35,7 @@ import androidx.compose.material.icons.outlined.Pause
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
@ -72,6 +73,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.ItemCover
import eu.kanade.presentation.components.TextButton
import eu.kanade.presentation.entries.DotSeparatorText
@ -211,7 +213,8 @@ fun ExpandableMangaDescription(
defaultExpandState: Boolean,
description: String?,
tagsProvider: () -> List<String>?,
onTagClicked: (String) -> Unit,
onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit,
) {
Column(modifier = modifier) {
val (expanded, onExpanded) = rememberSaveable {
@ -241,6 +244,27 @@ fun ExpandableMangaDescription(
.padding(vertical = 12.dp)
.animateContentSize(),
) {
var showMenu by remember { mutableStateOf(false) }
var tagSelected by remember { mutableStateOf("") }
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_search)) },
onClick = {
onTagSearch(tagSelected)
showMenu = false
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_copy_to_clipboard)) },
onClick = {
onCopyTagToClipboard(tagSelected)
showMenu = false
},
)
}
if (expanded) {
FlowRow(
modifier = Modifier.padding(horizontal = 16.dp),
@ -250,7 +274,10 @@ fun ExpandableMangaDescription(
tags.forEach {
TagsChip(
text = it,
onClick = { onTagClicked(it) },
onClick = {
tagSelected = it
showMenu = true
},
)
}
}
@ -262,7 +289,10 @@ fun ExpandableMangaDescription(
items(items = tags) {
TagsChip(
text = it,
onClick = { onTagClicked(it) },
onClick = {
tagSelected = it
showMenu = true
},
)
}
}

View file

@ -27,7 +27,6 @@ import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateNotifier
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.anime.AnimeSourceManager
import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.storage.toFFmpegString
@ -36,6 +35,7 @@ import kotlinx.coroutines.async
import logcat.LogPriority
import okhttp3.HttpUrl.Companion.toHttpUrl
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription
@ -763,6 +763,8 @@ class AnimeDownloader(
return queue.none { it.status.value <= AnimeDownload.State.DOWNLOADING.value }
}
private operator fun CompositeSubscription.plusAssign(subscription: Subscription) = add(subscription)
companion object {
const val TMP_DIR_SUFFIX = "_tmp"
const val WARNING_NOTIF_TIMEOUT_MS = 30_000L

View file

@ -20,7 +20,6 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
import eu.kanade.tachiyomi.util.lang.RetryWithDelay
import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
import eu.kanade.tachiyomi.util.storage.saveTo
@ -30,6 +29,7 @@ import logcat.LogPriority
import nl.adaptivity.xmlutil.serialization.XML
import okhttp3.Response
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription
@ -637,6 +637,8 @@ class MangaDownloader(
return queue.none { it.status.value <= MangaDownload.State.DOWNLOADING.value }
}
private operator fun CompositeSubscription.plusAssign(subscription: Subscription) = add(subscription)
companion object {
const val TMP_DIR_SUFFIX = "_tmp"
const val WARNING_NOTIF_TIMEOUT_MS = 30_000L

View file

@ -137,6 +137,6 @@ class AnimeSourceManager(
}
}
inner class AnimeSourceNotInstalledException(val sourceString: String) :
inner class AnimeSourceNotInstalledException(sourceString: String) :
Exception(context.getString(R.string.source_not_installed, sourceString))
}

View file

@ -154,6 +154,6 @@ class MangaSourceManager(
}
}
inner class SourceNotInstalledException(val sourceString: String) :
inner class SourceNotInstalledException(sourceString: String) :
Exception(context.getString(R.string.source_not_installed, sourceString))
}

View file

@ -49,7 +49,7 @@ class MigrateAnimeSearchScreenModel(
.filter { it.lang in enabledLanguages }
.filterNot { "${it.id}" in disabledSources }
.sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" }))
.sortedByDescending { it.id == state.value.anime!!.id }
.sortedByDescending { it.id == state.value.anime!!.source }
}
override fun updateSearchQuery(query: String?) {

View file

@ -39,6 +39,7 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.anime.BrowseAnimeSourceContent
import eu.kanade.presentation.browse.anime.MissingSourceScreen
import eu.kanade.presentation.browse.anime.components.BrowseAnimeSourceToolbar
import eu.kanade.presentation.browse.anime.components.RemoveEntryDialog
import eu.kanade.presentation.components.ChangeCategoryDialog
@ -48,8 +49,10 @@ import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.AssistContentScreen
import eu.kanade.presentation.util.padding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.core.Constants
import eu.kanade.tachiyomi.source.anime.AnimeSourceManager
import eu.kanade.tachiyomi.source.anime.LocalAnimeSource
import eu.kanade.tachiyomi.ui.browse.anime.source.browse.BrowseAnimeSourceScreenModel.Listing
import eu.kanade.tachiyomi.ui.category.CategoriesTab
@ -73,17 +76,10 @@ data class BrowseAnimeSourceScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val uriHandler = LocalUriHandler.current
val screenModel = rememberScreenModel { BrowseAnimeSourceScreenModel(sourceId, listingQuery) }
val state by screenModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val navigator = LocalNavigator.currentOrThrow
val navigateUp: () -> Unit = {
when {
!state.isUserQuery && state.toolbarQuery != null -> screenModel.setToolbarQuery(null)
@ -91,8 +87,21 @@ data class BrowseAnimeSourceScreen(
}
}
val onHelpClick = { uriHandler.openUri(LocalAnimeSource.HELP_URL) }
if (screenModel.source is AnimeSourceManager.StubAnimeSource) {
MissingSourceScreen(
source = screenModel.source,
navigateUp = navigateUp,
)
return
}
val scope = rememberCoroutineScope()
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val uriHandler = LocalUriHandler.current
val snackbarHostState = remember { SnackbarHostState() }
val onHelpClick = { uriHandler.openUri(LocalAnimeSource.HELP_URL) }
val onWebViewClick = f@{
val source = screenModel.source as? AnimeHttpSource ?: return@f
navigator.push(
@ -147,7 +156,7 @@ data class BrowseAnimeSourceScreen(
Text(text = stringResource(R.string.popular))
},
)
if (screenModel.source.supportsLatest) {
if ((screenModel.source as AnimeCatalogueSource).supportsLatest) {
FilterChip(
selected = state.listing == Listing.Latest,
onClick = {

View file

@ -103,9 +103,10 @@ class BrowseAnimeSourceScreenModel(
var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope)
val source = sourceManager.get(sourceId) as AnimeCatalogueSource
val source = sourceManager.getOrStub(sourceId)
init {
if (source is AnimeCatalogueSource) {
mutableState.update {
var query: String? = null
var listing = it.listing
@ -122,6 +123,7 @@ class BrowseAnimeSourceScreenModel(
)
}
}
}
/**
* Sheet containing filter items.
@ -164,6 +166,8 @@ class BrowseAnimeSourceScreenModel(
}
fun resetFilters() {
if (source !is AnimeCatalogueSource) return
mutableState.update { it.copy(filters = source.getFilterList()) }
}
@ -172,6 +176,8 @@ class BrowseAnimeSourceScreenModel(
}
fun search(query: String? = null, filters: AnimeFilterList? = null) {
if (source !is AnimeCatalogueSource) return
val input = state.value.listing as? Listing.Search
?: Listing.Search(query = null, filters = source.getFilterList())
@ -187,6 +193,8 @@ class BrowseAnimeSourceScreenModel(
}
fun searchGenre(genreName: String) {
if (source !is AnimeCatalogueSource) return
val defaultFilters = source.getFilterList()
var genreExists = false

View file

@ -49,7 +49,7 @@ class MigrateSearchScreenModel(
.filter { it.lang in enabledLanguages }
.filterNot { "${it.id}" in disabledSources }
.sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" }))
.sortedByDescending { it.id == state.value.manga!!.id }
.sortedByDescending { it.id == state.value.manga!!.source }
}
override fun updateSearchQuery(query: String?) {

View file

@ -40,6 +40,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.anime.components.RemoveEntryDialog
import eu.kanade.presentation.browse.manga.BrowseSourceContent
import eu.kanade.presentation.browse.manga.MissingSourceScreen
import eu.kanade.presentation.browse.manga.components.BrowseMangaSourceToolbar
import eu.kanade.presentation.components.ChangeCategoryDialog
import eu.kanade.presentation.components.Divider
@ -49,7 +50,9 @@ import eu.kanade.presentation.util.AssistContentScreen
import eu.kanade.presentation.util.padding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.Constants
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import eu.kanade.tachiyomi.source.manga.MangaSourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.manga.source.browse.BrowseMangaSourceScreenModel.Listing
import eu.kanade.tachiyomi.ui.category.CategoriesTab
@ -73,17 +76,10 @@ data class BrowseMangaSourceScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val uriHandler = LocalUriHandler.current
val screenModel = rememberScreenModel { BrowseMangaSourceScreenModel(sourceId, listingQuery) }
val state by screenModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val navigator = LocalNavigator.currentOrThrow
val navigateUp: () -> Unit = {
when {
!state.isUserQuery && state.toolbarQuery != null -> screenModel.setToolbarQuery(null)
@ -91,8 +87,21 @@ data class BrowseMangaSourceScreen(
}
}
val onHelpClick = { uriHandler.openUri(LocalMangaSource.HELP_URL) }
if (screenModel.source is MangaSourceManager.StubMangaSource) {
MissingSourceScreen(
source = screenModel.source,
navigateUp = navigateUp,
)
return
}
val scope = rememberCoroutineScope()
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val uriHandler = LocalUriHandler.current
val snackbarHostState = remember { SnackbarHostState() }
val onHelpClick = { uriHandler.openUri(LocalMangaSource.HELP_URL) }
val onWebViewClick = f@{
val source = screenModel.source as? HttpSource ?: return@f
navigator.push(
@ -147,7 +156,7 @@ data class BrowseMangaSourceScreen(
Text(text = stringResource(R.string.popular))
},
)
if (screenModel.source.supportsLatest) {
if ((screenModel.source as CatalogueSource).supportsLatest) {
FilterChip(
selected = state.listing == Listing.Latest,
onClick = {

View file

@ -103,9 +103,10 @@ class BrowseMangaSourceScreenModel(
var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope)
val source = sourceManager.get(sourceId) as CatalogueSource
val source = sourceManager.getOrStub(sourceId)
init {
if (source is CatalogueSource) {
mutableState.update {
var query: String? = null
var listing = it.listing
@ -122,6 +123,7 @@ class BrowseMangaSourceScreenModel(
)
}
}
}
/**
* Sheet containing filter items.
@ -164,6 +166,8 @@ class BrowseMangaSourceScreenModel(
}
fun resetFilters() {
if (source !is CatalogueSource) return
mutableState.update { it.copy(filters = source.getFilterList()) }
}
@ -172,6 +176,8 @@ class BrowseMangaSourceScreenModel(
}
fun search(query: String? = null, filters: FilterList? = null) {
if (source !is CatalogueSource) return
val input = state.value.listing as? Listing.Search
?: Listing.Search(query = null, filters = source.getFilterList())
@ -187,6 +193,8 @@ class BrowseMangaSourceScreenModel(
}
fun searchGenre(genreName: String) {
if (source !is CatalogueSource) return
val defaultFilters = source.getFilterList()
var genreExists = false

View file

@ -136,7 +136,7 @@ class AnimeScreen(
onWebViewClicked = { openAnimeInWebView(navigator, screenModel.anime, screenModel.source) }.takeIf { isAnimeHttpSource },
onWebViewLongClicked = { copyAnimeUrl(context, screenModel.anime, screenModel.source) }.takeIf { isAnimeHttpSource },
onTrackingClicked = screenModel::showTrackDialog.takeIf { successState.trackingAvailable },
onTagClicked = { scope.launch { performGenreSearch(navigator, it, screenModel.source!!) } },
onTagSearch = { scope.launch { performGenreSearch(navigator, it, screenModel.source!!) } },
onFilterButtonClicked = screenModel::showSettingsDialog,
onRefresh = screenModel::fetchAllFromSource,
onContinueWatching = {

View file

@ -115,7 +115,7 @@ class MangaScreen(
onWebViewClicked = { openMangaInWebView(navigator, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
onWebViewLongClicked = { copyMangaUrl(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
onTrackingClicked = screenModel::showTrackDialog.takeIf { successState.trackingAvailable },
onTagClicked = { scope.launch { performGenreSearch(navigator, it, screenModel.source!!) } },
onTagSearch = { scope.launch { performGenreSearch(navigator, it, screenModel.source!!) } },
onFilterButtonClicked = screenModel::showSettingsDialog,
onRefresh = screenModel::fetchAllFromSource,
onContinueReading = { continueReading(context, screenModel.getNextUnreadChapter()) },

View file

@ -3,12 +3,14 @@ package eu.kanade.tachiyomi.ui.main
import android.animation.ValueAnimator
import android.app.SearchManager
import android.app.assist.AssistContent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
@ -41,6 +43,7 @@ import androidx.core.animation.doOnEnd
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.util.Consumer
import androidx.core.view.WindowCompat
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
@ -96,7 +99,10 @@ import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setComposeContent
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
@ -136,8 +142,7 @@ class MainActivity : BaseActivity() {
private var animeSettingsSheet: AnimeLibrarySettingsSheet? = null
private var mangaSettingsSheet: MangaLibrarySettingsSheet? = null
private var isHandlingShortcut: Boolean = false
private lateinit var navigator: Navigator
private var navigator: Navigator? = null
override fun onCreate(savedInstanceState: Bundle?) {
// Prevent splash screen showing up on configuration changes
@ -234,7 +239,7 @@ class MainActivity : BaseActivity() {
if (savedInstanceState == null) {
// Set start screen
handleIntentAction(intent)
handleIntentAction(intent, navigator)
// Reset Incognito Mode on relaunch
preferences.incognitoMode().set(false)
@ -280,6 +285,8 @@ class MainActivity : BaseActivity() {
.launchIn(this)
}
HandleOnNewIntent(context = context, navigator = navigator)
CheckForUpdates()
}
@ -312,7 +319,7 @@ class MainActivity : BaseActivity() {
override fun onProvideAssistContent(outContent: AssistContent) {
super.onProvideAssistContent(outContent)
when (val screen = navigator.lastItem) {
when (val screen = navigator?.lastItem) {
is AssistContentScreen -> {
screen.onProvideAssistUrl()?.let { outContent.webUri = it.toUri() }
}
@ -355,6 +362,18 @@ class MainActivity : BaseActivity() {
}
}
@Composable
fun HandleOnNewIntent(context: Context, navigator: Navigator) {
LaunchedEffect(Unit) {
callbackFlow<Intent> {
val componentActivity = context as ComponentActivity
val consumer = Consumer<Intent> { trySend(it) }
componentActivity.addOnNewIntentListener(consumer)
awaitClose { componentActivity.removeOnNewIntentListener(consumer) }
}.collectLatest { handleIntentAction(it, navigator) }
}
}
@Composable
private fun CheckForUpdates() {
val context = LocalContext.current
@ -434,47 +453,36 @@ class MainActivity : BaseActivity() {
}
}
override fun onNewIntent(intent: Intent) {
lifecycleScope.launch {
val handle = handleIntentAction(intent)
if (!handle) {
super.onNewIntent(intent)
}
}
}
private suspend fun handleIntentAction(intent: Intent): Boolean {
private fun handleIntentAction(intent: Intent, navigator: Navigator): Boolean {
val notificationId = intent.getIntExtra("notificationId", -1)
if (notificationId > -1) {
NotificationReceiver.dismissNotification(applicationContext, notificationId, intent.getIntExtra("groupId", 0))
}
isHandlingShortcut = true
when (intent.action) {
Constants.SHORTCUT_ANIMELIB -> HomeScreen.openTab(HomeScreen.Tab.Animelib())
Constants.SHORTCUT_LIBRARY -> HomeScreen.openTab(HomeScreen.Tab.Library())
val tabToOpen = when (intent.action) {
Constants.SHORTCUT_ANIMELIB -> HomeScreen.Tab.Animelib()
Constants.SHORTCUT_LIBRARY -> HomeScreen.Tab.Library()
Constants.SHORTCUT_MANGA -> {
val idToOpen = intent.extras?.getLong(Constants.MANGA_EXTRA) ?: return false
navigator.popUntilRoot()
HomeScreen.openTab(HomeScreen.Tab.Library(idToOpen))
HomeScreen.Tab.Library(idToOpen)
}
Constants.SHORTCUT_ANIME -> {
val idToOpen = intent.extras?.getLong(Constants.ANIME_EXTRA) ?: return false
navigator.popUntilRoot()
HomeScreen.openTab(HomeScreen.Tab.Animelib(idToOpen))
HomeScreen.Tab.Animelib(idToOpen)
}
Constants.SHORTCUT_UPDATES -> HomeScreen.openTab(HomeScreen.Tab.Updates)
Constants.SHORTCUT_HISTORY -> HomeScreen.openTab(HomeScreen.Tab.History)
Constants.SHORTCUT_SOURCES -> HomeScreen.openTab(HomeScreen.Tab.Browse(false))
Constants.SHORTCUT_EXTENSIONS -> HomeScreen.openTab(HomeScreen.Tab.Browse(true))
Constants.SHORTCUT_UPDATES -> HomeScreen.Tab.Updates
Constants.SHORTCUT_HISTORY -> HomeScreen.Tab.History
Constants.SHORTCUT_SOURCES -> HomeScreen.Tab.Browse(false)
Constants.SHORTCUT_EXTENSIONS -> HomeScreen.Tab.Browse(true)
Constants.SHORTCUT_DOWNLOADS -> {
navigator.popUntilRoot()
HomeScreen.openTab(HomeScreen.Tab.More(toDownloads = true))
HomeScreen.Tab.More(toDownloads = true)
}
Constants.SHORTCUT_ANIME_DOWNLOADS -> {
navigator.popUntilRoot()
HomeScreen.openTab(HomeScreen.Tab.More(toDownloads = true))
HomeScreen.Tab.More(toDownloads = true)
}
Intent.ACTION_SEARCH, Intent.ACTION_SEND, "com.google.android.gms.actions.SEARCH_ACTION" -> {
// If the intent match the "standard" Android search intent
@ -486,6 +494,7 @@ class MainActivity : BaseActivity() {
navigator.popUntilRoot()
navigator.push(GlobalMangaSearchScreen(query))
}
null
}
INTENT_SEARCH -> {
val query = intent.getStringExtra(INTENT_SEARCH_QUERY)
@ -494,6 +503,7 @@ class MainActivity : BaseActivity() {
navigator.popUntilRoot()
navigator.push(GlobalMangaSearchScreen(query, filter))
}
null
}
INTENT_ANIMESEARCH -> {
val query = intent.getStringExtra(INTENT_SEARCH_QUERY)
@ -502,15 +512,16 @@ class MainActivity : BaseActivity() {
navigator.popUntilRoot()
navigator.push(GlobalAnimeSearchScreen(query, filter))
}
null
}
else -> {
isHandlingShortcut = false
return false
else -> return false
}
if (tabToOpen != null) {
lifecycleScope.launch { HomeScreen.openTab(tabToOpen) }
}
ready = true
isHandlingShortcut = false
return true
}
@ -523,7 +534,7 @@ class MainActivity : BaseActivity() {
}
override fun onBackPressed() {
if (navigator.size == 1 &&
if (navigator?.size == 1 &&
!onBackPressedDispatcher.hasEnabledCallbacks() &&
libraryPreferences.autoClearItemCache().get()
) {

View file

@ -582,7 +582,12 @@ class ReaderViewModel(
val sChapter = getCurrentChapter()?.chapter ?: return null
val source = getSource() ?: return null
return source.getChapterUrl(sChapter)
return try {
source.getChapterUrl(sChapter)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
null
}
}
/**

View file

@ -19,10 +19,10 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import kotlinx.coroutines.supervisorScope
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.InputStream
@ -60,29 +60,13 @@ class PagerPageHolder(
private val scope = MainScope()
/**
* Job for loading the page.
* Job for loading the page and processing changes to the page's status.
*/
private var loadJob: Job? = null
/**
* Job for status changes of the page.
*/
private var statusJob: Job? = null
/**
* Job for progress changes of the page.
*/
private var progressJob: Job? = null
/**
* Subscription used to read the header of the image. This is needed in order to instantiate
* the appropiate image view depending if the image is animated (GIF).
*/
private var readImageHeaderSubscription: Subscription? = null
init {
addView(progressIndicator)
launchLoadJob()
loadJob = scope.launch { loadPageAndProcessStatus() }
}
/**
@ -91,81 +75,39 @@ class PagerPageHolder(
@SuppressLint("ClickableViewAccessibility")
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
cancelProgressJob()
cancelLoadJob()
unsubscribeReadImageHeader()
loadJob?.cancel()
loadJob = null
}
/**
* Starts loading the page and processing changes to the page's status.
* Loads the page and processes changes to the page's status.
*
* @see processStatus
* Returns immediately if the page has no PageLoader.
* Otherwise, this function does not return. It will continue to process status changes until
* the Job is cancelled.
*/
private fun launchLoadJob() {
loadJob?.cancel()
statusJob?.cancel()
private suspend fun loadPageAndProcessStatus() {
val loader = page.chapter.pageLoader ?: return
loadJob = scope.launch {
supervisorScope {
launchIO {
loader.loadPage(page)
}
statusJob = scope.launch {
page.statusFlow.collectLatest { processStatus(it) }
}
}
private fun launchProgressJob() {
progressJob?.cancel()
progressJob = scope.launch {
page.progressFlow.collectLatest { value -> progressIndicator.setProgress(value) }
}
}
/**
* Called when the status of the page changes.
*
* @param status the new status of the page.
*/
private fun processStatus(status: Page.State) {
when (status) {
page.statusFlow.collectLatest { state ->
when (state) {
Page.State.QUEUE -> setQueued()
Page.State.LOAD_PAGE -> setLoading()
Page.State.DOWNLOAD_IMAGE -> {
launchProgressJob()
setDownloading()
page.progressFlow.collectLatest { value ->
progressIndicator.setProgress(value)
}
Page.State.READY -> {
setImage()
cancelProgressJob()
}
Page.State.ERROR -> {
setError()
cancelProgressJob()
Page.State.READY -> setImage()
Page.State.ERROR -> setError()
}
}
}
/**
* Cancels loading the page and processing changes to the page's status.
*/
private fun cancelLoadJob() {
loadJob?.cancel()
loadJob = null
statusJob?.cancel()
statusJob = null
}
private fun cancelProgressJob() {
progressJob?.cancel()
progressJob = null
}
/**
* Unsubscribes from the read image header subscription.
*/
private fun unsubscribeReadImageHeader() {
readImageHeaderSubscription?.unsubscribe()
readImageHeaderSubscription = null
}
/**
@ -195,19 +137,16 @@ class PagerPageHolder(
/**
* Called when the page is ready.
*/
private fun setImage() {
private suspend fun setImage() {
progressIndicator.setProgress(0)
errorLayout?.root?.isVisible = false
unsubscribeReadImageHeader()
val streamFn = page.stream ?: return
readImageHeaderSubscription = Observable
.fromCallable {
val stream = streamFn().buffered(16)
val itemStream = process(item, stream)
val (bais, isAnimated, background) = withIOContext {
streamFn().buffered(16).use { stream ->
process(item, stream).use { itemStream ->
val bais = ByteArrayInputStream(itemStream.readBytes())
try {
val isAnimated = ImageUtil.isAnimatedAndSupported(bais)
bais.reset()
val background = if (!isAnimated && viewer.config.automaticBackground) {
@ -217,14 +156,10 @@ class PagerPageHolder(
}
bais.reset()
Triple(bais, isAnimated, background)
} finally {
stream.close()
itemStream.close()
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { (bais, isAnimated, background) ->
}
withUIContext {
bais.use {
setImage(
it,
@ -242,7 +177,6 @@ class PagerPageHolder(
}
}
}
.subscribe({}, {})
}
private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream {

View file

@ -4,7 +4,6 @@ import android.content.Context
import android.view.View
import android.view.ViewGroup.LayoutParams
import androidx.recyclerview.widget.RecyclerView
import rx.Subscription
abstract class WebtoonBaseHolder(
view: View,
@ -21,21 +20,6 @@ abstract class WebtoonBaseHolder(
*/
open fun recycle() {}
/**
* Adds a subscription to a list of subscriptions that will automatically unsubscribe when the
* activity or the reader is destroyed.
*/
protected fun addSubscription(subscription: Subscription?) {
viewer.subscriptions.add(subscription)
}
/**
* Removes a subscription from the list of subscriptions.
*/
protected fun removeSubscription(subscription: Subscription?) {
subscription?.let { viewer.subscriptions.remove(it) }
}
/**
* Extension method to set layout params to wrap content on this view.
*/

View file

@ -24,10 +24,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.suspendCancellableCoroutine
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import java.io.BufferedInputStream
import java.io.InputStream
@ -77,22 +78,6 @@ class WebtoonPageHolder(
*/
private var loadJob: Job? = null
/**
* Job for status changes of the page.
*/
private var statusJob: Job? = null
/**
* Job for progress changes of the page.
*/
private var progressJob: Job? = null
/**
* Subscription used to read the header of the image. This is needed in order to instantiate
* the appropriate image view depending if the image is animated (GIF).
*/
private var readImageHeaderSubscription: Subscription? = null
init {
refreshLayoutParams()
@ -106,7 +91,8 @@ class WebtoonPageHolder(
*/
fun bind(page: ReaderPage) {
this.page = page
launchLoadJob()
loadJob?.cancel()
loadJob = scope.launch { loadPageAndProcessStatus() }
refreshLayoutParams()
}
@ -126,9 +112,8 @@ class WebtoonPageHolder(
* Called when the view is recycled and added to the view pool.
*/
override fun recycle() {
cancelLoadJob()
cancelProgressJob()
unsubscribeReadImageHeader()
loadJob?.cancel()
loadJob = null
removeErrorLayout()
frame.recycle()
@ -136,84 +121,34 @@ class WebtoonPageHolder(
}
/**
* Starts loading the page and processing changes to the page's status.
* Loads the page and processes changes to the page's status.
*
* @see processStatus
* Returns immediately if there is no page or the page has no PageLoader.
* Otherwise, this function does not return. It will continue to process status changes until
* the Job is cancelled.
*/
private fun launchLoadJob() {
cancelLoadJob()
private suspend fun loadPageAndProcessStatus() {
val page = page ?: return
val loader = page.chapter.pageLoader ?: return
loadJob = scope.launch {
supervisorScope {
launchIO {
loader.loadPage(page)
}
statusJob = scope.launch {
page.statusFlow.collectLatest { processStatus(it) }
}
}
/**
* Observes the progress of the page and updates view.
*/
private fun launchProgressJob() {
cancelProgressJob()
val page = page ?: return
progressJob = scope.launch {
page.progressFlow.collectLatest { value -> progressIndicator.setProgress(value) }
}
}
/**
* Called when the status of the page changes.
*
* @param status the new status of the page.
*/
private fun processStatus(status: Page.State) {
when (status) {
page.statusFlow.collectLatest { state ->
when (state) {
Page.State.QUEUE -> setQueued()
Page.State.LOAD_PAGE -> setLoading()
Page.State.DOWNLOAD_IMAGE -> {
launchProgressJob()
setDownloading()
page.progressFlow.collectLatest { value ->
progressIndicator.setProgress(value)
}
Page.State.READY -> {
setImage()
cancelProgressJob()
}
Page.State.ERROR -> {
setError()
cancelProgressJob()
Page.State.READY -> setImage()
Page.State.ERROR -> setError()
}
}
}
/**
* Cancels loading the page and processing changes to the page's status.
*/
private fun cancelLoadJob() {
loadJob?.cancel()
loadJob = null
statusJob?.cancel()
statusJob = null
}
/**
* Unsubscribes from the progress subscription.
*/
private fun cancelProgressJob() {
progressJob?.cancel()
progressJob = null
}
/**
* Unsubscribes from the read image header subscription.
*/
private fun unsubscribeReadImageHeader() {
removeSubscription(readImageHeaderSubscription)
readImageHeaderSubscription = null
}
/**
@ -246,26 +181,22 @@ class WebtoonPageHolder(
/**
* Called when the page is ready.
*/
private fun setImage() {
private suspend fun setImage() {
progressIndicator.setProgress(0)
removeErrorLayout()
unsubscribeReadImageHeader()
val streamFn = page?.stream ?: return
var openStream: InputStream? = null
readImageHeaderSubscription = Observable
.fromCallable {
val (openStream, isAnimated) = withIOContext {
val stream = streamFn().buffered(16)
openStream = process(stream)
val openStream = process(stream)
ImageUtil.isAnimatedAndSupported(stream)
val isAnimated = ImageUtil.isAnimatedAndSupported(stream)
Pair(openStream, isAnimated)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated ->
withUIContext {
frame.setImage(
openStream!!,
openStream,
isAnimated,
ReaderPageImageView.Config(
zoomDuration = viewer.config.doubleTapAnimDuration,
@ -274,12 +205,10 @@ class WebtoonPageHolder(
),
)
}
// Keep the Rx stream alive to close the input stream only when unsubscribed
.flatMap { Observable.never<Unit>() }
.doOnUnsubscribe { openStream?.close() }
.subscribe({}, {})
addSubscription(readImageHeaderSubscription)
// Suspend the coroutine to close the input stream only when the WebtoonPageHolder is recycled
suspendCancellableCoroutine<Nothing> { continuation ->
continuation.invokeOnCancellation { openStream.close() }
}
}
private fun process(imageStream: BufferedInputStream): InputStream {

View file

@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import rx.subscriptions.CompositeSubscription
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -74,11 +73,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
*/
private var currentPage: Any? = null
/**
* Subscriptions to keep while this viewer is used.
*/
val subscriptions = CompositeSubscription()
private val threshold: Int =
Injekt.get<ReaderPreferences>()
.readerHideThreshold()
@ -196,7 +190,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
override fun destroy() {
super.destroy()
scope.cancel()
subscriptions.unsubscribe()
}
/**

View file

@ -1,6 +0,0 @@
package eu.kanade.tachiyomi.util.lang
import rx.Subscription
import rx.subscriptions.CompositeSubscription
operator fun CompositeSubscription.plusAssign(subscription: Subscription) = add(subscription)

View file

@ -1,15 +1,8 @@
package tachiyomi.core.util.lang
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import rx.Emitter
import rx.Observable
import rx.Subscriber
import rx.Subscription
@ -61,31 +54,5 @@ private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutin
)
}
internal fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) =
private fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) =
invokeOnCancellation { sub.unsubscribe() }
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> runAsObservable(
backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE,
block: suspend () -> T,
): Observable<T> {
return Observable.create(
{ emitter ->
val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) {
try {
emitter.onNext(block())
emitter.onCompleted()
} catch (e: Throwable) {
// Ignore `CancellationException` as error, since it indicates "normal cancellation"
if (e !is CancellationException) {
emitter.onError(e)
} else {
emitter.onCompleted()
}
}
}
emitter.setCancellation { job.cancel() }
},
backpressureMode,
)
}

View file

@ -93,6 +93,7 @@
<string name="action_start">Start</string>
<string name="action_resume">Resume</string>
<string name="action_open_in_browser">Open in browser</string>
<string name="action_copy_to_clipboard">Copy to clipboard</string>
<!-- Do not translate "WebView" -->
<string name="action_open_in_web_view">Open in WebView</string>
<string name="action_web_view" translatable="false">WebView</string>