mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-27 19:47:51 +02:00
Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
c615f4d458 | |||
9e09a20e65 | |||
7115a9b9fe | |||
fd8b97fc87 | |||
4dd67e4348 | |||
10973bf3cd | |||
934ed0551a | |||
38428c6ebe | |||
bf85e147e7 | |||
d2dd34c2e5 | |||
c4ab2b4675 | |||
aa2ec5940f | |||
79323de326 | |||
08e6487a9a | |||
4498b10a10 | |||
6f2bb18d72 | |||
b690de55e5 | |||
83fda20078 | |||
f656a37045 | |||
c58b495433 | |||
242aeb6a68 | |||
d9969cea8a | |||
d61db5931e | |||
0ea3ac9807 | |||
f9e43f574f | |||
5ef11e61d0 | |||
48546c3db4 | |||
4d87ed496c | |||
06d12e6562 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -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
|
||||||
|
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -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
|
||||||
|
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -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
|
||||||
|
@ -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()}\"")
|
||||||
|
@ -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))
|
||||||
|
@ -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 {
|
||||||
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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 }
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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()
|
||||||
|
@ -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?) {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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()
|
||||||
) {
|
) {
|
||||||
|
@ -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"
|
|
||||||
}
|
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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 {
|
||||||
|
@ -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" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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\"/>"
|
||||||
}
|
}
|
||||||
|
@ -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()) {
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
@ -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"
|
||||||
|
Reference in New Issue
Block a user