mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-26 06:43:45 +03:00
parent
af98923989
commit
d005475754
30 changed files with 437 additions and 459 deletions
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -103,23 +103,25 @@ class BrowseAnimeSourceScreenModel(
|
|||
|
||||
var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope)
|
||||
|
||||
val source = sourceManager.get(sourceId) as AnimeCatalogueSource
|
||||
val source = sourceManager.getOrStub(sourceId)
|
||||
|
||||
init {
|
||||
mutableState.update {
|
||||
var query: String? = null
|
||||
var listing = it.listing
|
||||
if (source is AnimeCatalogueSource) {
|
||||
mutableState.update {
|
||||
var query: String? = null
|
||||
var listing = it.listing
|
||||
|
||||
if (listing is Listing.Search) {
|
||||
query = listing.query
|
||||
listing = Listing.Search(query, source.getFilterList())
|
||||
if (listing is Listing.Search) {
|
||||
query = listing.query
|
||||
listing = Listing.Search(query, source.getFilterList())
|
||||
}
|
||||
|
||||
it.copy(
|
||||
listing = listing,
|
||||
filters = source.getFilterList(),
|
||||
toolbarQuery = query,
|
||||
)
|
||||
}
|
||||
|
||||
it.copy(
|
||||
listing = listing,
|
||||
filters = source.getFilterList(),
|
||||
toolbarQuery = query,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -103,23 +103,25 @@ class BrowseMangaSourceScreenModel(
|
|||
|
||||
var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope)
|
||||
|
||||
val source = sourceManager.get(sourceId) as CatalogueSource
|
||||
val source = sourceManager.getOrStub(sourceId)
|
||||
|
||||
init {
|
||||
mutableState.update {
|
||||
var query: String? = null
|
||||
var listing = it.listing
|
||||
if (source is CatalogueSource) {
|
||||
mutableState.update {
|
||||
var query: String? = null
|
||||
var listing = it.listing
|
||||
|
||||
if (listing is Listing.Search) {
|
||||
query = listing.query
|
||||
listing = Listing.Search(query, source.getFilterList())
|
||||
if (listing is Listing.Search) {
|
||||
query = listing.query
|
||||
listing = Listing.Search(query, source.getFilterList())
|
||||
}
|
||||
|
||||
it.copy(
|
||||
listing = listing,
|
||||
filters = source.getFilterList(),
|
||||
toolbarQuery = query,
|
||||
)
|
||||
}
|
||||
|
||||
it.copy(
|
||||
listing = listing,
|
||||
filters = source.getFilterList(),
|
||||
toolbarQuery = query,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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()) },
|
||||
|
|
|
@ -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()
|
||||
) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts loading the page and processing changes to the page's status.
|
||||
*
|
||||
* @see processStatus
|
||||
*/
|
||||
private fun launchLoadJob() {
|
||||
loadJob?.cancel()
|
||||
statusJob?.cancel()
|
||||
|
||||
val loader = page.chapter.pageLoader ?: return
|
||||
loadJob = scope.launch {
|
||||
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.State.QUEUE -> setQueued()
|
||||
Page.State.LOAD_PAGE -> setLoading()
|
||||
Page.State.DOWNLOAD_IMAGE -> {
|
||||
launchProgressJob()
|
||||
setDownloading()
|
||||
}
|
||||
Page.State.READY -> {
|
||||
setImage()
|
||||
cancelProgressJob()
|
||||
}
|
||||
Page.State.ERROR -> {
|
||||
setError()
|
||||
cancelProgressJob()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Loads the page and processes changes to the page's status.
|
||||
*
|
||||
* 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 unsubscribeReadImageHeader() {
|
||||
readImageHeaderSubscription?.unsubscribe()
|
||||
readImageHeaderSubscription = null
|
||||
private suspend fun loadPageAndProcessStatus() {
|
||||
val loader = page.chapter.pageLoader ?: return
|
||||
|
||||
supervisorScope {
|
||||
launchIO {
|
||||
loader.loadPage(page)
|
||||
}
|
||||
page.statusFlow.collectLatest { state ->
|
||||
when (state) {
|
||||
Page.State.QUEUE -> setQueued()
|
||||
Page.State.LOAD_PAGE -> setLoading()
|
||||
Page.State.DOWNLOAD_IMAGE -> {
|
||||
setDownloading()
|
||||
page.progressFlow.collectLatest { value ->
|
||||
progressIndicator.setProgress(value)
|
||||
}
|
||||
}
|
||||
Page.State.READY -> setImage()
|
||||
Page.State.ERROR -> setError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 = ByteArrayInputStream(itemStream.readBytes())
|
||||
try {
|
||||
val (bais, isAnimated, background) = withIOContext {
|
||||
streamFn().buffered(16).use { stream ->
|
||||
process(item, stream).use { itemStream ->
|
||||
val bais = ByteArrayInputStream(itemStream.readBytes())
|
||||
val isAnimated = ImageUtil.isAnimatedAndSupported(bais)
|
||||
bais.reset()
|
||||
val background = if (!isAnimated && viewer.config.automaticBackground) {
|
||||
|
@ -217,32 +156,27 @@ class PagerPageHolder(
|
|||
}
|
||||
bais.reset()
|
||||
Triple(bais, isAnimated, background)
|
||||
} finally {
|
||||
stream.close()
|
||||
itemStream.close()
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { (bais, isAnimated, background) ->
|
||||
bais.use {
|
||||
setImage(
|
||||
it,
|
||||
isAnimated,
|
||||
Config(
|
||||
zoomDuration = viewer.config.doubleTapAnimDuration,
|
||||
minimumScaleType = viewer.config.imageScaleType,
|
||||
cropBorders = viewer.config.imageCropBorders,
|
||||
zoomStartPosition = viewer.config.imageZoomType,
|
||||
landscapeZoom = viewer.config.landscapeZoom,
|
||||
),
|
||||
)
|
||||
if (!isAnimated) {
|
||||
pageBackground = background
|
||||
}
|
||||
}
|
||||
withUIContext {
|
||||
bais.use {
|
||||
setImage(
|
||||
it,
|
||||
isAnimated,
|
||||
Config(
|
||||
zoomDuration = viewer.config.doubleTapAnimDuration,
|
||||
minimumScaleType = viewer.config.imageScaleType,
|
||||
cropBorders = viewer.config.imageCropBorders,
|
||||
zoomStartPosition = viewer.config.imageZoomType,
|
||||
landscapeZoom = viewer.config.landscapeZoom,
|
||||
),
|
||||
)
|
||||
if (!isAnimated) {
|
||||
pageBackground = background
|
||||
}
|
||||
}
|
||||
.subscribe({}, {})
|
||||
}
|
||||
}
|
||||
|
||||
private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream {
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,86 +121,36 @@ 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 {
|
||||
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.State.QUEUE -> setQueued()
|
||||
Page.State.LOAD_PAGE -> setLoading()
|
||||
Page.State.DOWNLOAD_IMAGE -> {
|
||||
launchProgressJob()
|
||||
setDownloading()
|
||||
supervisorScope {
|
||||
launchIO {
|
||||
loader.loadPage(page)
|
||||
}
|
||||
Page.State.READY -> {
|
||||
setImage()
|
||||
cancelProgressJob()
|
||||
}
|
||||
Page.State.ERROR -> {
|
||||
setError()
|
||||
cancelProgressJob()
|
||||
page.statusFlow.collectLatest { state ->
|
||||
when (state) {
|
||||
Page.State.QUEUE -> setQueued()
|
||||
Page.State.LOAD_PAGE -> setLoading()
|
||||
Page.State.DOWNLOAD_IMAGE -> {
|
||||
setDownloading()
|
||||
page.progressFlow.collectLatest { value ->
|
||||
progressIndicator.setProgress(value)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the page is queued.
|
||||
*/
|
||||
|
@ -246,40 +181,34 @@ 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 stream = streamFn().buffered(16)
|
||||
openStream = process(stream)
|
||||
val (openStream, isAnimated) = withIOContext {
|
||||
val stream = streamFn().buffered(16)
|
||||
val openStream = process(stream)
|
||||
|
||||
ImageUtil.isAnimatedAndSupported(stream)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { isAnimated ->
|
||||
frame.setImage(
|
||||
openStream!!,
|
||||
isAnimated,
|
||||
ReaderPageImageView.Config(
|
||||
zoomDuration = viewer.config.doubleTapAnimDuration,
|
||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH,
|
||||
cropBorders = viewer.config.imageCropBorders,
|
||||
),
|
||||
)
|
||||
}
|
||||
// Keep the Rx stream alive to close the input stream only when unsubscribed
|
||||
.flatMap { Observable.never<Unit>() }
|
||||
.doOnUnsubscribe { openStream?.close() }
|
||||
.subscribe({}, {})
|
||||
|
||||
addSubscription(readImageHeaderSubscription)
|
||||
val isAnimated = ImageUtil.isAnimatedAndSupported(stream)
|
||||
Pair(openStream, isAnimated)
|
||||
}
|
||||
withUIContext {
|
||||
frame.setImage(
|
||||
openStream,
|
||||
isAnimated,
|
||||
ReaderPageImageView.Config(
|
||||
zoomDuration = viewer.config.doubleTapAnimDuration,
|
||||
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH,
|
||||
cropBorders = viewer.config.imageCropBorders,
|
||||
),
|
||||
)
|
||||
}
|
||||
// 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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue