Compare commits

...

29 Commits

Author SHA1 Message Date
c615f4d458 Release v0.14.6 2023-04-16 11:00:14 -04:00
9e09a20e65 Avoid uncaught exceptions from OkHttp interceptors crashing entire app
(cherry picked from commit 26d422b0ae)
2023-04-16 10:57:40 -04:00
7115a9b9fe Update track domain shikimori.me (#9333)
shikimori.me

(cherry picked from commit 564a0980b9)
2023-04-16 10:53:01 -04:00
fd8b97fc87 Better handle overflowing content in MigrateDialog actions
Fixes #9207

(cherry picked from commit b7cd7b8b4e)
2023-04-16 10:52:53 -04:00
4dd67e4348 Save current chapter progress when navigating to adjacent chapters
Fixes #9295

(cherry picked from commit 776d36caf1)
2023-04-16 10:52:42 -04:00
10973bf3cd Fix Spanish (Latin America) being missing from in-app language selection
(cherry picked from commit 290efb0283)
2023-04-16 10:51:29 -04:00
934ed0551a Bump subsampling-scale-image-view
(cherry picked from commit e5e18c2030)
2023-04-16 10:51:16 -04:00
38428c6ebe Show proper string in manga detail screen for SourceNotInstalledException
(cherry picked from commit 14d1bcacc9)
2023-04-16 10:51:05 -04:00
bf85e147e7 Set default automatic library updates to off
(cherry picked from commit abd23b6826)
2023-04-16 10:50:55 -04:00
d2dd34c2e5 Use queued last chapter read number when performing delayed tracker update
Fixes #8876

(cherry picked from commit f7f2072621)
2023-04-16 10:50:24 -04:00
c4ab2b4675 Bump default user agent string and minimum WebView version
(cherry picked from commit c6e5f8abd9)
2023-04-16 10:49:28 -04:00
aa2ec5940f Avoid crashing in SourcePreferencesScreen if source can't be loaded
(cherry picked from commit 4efca04765)
2023-04-16 10:49:11 -04:00
79323de326 Avoid crash in DeleteLibraryMangaDialog
No clue why it ever gets a -1 index though.

(cherry picked from commit b12c7cf963)
2023-04-16 10:49:05 -04:00
08e6487a9a Fix download queue page count display bug (#9126)
When restarting a download, the page count would display as 0 until
the first page download completion, after all the existing pages were
rechecked.

To fix, calculate downloadedImages from pages instead of relying on
the downloader to reset and increment the count.

(cherry picked from commit 779df32e98)
2023-04-16 10:48:16 -04:00
4498b10a10 Fix occasional crash when opening library settings sheet
See https://stackoverflow.com/questions/47648689/sealed-classs-objects-mysteriously-becoming-null-when-referenced-by-other-compa

(cherry picked from commit c0e2eb211d)
2023-04-16 10:48:05 -04:00
6f2bb18d72 Avoid crash when loading invalid extension package
(cherry picked from commit 3d7c136320)
2023-04-16 10:47:58 -04:00
b690de55e5 Release v0.14.5 2023-02-19 15:25:35 -05:00
83fda20078 Avoid crashes if headers can't be built for usage in WebView
(cherry picked from commit ec49411bee)
2023-02-19 11:52:09 -05:00
f656a37045 Avoid crashing if getChapterUrl is not implemented
Fixes #9105

(cherry picked from commit ceaf579cb0)
2023-02-19 11:51:53 -05:00
c58b495433 MainActivity: Avoid navigator-related crash when handling onNewIntent (#9104)
(cherry picked from commit d3dadf71e8)
2023-02-19 11:51:44 -05:00
242aeb6a68 Avoid crashing if opening browse with unavailable source
(cherry picked from commit 0ef7650c1a)
2023-02-19 11:50:33 -05:00
d9969cea8a Fix ID type mismatch in MigrateSearchScreenModel (#9090)
`it.id` is the source ID of the source being sorted.
`state.value.manga!!.id` is the manga ID of the selected manga.
`state.value.manga!!.source` is the source ID of the selected manga.

(cherry picked from commit dc2eaf0788)
2023-02-19 11:50:26 -05:00
d61db5931e Move reader preloading to IO scope
Maybe fixes #8440

(cherry picked from commit e052bdef96)
2023-02-19 11:50:05 -05:00
0ea3ac9807 Avoid preload download check if chapter is already loaded or loading
Maybe fixes #8953, #9060

(cherry picked from commit d522d6d545)
2023-02-19 11:49:58 -05:00
f9e43f574f MangaCoverDialog: Disable memory cache (#9066)
(cherry picked from commit 1671a56f42)
2023-02-19 11:49:51 -05:00
5ef11e61d0 Prioritize finding selected chapter when deduping reader chapters
Fixes #9054

(cherry picked from commit 23432e4405)
2023-02-19 11:49:44 -05:00
48546c3db4 Scaffold: Fix snackbar bottom inset (#9052)
(cherry picked from commit 34a586ce48)
2023-02-19 11:49:38 -05:00
4d87ed496c Remove FAB extra padding in DownloadQueueScreen (#9053)
(cherry picked from commit ad762f8303)
2023-02-19 11:49:32 -05:00
06d12e6562 Fix crash in library when selected category is deleted (#9044)
(cherry picked from commit 13bb45b4be)
2023-02-19 11:49:24 -05:00
37 changed files with 264 additions and 162 deletions

View File

@ -3,7 +3,7 @@
I acknowledge that: I acknowledge that:
- I have updated: - I have updated:
- To the latest version of the app (stable is v0.14.4) - To the latest version of the app (stable is v0.14.6)
- All extensions - All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/ - I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions

View File

@ -53,7 +53,7 @@ body:
label: Tachiyomi version label: Tachiyomi version
description: You can find your Tachiyomi version in **More → About**. description: You can find your Tachiyomi version in **More → About**.
placeholder: | placeholder: |
Example: "0.14.4" Example: "0.14.6"
validations: validations:
required: true required: true
@ -98,7 +98,7 @@ body:
required: true required: true
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/). - label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
required: true required: true
- label: I have updated the app to version **[0.14.4](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. - label: I have updated the app to version **[0.14.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true required: true
- label: I have updated all installed extensions. - label: I have updated all installed extensions.
required: true required: true

View File

@ -33,7 +33,7 @@ body:
required: true required: true
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose). - label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
required: true required: true
- label: I have updated the app to version **[0.14.4](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. - label: I have updated the app to version **[0.14.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

View File

@ -22,8 +22,8 @@ android {
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi" applicationId = "eu.kanade.tachiyomi"
versionCode = 95 versionCode = 101
versionName = "0.14.4" versionName = "0.14.6"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")

View File

@ -22,7 +22,7 @@ class LibraryPreferences(
fun landscapeColumns() = preferenceStore.getInt("pref_library_columns_landscape_key", 0) fun landscapeColumns() = preferenceStore.getInt("pref_library_columns_landscape_key", 0)
fun libraryUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 24) fun libraryUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0)
fun libraryUpdateLastTimestamp() = preferenceStore.getLong("library_update_last_timestamp", 0L) fun libraryUpdateLastTimestamp() = preferenceStore.getLong("library_update_last_timestamp", 0L)
fun libraryUpdateDeviceRestriction() = preferenceStore.getStringSet("library_update_restriction", setOf(DEVICE_ONLY_ON_WIFI)) fun libraryUpdateDeviceRestriction() = preferenceStore.getStringSet("library_update_restriction", setOf(DEVICE_ONLY_ON_WIFI))

View File

@ -31,30 +31,33 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
val trackManager = Injekt.get<TrackManager>() val trackManager = Injekt.get<TrackManager>()
val delayedTrackingStore = Injekt.get<DelayedTrackingStore>() val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
withIOContext { val results = withIOContext {
val tracks = delayedTrackingStore.getItems().mapNotNull { delayedTrackingStore.getItems()
val track = getTracks.awaitOne(it.trackId) .mapNotNull {
if (track == null) { val track = getTracks.awaitOne(it.trackId)
delayedTrackingStore.remove(it.trackId) if (track == null) {
} delayedTrackingStore.remove(it.trackId)
track }
} track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
}
tracks.forEach { track -> .mapNotNull { track ->
try { try {
val service = trackManager.getService(track.syncId) val service = trackManager.getService(track.syncId)
if (service != null && service.isLogged) { if (service != null && service.isLogged) {
service.update(track.toDbTrack(), true) logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.id}, last chapter read: ${track.lastChapterRead}" }
insertTrack.await(track) service.update(track.toDbTrack(), true)
insertTrack.await(track)
}
delayedTrackingStore.remove(track.id)
null
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
} }
delayedTrackingStore.remove(track.id)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
} }
}
} }
return Result.success() return if (results.isNotEmpty()) Result.failure() else Result.success()
} }
companion object { companion object {

View File

@ -1,6 +1,7 @@
package eu.kanade.presentation.browse package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
@ -11,6 +12,7 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
@ -18,19 +20,22 @@ import eu.kanade.data.source.NoResultsException
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
import eu.kanade.presentation.browse.components.BrowseSourceList import eu.kanade.presentation.browse.components.BrowseSourceList
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.EmptyScreenAction import eu.kanade.presentation.components.EmptyScreenAction
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@Composable @Composable
fun BrowseSourceContent( fun BrowseSourceContent(
source: CatalogueSource?, source: Source?,
mangaList: LazyPagingItems<StateFlow<Manga>>, mangaList: LazyPagingItems<StateFlow<Manga>>,
columns: GridCells, columns: GridCells,
displayMode: LibraryDisplayMode, displayMode: LibraryDisplayMode,
@ -139,3 +144,24 @@ fun BrowseSourceContent(
} }
} }
} }
@Composable
fun MissingSourceScreen(
source: SourceManager.StubSource,
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,15 +20,15 @@ import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.RadioMenuItem import eu.kanade.presentation.components.RadioMenuItem
import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
@Composable @Composable
fun BrowseSourceToolbar( fun BrowseSourceToolbar(
searchQuery: String?, searchQuery: String?,
onSearchQueryChange: (String?) -> Unit, onSearchQueryChange: (String?) -> Unit,
source: CatalogueSource?, source: Source?,
displayMode: LibraryDisplayMode, displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit, onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit, navigateUp: () -> Unit,

View File

@ -64,9 +64,11 @@ fun DeleteLibraryMangaDialog(
list.forEach { state -> list.forEach { state ->
val onCheck = { val onCheck = {
val index = list.indexOf(state) val index = list.indexOf(state)
val mutableList = list.toMutableList() if (index != -1) {
mutableList[index] = state.next() as CheckboxState.State<Int> val mutableList = list.toMutableList()
list = mutableList.toList() mutableList[index] = state.next() as CheckboxState.State<Int>
list = mutableList.toList()
}
} }
Row( Row(

View File

@ -240,13 +240,16 @@ private fun ScaffoldLayout(
) )
}.fastMap { it.measure(looseConstraints) } }.fastMap { it.measure(looseConstraints) }
val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height val bottomBarHeight = bottomBarPlaceables
.fastMaxBy { it.height }
?.height
?.takeIf { it != 0 }
val fabOffsetFromBottom = fabPlacement?.let { val fabOffsetFromBottom = fabPlacement?.let {
max(bottomBarHeight ?: 0, bottomInset) + it.height + FabSpacing.roundToPx() max(bottomBarHeight ?: 0, bottomInset) + it.height + FabSpacing.roundToPx()
} }
val snackbarOffsetFromBottom = if (snackbarHeight != 0) { val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight ?: bottomInset) snackbarHeight + (fabOffsetFromBottom ?: max(bottomBarHeight ?: 0, bottomInset))
} else { } else {
0 0
} }

View File

@ -60,6 +60,9 @@ fun LibraryContent(
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) } var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
if (showPageTabs && categories.size > 1) { if (showPageTabs && categories.size > 1) {
if (categories.size <= pagerState.currentPage) {
pagerState.currentPage = categories.size - 1
}
LibraryTabs( LibraryTabs(
categories = categories, categories = categories,
currentPageIndex = pagerState.currentPage, currentPageIndex = pagerState.currentPage,

View File

@ -40,6 +40,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil.imageLoader import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Size import coil.size.Size
import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
@ -162,6 +163,7 @@ fun MangaCoverDialog(
val request = ImageRequest.Builder(view.context) val request = ImageRequest.Builder(view.context)
.data(coverDataProvider()) .data(coverDataProvider())
.size(Size.ORIGINAL) .size(Size.ORIGINAL)
.memoryCachePolicy(CachePolicy.DISABLED)
.target { drawable -> .target { drawable ->
// Copy bitmap in case it came from memory cache // Copy bitmap in case it came from memory cache
// Because SSIV needs to thoroughly read the image // Because SSIV needs to thoroughly read the image

View File

@ -339,7 +339,6 @@ class Downloader(
?.filter { it.name!!.endsWith(".tmp") } ?.filter { it.name!!.endsWith(".tmp") }
?.forEach { it.delete() } ?.forEach { it.delete() }
download.downloadedImages = 0
download.status = Download.State.DOWNLOADING download.status = Download.State.DOWNLOADING
} }
// Get all the URLs to the source images, fetch pages if necessary // Get all the URLs to the source images, fetch pages if necessary
@ -403,7 +402,6 @@ class Downloader(
} }
page.uri = file.uri page.uri = file.uri
page.progress = 100 page.progress = 100
download.downloadedImages++
page.status = Page.State.READY page.status = Page.State.READY
} }
.map { page } .map { page }

View File

@ -21,9 +21,8 @@ data class Download(
val totalProgress: Int val totalProgress: Int
get() = pages?.sumOf(Page::progress) ?: 0 get() = pages?.sumOf(Page::progress) ?: 0
@Volatile val downloadedImages: Int
@Transient get() = pages?.count { it.status == Page.State.READY } ?: 0
var downloadedImages: Int = 0
@Volatile @Volatile
@Transient @Transient

View File

@ -158,7 +158,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc" private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0" private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
private const val baseUrl = "https://shikimori.one" private const val baseUrl = "https://shikimori.me"
private const val apiUrl = "$baseUrl/api" private const val apiUrl = "$baseUrl/api"
private const val oauthUrl = "$baseUrl/oauth/token" private const val oauthUrl = "$baseUrl/oauth/token"
private const val loginUrl = "$baseUrl/oauth/authorize" private const val loginUrl = "$baseUrl/oauth/authorize"

View File

@ -126,8 +126,8 @@ internal object ExtensionLoader {
} }
// Validate lib version // Validate lib version
val libVersion = versionName.substringBeforeLast('.').toDouble() val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) { if (libVersion == null || libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
logcat(LogPriority.WARN) { logcat(LogPriority.WARN) {
"Lib version is $libVersion, while only versions " + "Lib version is $libVersion, while only versions " +
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed" "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
@ -136,7 +136,6 @@ internal object ExtensionLoader {
} }
val signatureHash = getSignatureHash(pkgInfo) val signatureHash = getSignatureHash(pkgInfo)
if (signatureHash == null) { if (signatureHash == null) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return LoadResult.Error return LoadResult.Error

View File

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

View File

@ -61,7 +61,7 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen {
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(text = Injekt.get<SourceManager>().get(sourceId)!!.toString()) }, title = { Text(text = Injekt.get<SourceManager>().getOrStub(sourceId).toString()) },
navigationIcon = { navigationIcon = {
IconButton(onClick = navigator::pop) { IconButton(onClick = navigator::pop) {
Icon( Icon(

View File

@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -23,6 +25,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastForEachIndexed
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import eu.kanade.domain.category.interactor.SetMangaCategories import eu.kanade.domain.category.interactor.SetMangaCategories
@ -111,7 +114,9 @@ internal fun MigrateDialog(
} }
}, },
confirmButton = { confirmButton = {
Row { FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
TextButton( TextButton(
onClick = { onClick = {
onClickTitle() onClickTitle()

View File

@ -49,7 +49,7 @@ class MigrateSearchScreenModel(
.filter { it.lang in enabledLanguages } .filter { it.lang in enabledLanguages }
.filterNot { "${it.id}" in disabledSources } .filterNot { "${it.id}" in disabledSources }
.sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" })) .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?) { 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.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.MissingSourceScreen
import eu.kanade.presentation.browse.components.BrowseSourceToolbar import eu.kanade.presentation.browse.components.BrowseSourceToolbar
import eu.kanade.presentation.browse.components.RemoveMangaDialog import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.components.ChangeCategoryDialog import eu.kanade.presentation.components.ChangeCategoryDialog
@ -48,7 +49,9 @@ import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.AssistContentScreen
import eu.kanade.presentation.util.padding import eu.kanade.presentation.util.padding
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen
@ -73,17 +76,10 @@ data class BrowseSourceScreen(
@Composable @Composable
override fun Content() { 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 { BrowseSourceScreenModel(sourceId, listingQuery) } val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId, listingQuery) }
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() } val navigator = LocalNavigator.currentOrThrow
val navigateUp: () -> Unit = { val navigateUp: () -> Unit = {
when { when {
!state.isUserQuery && state.toolbarQuery != null -> screenModel.setToolbarQuery(null) !state.isUserQuery && state.toolbarQuery != null -> screenModel.setToolbarQuery(null)
@ -91,8 +87,21 @@ data class BrowseSourceScreen(
} }
} }
val onHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) } if (screenModel.source is SourceManager.StubSource) {
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(LocalSource.HELP_URL) }
val onWebViewClick = f@{ val onWebViewClick = f@{
val source = screenModel.source as? HttpSource ?: return@f val source = screenModel.source as? HttpSource ?: return@f
navigator.push( navigator.push(
@ -147,7 +156,7 @@ data class BrowseSourceScreen(
Text(text = stringResource(R.string.popular)) Text(text = stringResource(R.string.popular))
}, },
) )
if (screenModel.source.supportsLatest) { if ((screenModel.source as CatalogueSource).supportsLatest) {
FilterChip( FilterChip(
selected = state.listing == Listing.Latest, selected = state.listing == Listing.Latest,
onClick = { onClick = {

View File

@ -102,23 +102,25 @@ class BrowseSourceScreenModel(
var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope) var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope)
val source = sourceManager.get(sourceId) as CatalogueSource val source = sourceManager.getOrStub(sourceId)
init { init {
mutableState.update { if (source is CatalogueSource) {
var query: String? = null mutableState.update {
var listing = it.listing var query: String? = null
var listing = it.listing
if (listing is Listing.Search) { if (listing is Listing.Search) {
query = listing.query query = listing.query
listing = Listing.Search(query, source.getFilterList()) listing = Listing.Search(query, source.getFilterList())
}
it.copy(
listing = listing,
filters = source.getFilterList(),
toolbarQuery = query,
)
} }
it.copy(
listing = listing,
filters = source.getFilterList(),
toolbarQuery = query,
)
} }
} }
@ -162,6 +164,8 @@ class BrowseSourceScreenModel(
} }
fun resetFilters() { fun resetFilters() {
if (source !is CatalogueSource) return
mutableState.update { it.copy(filters = source.getFilterList()) } mutableState.update { it.copy(filters = source.getFilterList()) }
} }
@ -170,6 +174,8 @@ class BrowseSourceScreenModel(
} }
fun search(query: String? = null, filters: FilterList? = null) { fun search(query: String? = null, filters: FilterList? = null) {
if (source !is CatalogueSource) return
val input = state.value.listing as? Listing.Search val input = state.value.listing as? Listing.Search
?: Listing.Search(query = null, filters = source.getFilterList()) ?: Listing.Search(query = null, filters = source.getFilterList())
@ -185,6 +191,8 @@ class BrowseSourceScreenModel(
} }
fun searchGenre(genreName: String) { fun searchGenre(genreName: String) {
if (source !is CatalogueSource) return
val defaultFilters = source.getFilterList() val defaultFilters = source.getFilterList()
var genreExists = false var genreExists = false

View File

@ -8,7 +8,6 @@ import androidx.compose.animation.fadeOut
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
@ -226,7 +225,6 @@ object DownloadQueueScreen : Screen {
} }
}, },
expanded = fabExpanded, expanded = fabExpanded,
modifier = Modifier.navigationBarsPadding(),
) )
} }
}, },

View File

@ -3,12 +3,14 @@ package eu.kanade.tachiyomi.ui.main
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.app.SearchManager import android.app.SearchManager
import android.app.assist.AssistContent import android.app.assist.AssistContent
import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -41,6 +43,7 @@ import androidx.core.animation.doOnEnd
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen import androidx.core.splashscreen.SplashScreen
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.util.Consumer
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
@ -85,7 +88,10 @@ import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setComposeContent import eu.kanade.tachiyomi.util.view.setComposeContent
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -119,8 +125,7 @@ class MainActivity : BaseActivity() {
*/ */
private var settingsSheet: LibrarySettingsSheet? = null private var settingsSheet: LibrarySettingsSheet? = null
private var isHandlingShortcut: Boolean = false private var navigator: Navigator? = null
private lateinit var navigator: Navigator
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// Prevent splash screen showing up on configuration changes // Prevent splash screen showing up on configuration changes
@ -210,7 +215,7 @@ class MainActivity : BaseActivity() {
if (savedInstanceState == null) { if (savedInstanceState == null) {
// Set start screen // Set start screen
handleIntentAction(intent) handleIntentAction(intent, navigator)
// Reset Incognito Mode on relaunch // Reset Incognito Mode on relaunch
preferences.incognitoMode().set(false) preferences.incognitoMode().set(false)
@ -257,6 +262,7 @@ class MainActivity : BaseActivity() {
} }
CheckForUpdate() CheckForUpdate()
HandleOnNewIntent(context = context, navigator = navigator)
} }
var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) } var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) }
@ -288,7 +294,7 @@ class MainActivity : BaseActivity() {
override fun onProvideAssistContent(outContent: AssistContent) { override fun onProvideAssistContent(outContent: AssistContent) {
super.onProvideAssistContent(outContent) super.onProvideAssistContent(outContent)
when (val screen = navigator.lastItem) { when (val screen = navigator?.lastItem) {
is AssistContentScreen -> { is AssistContentScreen -> {
screen.onProvideAssistUrl()?.let { outContent.webUri = it.toUri() } screen.onProvideAssistUrl()?.let { outContent.webUri = it.toUri() }
} }
@ -319,6 +325,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 @Composable
private fun CheckForUpdate() { private fun CheckForUpdate() {
val context = LocalContext.current val context = LocalContext.current
@ -387,37 +405,26 @@ class MainActivity : BaseActivity() {
} }
} }
override fun onNewIntent(intent: Intent) { private fun handleIntentAction(intent: Intent, navigator: Navigator): Boolean {
lifecycleScope.launch {
val handle = handleIntentAction(intent)
if (!handle) {
super.onNewIntent(intent)
}
}
}
private suspend fun handleIntentAction(intent: Intent): Boolean {
val notificationId = intent.getIntExtra("notificationId", -1) val notificationId = intent.getIntExtra("notificationId", -1)
if (notificationId > -1) { if (notificationId > -1) {
NotificationReceiver.dismissNotification(applicationContext, notificationId, intent.getIntExtra("groupId", 0)) NotificationReceiver.dismissNotification(applicationContext, notificationId, intent.getIntExtra("groupId", 0))
} }
isHandlingShortcut = true val tabToOpen = when (intent.action) {
Constants.SHORTCUT_LIBRARY -> HomeScreen.Tab.Library()
when (intent.action) {
Constants.SHORTCUT_LIBRARY -> HomeScreen.openTab(HomeScreen.Tab.Library())
Constants.SHORTCUT_MANGA -> { Constants.SHORTCUT_MANGA -> {
val idToOpen = intent.extras?.getLong(Constants.MANGA_EXTRA) ?: return false val idToOpen = intent.extras?.getLong(Constants.MANGA_EXTRA) ?: return false
navigator.popUntilRoot() navigator.popUntilRoot()
HomeScreen.openTab(HomeScreen.Tab.Library(idToOpen)) HomeScreen.Tab.Library(idToOpen)
} }
Constants.SHORTCUT_UPDATES -> HomeScreen.openTab(HomeScreen.Tab.Updates) Constants.SHORTCUT_UPDATES -> HomeScreen.Tab.Updates
Constants.SHORTCUT_HISTORY -> HomeScreen.openTab(HomeScreen.Tab.History) Constants.SHORTCUT_HISTORY -> HomeScreen.Tab.History
Constants.SHORTCUT_SOURCES -> HomeScreen.openTab(HomeScreen.Tab.Browse(false)) Constants.SHORTCUT_SOURCES -> HomeScreen.Tab.Browse(false)
Constants.SHORTCUT_EXTENSIONS -> HomeScreen.openTab(HomeScreen.Tab.Browse(true)) Constants.SHORTCUT_EXTENSIONS -> HomeScreen.Tab.Browse(true)
Constants.SHORTCUT_DOWNLOADS -> { Constants.SHORTCUT_DOWNLOADS -> {
navigator.popUntilRoot() 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" -> { Intent.ACTION_SEARCH, Intent.ACTION_SEND, "com.google.android.gms.actions.SEARCH_ACTION" -> {
// If the intent match the "standard" Android search intent // If the intent match the "standard" Android search intent
@ -429,6 +436,7 @@ class MainActivity : BaseActivity() {
navigator.popUntilRoot() navigator.popUntilRoot()
navigator.push(GlobalSearchScreen(query)) navigator.push(GlobalSearchScreen(query))
} }
null
} }
INTENT_SEARCH -> { INTENT_SEARCH -> {
val query = intent.getStringExtra(INTENT_SEARCH_QUERY) val query = intent.getStringExtra(INTENT_SEARCH_QUERY)
@ -437,15 +445,16 @@ class MainActivity : BaseActivity() {
navigator.popUntilRoot() navigator.popUntilRoot()
navigator.push(GlobalSearchScreen(query, filter)) navigator.push(GlobalSearchScreen(query, filter))
} }
null
} }
else -> { else -> return false
isHandlingShortcut = false }
return false
} if (tabToOpen != null) {
lifecycleScope.launch { HomeScreen.openTab(tabToOpen) }
} }
ready = true ready = true
isHandlingShortcut = false
return true return true
} }
@ -456,7 +465,7 @@ class MainActivity : BaseActivity() {
} }
override fun onBackPressed() { override fun onBackPressed() {
if (navigator.size == 1 && if (navigator?.size == 1 &&
!onBackPressedDispatcher.hasEnabledCallbacks() && !onBackPressedDispatcher.hasEnabledCallbacks() &&
libraryPreferences.autoClearChapterCache().get() libraryPreferences.autoClearChapterCache().get()
) { ) {

View File

@ -977,6 +977,14 @@ class MangaInfoScreenModel(
} }
} }
} }
private val Throwable.snackbarMessage: String
get() = when (val className = this::class.simpleName) {
null -> message ?: ""
"SourceNotInstalledException" -> context.getString(R.string.loader_not_implemented_error)
"Exception", "HttpException", "IOException" -> message ?: className
else -> "$className: $message"
}
} }
sealed class MangaScreenState { sealed class MangaScreenState {
@ -1055,10 +1063,3 @@ val chapterDecimalFormat = DecimalFormat(
DecimalFormatSymbols() DecimalFormatSymbols()
.apply { decimalSeparator = '.' }, .apply { decimalSeparator = '.' },
) )
private val Throwable.snackbarMessage: String
get() = when (val className = this::class.simpleName) {
null -> message ?: ""
"Exception", "HttpException", "IOException", "SourceNotInstalledException" -> message ?: className
else -> "$className: $message"
}

View File

@ -99,10 +99,6 @@ import uy.kohesive.injekt.injectLazy
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
/**
* Activity containing the reader of Tachiyomi. This activity is mostly a container of the
* viewers, to which calls from the presenter or UI events are delegated.
*/
class ReaderActivity : BaseActivity() { class ReaderActivity : BaseActivity() {
companion object { companion object {
@ -661,7 +657,7 @@ class ReaderActivity : BaseActivity() {
* Called from the presenter when a manga is ready. Used to instantiate the appropriate viewer * Called from the presenter when a manga is ready. Used to instantiate the appropriate viewer
* and the toolbar title. * and the toolbar title.
*/ */
fun setManga(manga: Manga) { private fun setManga(manga: Manga) {
val prevViewer = viewer val prevViewer = viewer
val viewerMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false)) val viewerMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false))
@ -776,7 +772,7 @@ class ReaderActivity : BaseActivity() {
* Called from the presenter if the initial load couldn't load the pages of the chapter. In * Called from the presenter if the initial load couldn't load the pages of the chapter. In
* this case the activity is closed and a toast is shown to the user. * this case the activity is closed and a toast is shown to the user.
*/ */
fun setInitialChapterError(error: Throwable) { private fun setInitialChapterError(error: Throwable) {
logcat(LogPriority.ERROR, error) logcat(LogPriority.ERROR, error)
finish() finish()
toast(error.message) toast(error.message)
@ -871,7 +867,7 @@ class ReaderActivity : BaseActivity() {
* the viewer is reaching the beginning or end of a chapter or the transition page is active. * the viewer is reaching the beginning or end of a chapter or the transition page is active.
*/ */
fun requestPreloadChapter(chapter: ReaderChapter) { fun requestPreloadChapter(chapter: ReaderChapter) {
lifecycleScope.launch { viewModel.preloadChapter(chapter) } lifecycleScope.launchIO { viewModel.preloadChapter(chapter) }
} }
/** /**

View File

@ -177,10 +177,11 @@ class ReaderViewModel(
}.run { }.run {
if (readerPreferences.skipDupe().get()) { if (readerPreferences.skipDupe().get()) {
groupBy { it.chapterNumber } groupBy { it.chapterNumber }
.mapValues { (_, chapters) -> .map { (_, chapters) ->
chapters.find { it.id == chapterId || it.scanlator == selectedChapter.scanlator } ?: chapters.first() chapters.find { it.id == selectedChapter.id }
?: chapters.find { it.scanlator == selectedChapter.scanlator }
?: chapters.first()
} }
.values
} else { } else {
this this
} }
@ -200,17 +201,6 @@ class ReaderViewModel(
private val incognitoMode = preferences.incognitoMode().get() private val incognitoMode = preferences.incognitoMode().get()
override fun onCleared() {
val currentChapters = state.value.viewerChapters
if (currentChapters != null) {
currentChapters.unref()
saveReadingProgress(currentChapters.currChapter)
chapterToDownload?.let {
downloadManager.addDownloadsToStartOfQueue(listOf(it))
}
}
}
init { init {
// To save state // To save state
state.map { it.viewerChapters?.currChapter } state.map { it.viewerChapters?.currChapter }
@ -225,6 +215,17 @@ class ReaderViewModel(
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
override fun onCleared() {
val currentChapters = state.value.viewerChapters
if (currentChapters != null) {
currentChapters.unref()
saveReadingProgress(currentChapters.currChapter)
chapterToDownload?.let {
downloadManager.addDownloadsToStartOfQueue(listOf(it))
}
}
}
/** /**
* Called when the user pressed the back button and is going to leave the reader. Used to * Called when the user pressed the back button and is going to leave the reader. Used to
* trigger deletion of the downloaded chapters. * trigger deletion of the downloaded chapters.
@ -337,10 +338,11 @@ class ReaderViewModel(
} }
/** /**
* Called when the user is going to load the prev/next chapter through the menu button. * Called when the user is going to load the prev/next chapter through the toolbar buttons.
*/ */
private suspend fun loadAdjacent(chapter: ReaderChapter) { private suspend fun loadAdjacent(chapter: ReaderChapter) {
val loader = loader ?: return val loader = loader ?: return
saveCurrentChapterReadingProgress()
logcat { "Loading adjacent ${chapter.chapter.url}" } logcat { "Loading adjacent ${chapter.chapter.url}" }
@ -364,6 +366,10 @@ class ReaderViewModel(
* that the user doesn't have to wait too long to continue reading. * that the user doesn't have to wait too long to continue reading.
*/ */
private suspend fun preload(chapter: ReaderChapter) { private suspend fun preload(chapter: ReaderChapter) {
if (chapter.state is ReaderChapter.State.Loaded || chapter.state == ReaderChapter.State.Loading) {
return
}
if (chapter.pageLoader is HttpPageLoader) { if (chapter.pageLoader is HttpPageLoader) {
val manga = manga ?: return val manga = manga ?: return
val dbChapter = chapter.chapter val dbChapter = chapter.chapter
@ -383,20 +389,17 @@ class ReaderViewModel(
return return
} }
logcat { "Preloading ${chapter.chapter.url}" }
val loader = loader ?: return val loader = loader ?: return
withIOContext { try {
try { logcat { "Preloading ${chapter.chapter.url}" }
loader.loadChapter(chapter) loader.loadChapter(chapter)
} catch (e: Throwable) { } catch (e: Throwable) {
if (e is CancellationException) { if (e is CancellationException) {
throw e throw e
}
return@withIOContext
} }
eventChannel.trySend(Event.ReloadViewerChapters) return
} }
eventChannel.trySend(Event.ReloadViewerChapters)
} }
/** /**
@ -580,7 +583,12 @@ class ReaderViewModel(
val sChapter = getCurrentChapter()?.chapter ?: return null val sChapter = getCurrentChapter()?.chapter ?: return null
val source = getSource() ?: 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

@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setComposeContent import eu.kanade.tachiyomi.util.view.setComposeContent
import logcat.LogPriority
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -47,7 +48,11 @@ class WebViewActivity : BaseActivity() {
var headers = emptyMap<String, String>() var headers = emptyMap<String, String>()
(sourceManager.get(intent.extras!!.getLong(SOURCE_KEY)) as? HttpSource)?.let { source -> (sourceManager.get(intent.extras!!.getLong(SOURCE_KEY)) as? HttpSource)?.let { source ->
headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } try {
headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to build headers" }
}
} }
setComposeContent { setComposeContent {

View File

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import logcat.LogPriority
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -25,7 +26,11 @@ class WebViewScreenModel(
init { init {
sourceId?.let { sourceManager.get(it) as? HttpSource }?.let { source -> sourceId?.let { sourceManager.get(it) as? HttpSource }?.let { source ->
headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } try {
headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to build headers" }
}
} }
} }

View File

@ -3,25 +3,22 @@ import org.gradle.api.Task
import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.TaskContainerScope import org.gradle.kotlin.dsl.TaskContainerScope
private val emptyResourcesElement = "<resources>\\s*</resources>|<resources/>".toRegex()
private val valuesPrefix = "values(-(b\\+)?)?".toRegex()
fun TaskContainerScope.registerLocalesConfigTask(project: Project): TaskProvider<Task> { fun TaskContainerScope.registerLocalesConfigTask(project: Project): TaskProvider<Task> {
return with(project) { return with(project) {
register("generateLocalesConfig") { register("generateLocalesConfig") {
val emptyResourcesElement = "<resources>\\s*</resources>|<resources/>".toRegex()
val valuesPrefix = "values-?".toRegex()
val languages = fileTree("$projectDir/src/main/res/") val languages = fileTree("$projectDir/src/main/res/")
.matching { .matching { include("**/strings.xml") }
include("**/strings.xml") .filterNot { it.readText().contains(emptyResourcesElement) }
}
.filterNot {
it.readText().contains(emptyResourcesElement)
}
.map { it.parentFile.name } .map { it.parentFile.name }
.sorted() .sorted()
.joinToString(separator = "\n") { .joinToString(separator = "\n") {
val language = it val language = it
.replace(valuesPrefix, "") .replace(valuesPrefix, "")
.replace("-r", "-") .replace("-r", "-")
.replace("+", "-")
.takeIf(String::isNotBlank) ?: "en" .takeIf(String::isNotBlank) ?: "en"
" <locale android:name=\"$language\"/>" " <locale android:name=\"$language\"/>"
} }

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -29,6 +30,7 @@ class NetworkHelper(context: Context) {
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.callTimeout(2, TimeUnit.MINUTES) .callTimeout(2, TimeUnit.MINUTES)
.addInterceptor(UncaughtExceptionInterceptor())
.addInterceptor(userAgentInterceptor) .addInterceptor(userAgentInterceptor)
if (preferences.verboseLogging().get()) { if (preferences.verboseLogging().get()) {

View File

@ -17,6 +17,6 @@ class NetworkPreferences(
} }
fun defaultUserAgent(): Preference<String> { fun defaultUserAgent(): Preference<String> {
return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0") return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0")
} }
} }

View File

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.network.interceptor
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
/**
* Catches any uncaught exceptions from later in the chain and rethrows as a non-fatal
* IOException to avoid catastrophic failure.
*
* This should be the first interceptor in the client.
*
* See https://square.github.io/okhttp/4.x/okhttp/okhttp3/-interceptor/
*/
class UncaughtExceptionInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
} catch (e: Exception) {
throw IOException(e)
}
}
}

View File

@ -12,7 +12,7 @@ import tachiyomi.core.util.system.logcat
object WebViewUtil { object WebViewUtil {
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome" const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
const val MINIMUM_WEBVIEW_VERSION = 105 const val MINIMUM_WEBVIEW_VERSION = 108
fun supportsWebView(context: Context): Boolean { fun supportsWebView(context: Context): Boolean {
try { try {

View File

@ -24,7 +24,7 @@ sealed class LibraryDisplayMode(
} }
companion object { companion object {
val values = setOf(CompactGrid, ComfortableGrid, List, CoverOnlyGrid) val values by lazy { setOf(CompactGrid, ComfortableGrid, List, CoverOnlyGrid) }
val default = CompactGrid val default = CompactGrid
fun valueOf(flag: Long?): LibraryDisplayMode { fun valueOf(flag: Long?): LibraryDisplayMode {

View File

@ -65,8 +65,8 @@ data class LibrarySort(
} }
companion object { companion object {
val types = setOf(Type.Alphabetical, Type.LastRead, Type.LastUpdate, Type.UnreadCount, Type.TotalChapters, Type.LatestChapter, Type.ChapterFetchDate, Type.DateAdded) val types by lazy { setOf(Type.Alphabetical, Type.LastRead, Type.LastUpdate, Type.UnreadCount, Type.TotalChapters, Type.LatestChapter, Type.ChapterFetchDate, Type.DateAdded) }
val directions = setOf(Direction.Ascending, Direction.Descending) val directions by lazy { setOf(Direction.Ascending, Direction.Descending) }
val default = LibrarySort(Type.Alphabetical, Direction.Ascending) val default = LibrarySort(Type.Alphabetical, Direction.Ascending)
fun valueOf(flag: Long): LibrarySort { fun valueOf(flag: Long): LibrarySort {

View File

@ -46,7 +46,7 @@ coil-core = { module = "io.coil-kt:coil", version.ref = "coil_version" }
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil_version" } coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil_version" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil_version" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil_version" }
subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:846abe0" subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:c8e2650"
image-decoder = "com.github.tachiyomiorg:image-decoder:7879b45" image-decoder = "com.github.tachiyomiorg:image-decoder:7879b45"
natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"