Remove dependency injection from core module and data module from presentation-widget module

Includes side effects:
- No longer need to restart app for user agent string change to take effect
- parseAs extension function requires a Json instance in the calling context, which doesn't necessarily need to be the default one provided by Injekt
This commit is contained in:
arkon 2023-02-20 19:02:38 -05:00
parent 10d7349506
commit 93523ef50b
34 changed files with 576 additions and 433 deletions

View file

@ -283,6 +283,7 @@ tasks {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> { withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
"-Xcontext-receivers",
"-opt-in=coil.annotation.ExperimentalCoilApi", "-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi", "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",

View file

@ -222,7 +222,7 @@ object SettingsAdvancedScreen : SearchableSettings {
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_clear_cookies), title = stringResource(R.string.pref_clear_cookies),
onClick = { onClick = {
networkHelper.cookieManager.removeAll() networkHelper.cookieJar.removeAll()
context.toast(R.string.cookies_cleared) context.toast(R.string.cookies_cleared)
}, },
), ),
@ -280,7 +280,6 @@ object SettingsAdvancedScreen : SearchableSettings {
context.toast(R.string.error_user_agent_string_invalid) context.toast(R.string.error_user_agent_string_invalid)
return@EditTextPreference false return@EditTextPreference false
} }
context.toast(R.string.requires_app_restart)
true true
}, },
), ),

View file

@ -118,7 +118,7 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { ChapterCache(app) } addSingletonFactory { ChapterCache(app) }
addSingletonFactory { CoverCache(app) } addSingletonFactory { CoverCache(app) }
addSingletonFactory { NetworkHelper(app) } addSingletonFactory { NetworkHelper(app, get()) }
addSingletonFactory { JavaScriptEngine(app) } addSingletonFactory { JavaScriptEngine(app) }
addSingletonFactory { SourceManager(app, get(), get()) } addSingletonFactory { SourceManager(app, get(), get()) }

View file

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
@ -24,11 +25,14 @@ import kotlinx.serialization.json.putJsonObject
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.util.Calendar import java.util.Calendar
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val json: Json by injectLazy()
private val authClient = client.newBuilder() private val authClient = client.newBuilder()
.addInterceptor(interceptor) .addInterceptor(interceptor)
.rateLimit(permits = 85, period = 1, unit = TimeUnit.MINUTES) .rateLimit(permits = 85, period = 1, unit = TimeUnit.MINUTES)
@ -53,19 +57,21 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("status", track.toAnilistStatus()) put("status", track.toAnilistStatus())
} }
} }
authClient.newCall( with(json) {
POST( authClient.newCall(
apiUrl, POST(
body = payload.toString().toRequestBody(jsonMime), apiUrl,
), body = payload.toString().toRequestBody(jsonMime),
) ),
.awaitSuccess() )
.parseAs<JsonObject>() .awaitSuccess()
.let { .parseAs<JsonObject>()
track.library_id = .let {
it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long track.library_id =
track it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
} track
}
}
} }
} }
@ -137,21 +143,23 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("query", search) put("query", search)
} }
} }
authClient.newCall( with(json) {
POST( authClient.newCall(
apiUrl, POST(
body = payload.toString().toRequestBody(jsonMime), apiUrl,
), body = payload.toString().toRequestBody(jsonMime),
) ),
.awaitSuccess() )
.parseAs<JsonObject>() .awaitSuccess()
.let { response -> .parseAs<JsonObject>()
val data = response["data"]!!.jsonObject .let { response ->
val page = data["Page"]!!.jsonObject val data = response["data"]!!.jsonObject
val media = page["media"]!!.jsonArray val page = data["Page"]!!.jsonObject
val entries = media.map { jsonToALManga(it.jsonObject) } val media = page["media"]!!.jsonArray
entries.map { it.toTrack() } val entries = media.map { jsonToALManga(it.jsonObject) }
} entries.map { it.toTrack() }
}
}
} }
} }
@ -205,21 +213,23 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("manga_id", track.media_id) put("manga_id", track.media_id)
} }
} }
authClient.newCall( with(json) {
POST( authClient.newCall(
apiUrl, POST(
body = payload.toString().toRequestBody(jsonMime), apiUrl,
), body = payload.toString().toRequestBody(jsonMime),
) ),
.awaitSuccess() )
.parseAs<JsonObject>() .awaitSuccess()
.let { response -> .parseAs<JsonObject>()
val data = response["data"]!!.jsonObject .let { response ->
val page = data["Page"]!!.jsonObject val data = response["data"]!!.jsonObject
val media = page["mediaList"]!!.jsonArray val page = data["Page"]!!.jsonObject
val entries = media.map { jsonToALUserManga(it.jsonObject) } val media = page["mediaList"]!!.jsonArray
entries.firstOrNull()?.toTrack() val entries = media.map { jsonToALUserManga(it.jsonObject) }
} entries.firstOrNull()?.toTrack()
}
}
} }
} }
@ -247,22 +257,24 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
val payload = buildJsonObject { val payload = buildJsonObject {
put("query", query) put("query", query)
} }
authClient.newCall( with(json) {
POST( authClient.newCall(
apiUrl, POST(
body = payload.toString().toRequestBody(jsonMime), apiUrl,
), body = payload.toString().toRequestBody(jsonMime),
) ),
.awaitSuccess() )
.parseAs<JsonObject>() .awaitSuccess()
.let { .parseAs<JsonObject>()
val data = it["data"]!!.jsonObject .let {
val viewer = data["Viewer"]!!.jsonObject val data = it["data"]!!.jsonObject
Pair( val viewer = data["Viewer"]!!.jsonObject
viewer["id"]!!.jsonPrimitive.int, Pair(
viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content, viewer["id"]!!.jsonPrimitive.int,
) viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content,
} )
}
}
} }
} }

View file

@ -118,10 +118,12 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
suspend fun findLibManga(track: Track): Track? { suspend fun findLibManga(track: Track): Track? {
return withIOContext { return withIOContext {
authClient.newCall(GET("$apiUrl/subject/${track.media_id}")) with(json) {
.awaitSuccess() authClient.newCall(GET("$apiUrl/subject/${track.media_id}"))
.parseAs<JsonObject>() .awaitSuccess()
.let { jsonToSearch(it) } .parseAs<JsonObject>()
.let { jsonToSearch(it) }
}
} }
} }
@ -155,9 +157,11 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
suspend fun accessToken(code: String): OAuth { suspend fun accessToken(code: String): OAuth {
return withIOContext { return withIOContext {
client.newCall(accessTokenRequest(code)) with(json) {
.awaitSuccess() client.newCall(accessTokenRequest(code))
.parseAs() .awaitSuccess()
.parseAs()
}
} }
} }
@ -181,7 +185,6 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
private const val loginUrl = "https://bgm.tv/oauth/authorize" private const val loginUrl = "https://bgm.tv/oauth/authorize"
private const val redirectUrl = "tachiyomi://bangumi-auth" private const val redirectUrl = "tachiyomi://bangumi-auth"
private const val baseMangaUrl = "$apiUrl/mangas"
fun authUrl(): Uri = fun authUrl(): Uri =
loginUrl.toUri().buildUpon() loginUrl.toUri().buildUpon()

View file

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import logcat.LogPriority import logcat.LogPriority
import okhttp3.Dns import okhttp3.Dns
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -13,11 +14,14 @@ import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor) { class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor) {
private val json: Json by injectLazy()
private val authClient = client.newBuilder() private val authClient = client.newBuilder()
.dns(Dns.SYSTEM) .dns(Dns.SYSTEM)
.addInterceptor(interceptor) .addInterceptor(interceptor)
@ -39,18 +43,20 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()), body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()),
) )
try { try {
client.newCall(request).execute().use { with(json) {
when (it.code) { client.newCall(request).execute().use {
200 -> return it.parseAs<AuthenticationDto>().token when (it.code) {
401 -> { 200 -> return it.parseAs<AuthenticationDto>().token
logcat(LogPriority.WARN) { "Unauthorized / api key not valid: Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" } 401 -> {
throw IOException("Unauthorized / api key not valid") logcat(LogPriority.WARN) { "Unauthorized / api key not valid: Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" }
throw IOException("Unauthorized / api key not valid")
}
500 -> {
logcat(LogPriority.WARN) { "Error fetching JWT token. Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" }
throw IOException("Error fetching JWT token")
}
else -> {}
} }
500 -> {
logcat(LogPriority.WARN) { "Error fetching JWT token. Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" }
throw IOException("Error fetching JWT token")
}
else -> {}
} }
} }
// Not sure which one to catch // Not sure which one to catch
@ -86,9 +92,11 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
private fun getTotalChapters(url: String): Int { private fun getTotalChapters(url: String): Int {
val requestUrl = getApiVolumesUrl(url) val requestUrl = getApiVolumesUrl(url)
try { try {
val listVolumeDto = authClient.newCall(GET(requestUrl)) val listVolumeDto = with(json) {
.execute() authClient.newCall(GET(requestUrl))
.parseAs<List<VolumeDto>>() .execute()
.parseAs<List<VolumeDto>>()
}
var volumeNumber = 0 var volumeNumber = 0
var maxChapterNumber = 0 var maxChapterNumber = 0
for (volume in listVolumeDto) { for (volume in listVolumeDto) {
@ -110,12 +118,14 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
val serieId = getIdFromUrl(url) val serieId = getIdFromUrl(url)
val requestUrl = "${getApiFromUrl(url)}/Tachiyomi/latest-chapter?seriesId=$serieId" val requestUrl = "${getApiFromUrl(url)}/Tachiyomi/latest-chapter?seriesId=$serieId"
try { try {
authClient.newCall(GET(requestUrl)).execute().use { with(json) {
if (it.code == 200) { authClient.newCall(GET(requestUrl)).execute().use {
return it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat() if (it.code == 200) {
} return it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat()
if (it.code == 204) { }
return 0F if (it.code == 204) {
return 0F
}
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -127,9 +137,11 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
suspend fun getTrackSearch(url: String): TrackSearch = withIOContext { suspend fun getTrackSearch(url: String): TrackSearch = withIOContext {
try { try {
val serieDto: SeriesDto = authClient.newCall(GET(url)) val serieDto: SeriesDto = with(json) {
.awaitSuccess() authClient.newCall(GET(url))
.parseAs() .awaitSuccess()
.parseAs()
}
val track = serieDto.toTrack() val track = serieDto.toTrack()
track.apply { track.apply {

View file

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
@ -24,11 +25,14 @@ import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder import java.net.URLEncoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) { class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
private val json: Json by injectLazy()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun addLibManga(track: Track, userId: String): Track { suspend fun addLibManga(track: Track, userId: String): Track {
@ -57,22 +61,25 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
authClient.newCall( with(json) {
POST( authClient.newCall(
"${baseUrl}library-entries", POST(
headers = headersOf( "${baseUrl}library-entries",
"Content-Type", headers = headersOf(
"application/vnd.api+json", "Content-Type",
"application/vnd.api+json",
),
body = data.toString()
.toRequestBody("application/vnd.api+json".toMediaType()),
), ),
body = data.toString().toRequestBody("application/vnd.api+json".toMediaType()), )
), .awaitSuccess()
) .parseAs<JsonObject>()
.awaitSuccess() .let {
.parseAs<JsonObject>() track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long
.let { track
track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long }
track }
}
} }
} }
@ -92,35 +99,41 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
authClient.newCall( with(json) {
Request.Builder() authClient.newCall(
.url("${baseUrl}library-entries/${track.media_id}") Request.Builder()
.headers( .url("${baseUrl}library-entries/${track.media_id}")
headersOf( .headers(
"Content-Type", headersOf(
"application/vnd.api+json", "Content-Type",
), "application/vnd.api+json",
) ),
.patch(data.toString().toRequestBody("application/vnd.api+json".toMediaType())) )
.build(), .patch(
) data.toString().toRequestBody("application/vnd.api+json".toMediaType()),
.awaitSuccess() )
.parseAs<JsonObject>() .build(),
.let { )
track .awaitSuccess()
} .parseAs<JsonObject>()
.let {
track
}
}
} }
} }
suspend fun search(query: String): List<TrackSearch> { suspend fun search(query: String): List<TrackSearch> {
return withIOContext { return withIOContext {
authClient.newCall(GET(algoliaKeyUrl)) with(json) {
.awaitSuccess() authClient.newCall(GET(algoliaKeyUrl))
.parseAs<JsonObject>() .awaitSuccess()
.let { .parseAs<JsonObject>()
val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content .let {
algoliaSearch(key, query) val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content
} algoliaSearch(key, query)
}
}
} }
} }
@ -130,26 +143,28 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
put("params", "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$algoliaFilter") put("params", "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$algoliaFilter")
} }
client.newCall( with(json) {
POST( client.newCall(
algoliaUrl, POST(
headers = headersOf( algoliaUrl,
"X-Algolia-Application-Id", headers = headersOf(
algoliaAppId, "X-Algolia-Application-Id",
"X-Algolia-API-Key", algoliaAppId,
key, "X-Algolia-API-Key",
key,
),
body = jsonObject.toString().toRequestBody(jsonMime),
), ),
body = jsonObject.toString().toRequestBody(jsonMime), )
), .awaitSuccess()
) .parseAs<JsonObject>()
.awaitSuccess() .let {
.parseAs<JsonObject>() it["hits"]!!.jsonArray
.let { .map { KitsuSearchManga(it.jsonObject) }
it["hits"]!!.jsonArray .filter { it.subType != "novel" }
.map { KitsuSearchManga(it.jsonObject) } .map { it.toTrack() }
.filter { it.subType != "novel" } }
.map { it.toTrack() } }
}
} }
} }
@ -159,18 +174,20 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId") .encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId")
.appendQueryParameter("include", "manga") .appendQueryParameter("include", "manga")
.build() .build()
authClient.newCall(GET(url.toString())) with(json) {
.awaitSuccess() authClient.newCall(GET(url.toString()))
.parseAs<JsonObject>() .awaitSuccess()
.let { .parseAs<JsonObject>()
val data = it["data"]!!.jsonArray .let {
if (data.size > 0) { val data = it["data"]!!.jsonArray
val manga = it["included"]!!.jsonArray[0].jsonObject if (data.size > 0) {
KitsuLibManga(data[0].jsonObject, manga).toTrack() val manga = it["included"]!!.jsonArray[0].jsonObject
} else { KitsuLibManga(data[0].jsonObject, manga).toTrack()
null } else {
null
}
} }
} }
} }
} }
@ -180,18 +197,20 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.encodedQuery("filter[id]=${track.media_id}") .encodedQuery("filter[id]=${track.media_id}")
.appendQueryParameter("include", "manga") .appendQueryParameter("include", "manga")
.build() .build()
authClient.newCall(GET(url.toString())) with(json) {
.awaitSuccess() authClient.newCall(GET(url.toString()))
.parseAs<JsonObject>() .awaitSuccess()
.let { .parseAs<JsonObject>()
val data = it["data"]!!.jsonArray .let {
if (data.size > 0) { val data = it["data"]!!.jsonArray
val manga = it["included"]!!.jsonArray[0].jsonObject if (data.size > 0) {
KitsuLibManga(data[0].jsonObject, manga).toTrack() val manga = it["included"]!!.jsonArray[0].jsonObject
} else { KitsuLibManga(data[0].jsonObject, manga).toTrack()
throw Exception("Could not find manga") } else {
throw Exception("Could not find manga")
}
} }
} }
} }
} }
@ -204,9 +223,11 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.add("client_id", clientId) .add("client_id", clientId)
.add("client_secret", clientSecret) .add("client_secret", clientSecret)
.build() .build()
client.newCall(POST(loginUrl, body = formBody)) with(json) {
.awaitSuccess() client.newCall(POST(loginUrl, body = formBody))
.parseAs() .awaitSuccess()
.parseAs()
}
} }
} }
@ -215,12 +236,14 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
val url = "${baseUrl}users".toUri().buildUpon() val url = "${baseUrl}users".toUri().buildUpon()
.encodedQuery("filter[self]=true") .encodedQuery("filter[self]=true")
.build() .build()
authClient.newCall(GET(url.toString())) with(json) {
.awaitSuccess() authClient.newCall(GET(url.toString()))
.parseAs<JsonObject>() .awaitSuccess()
.let { .parseAs<JsonObject>()
it["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content .let {
} it["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content
}
}
} }
} }

View file

@ -26,25 +26,29 @@ class KomgaApi(private val client: OkHttpClient) {
suspend fun getTrackSearch(url: String): TrackSearch = suspend fun getTrackSearch(url: String): TrackSearch =
withIOContext { withIOContext {
try { try {
val track = if (url.contains(READLIST_API)) { val track = with(json) {
client.newCall(GET(url)) if (url.contains(READLIST_API)) {
.awaitSuccess() client.newCall(GET(url))
.parseAs<ReadListDto>() .awaitSuccess()
.toTrack() .parseAs<ReadListDto>()
} else { .toTrack()
client.newCall(GET(url)) } else {
.awaitSuccess() client.newCall(GET(url))
.parseAs<SeriesDto>() .awaitSuccess()
.toTrack() .parseAs<SeriesDto>()
.toTrack()
}
} }
val progress = client val progress = client
.newCall(GET("${url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi")) .newCall(GET("${url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi"))
.awaitSuccess().let { .awaitSuccess().let {
if (url.contains("/api/v1/series/")) { with(json) {
it.parseAs<ReadProgressV2Dto>() if (url.contains("/api/v1/series/")) {
} else { it.parseAs<ReadProgressV2Dto>()
it.parseAs<ReadProgressDto>().toV2() } else {
it.parseAs<ReadProgressDto>().toV2()
}
} }
} }

View file

@ -47,7 +47,7 @@ class MangaUpdatesApi(
} }
suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> { suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> {
val listItem = val listItem = with(json) {
authClient.newCall( authClient.newCall(
GET( GET(
url = "$baseUrl/v1/lists/series/${track.media_id}", url = "$baseUrl/v1/lists/series/${track.media_id}",
@ -55,6 +55,7 @@ class MangaUpdatesApi(
) )
.awaitSuccess() .awaitSuccess()
.parseAs<ListItem>() .parseAs<ListItem>()
}
val rating = getSeriesRating(track) val rating = getSeriesRating(track)
@ -111,13 +112,15 @@ class MangaUpdatesApi(
suspend fun getSeriesRating(track: Track): Rating? { suspend fun getSeriesRating(track: Track): Rating? {
return try { return try {
authClient.newCall( with(json) {
GET( authClient.newCall(
url = "$baseUrl/v1/series/${track.media_id}/rating", GET(
), url = "$baseUrl/v1/series/${track.media_id}/rating",
) ),
.awaitSuccess() )
.parseAs<Rating>() .awaitSuccess()
.parseAs<Rating>()
}
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@ -156,20 +159,22 @@ class MangaUpdatesApi(
}, },
) )
} }
return client.newCall( return with(json) {
POST( client.newCall(
url = "$baseUrl/v1/series/search", POST(
body = body.toString().toRequestBody(contentType), url = "$baseUrl/v1/series/search",
), body = body.toString().toRequestBody(contentType),
) ),
.awaitSuccess() )
.parseAs<JsonObject>() .awaitSuccess()
.let { obj -> .parseAs<JsonObject>()
obj["results"]?.jsonArray?.map { element -> .let { obj ->
json.decodeFromJsonElement<Record>(element.jsonObject["record"]!!) obj["results"]?.jsonArray?.map { element ->
json.decodeFromJsonElement<Record>(element.jsonObject["record"]!!)
}
} }
} .orEmpty()
.orEmpty() }
} }
suspend fun authenticate(username: String, password: String): Context? { suspend fun authenticate(username: String, password: String): Context? {
@ -177,21 +182,23 @@ class MangaUpdatesApi(
put("username", username) put("username", username)
put("password", password) put("password", password)
} }
return client.newCall( return with(json) {
PUT( client.newCall(
url = "$baseUrl/v1/account/login", PUT(
body = body.toString().toRequestBody(contentType), url = "$baseUrl/v1/account/login",
), body = body.toString().toRequestBody(contentType),
) ),
.awaitSuccess() )
.parseAs<JsonObject>() .awaitSuccess()
.let { obj -> .parseAs<JsonObject>()
try { .let { obj ->
json.decodeFromJsonElement<Context>(obj["context"]!!) try {
} catch (e: Exception) { json.decodeFromJsonElement<Context>(obj["context"]!!)
logcat(LogPriority.ERROR, e) } catch (e: Exception) {
null logcat(LogPriority.ERROR, e)
null
}
} }
} }
} }
} }

View file

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.PkceUtil import eu.kanade.tachiyomi.util.PkceUtil
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.contentOrNull
@ -27,11 +28,14 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) { class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
private val json: Json by injectLazy()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun getAccessToken(authCode: String): OAuth { suspend fun getAccessToken(authCode: String): OAuth {
@ -42,9 +46,11 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.add("code_verifier", codeVerifier) .add("code_verifier", codeVerifier)
.add("grant_type", "authorization_code") .add("grant_type", "authorization_code")
.build() .build()
client.newCall(POST("$baseOAuthUrl/token", body = formBody)) with(json) {
.awaitSuccess() client.newCall(POST("$baseOAuthUrl/token", body = formBody))
.parseAs() .awaitSuccess()
.parseAs()
}
} }
} }
@ -54,10 +60,12 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url("$baseApiUrl/users/@me") .url("$baseApiUrl/users/@me")
.get() .get()
.build() .build()
authClient.newCall(request) with(json) {
.awaitSuccess() authClient.newCall(request)
.parseAs<JsonObject>() .awaitSuccess()
.let { it["name"]!!.jsonPrimitive.content } .parseAs<JsonObject>()
.let { it["name"]!!.jsonPrimitive.content }
}
} }
} }
@ -68,19 +76,21 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendQueryParameter("q", query.take(64)) .appendQueryParameter("q", query.take(64))
.appendQueryParameter("nsfw", "true") .appendQueryParameter("nsfw", "true")
.build() .build()
authClient.newCall(GET(url.toString())) with(json) {
.awaitSuccess() authClient.newCall(GET(url.toString()))
.parseAs<JsonObject>() .awaitSuccess()
.let { .parseAs<JsonObject>()
it["data"]!!.jsonArray .let {
.map { data -> data.jsonObject["node"]!!.jsonObject } it["data"]!!.jsonArray
.map { node -> .map { data -> data.jsonObject["node"]!!.jsonObject }
val id = node["id"]!!.jsonPrimitive.int .map { node ->
async { getMangaDetails(id) } val id = node["id"]!!.jsonPrimitive.int
} async { getMangaDetails(id) }
.awaitAll() }
.filter { trackSearch -> !trackSearch.publishing_type.contains("novel") } .awaitAll()
} .filter { trackSearch -> !trackSearch.publishing_type.contains("novel") }
}
}
} }
} }
@ -90,28 +100,34 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendPath(id.toString()) .appendPath(id.toString())
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date") .appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
.build() .build()
authClient.newCall(GET(url.toString())) with(json) {
.awaitSuccess() authClient.newCall(GET(url.toString()))
.parseAs<JsonObject>() .awaitSuccess()
.let { .parseAs<JsonObject>()
val obj = it.jsonObject .let {
TrackSearch.create(TrackManager.MYANIMELIST).apply { val obj = it.jsonObject
media_id = obj["id"]!!.jsonPrimitive.long TrackSearch.create(TrackManager.MYANIMELIST).apply {
title = obj["title"]!!.jsonPrimitive.content media_id = obj["id"]!!.jsonPrimitive.long
summary = obj["synopsis"]?.jsonPrimitive?.content ?: "" title = obj["title"]!!.jsonPrimitive.content
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
cover_url = obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content ?: "" total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
tracking_url = "https://myanimelist.net/manga/$media_id" cover_url =
publishing_status = obj["status"]!!.jsonPrimitive.content.replace("_", " ") obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content
publishing_type = obj["media_type"]!!.jsonPrimitive.content.replace("_", " ") ?: ""
start_date = try { tracking_url = "https://myanimelist.net/manga/$media_id"
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) publishing_status =
outputDf.format(obj["start_date"]!!) obj["status"]!!.jsonPrimitive.content.replace("_", " ")
} catch (e: Exception) { publishing_type =
"" obj["media_type"]!!.jsonPrimitive.content.replace("_", " ")
start_date = try {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
outputDf.format(obj["start_date"]!!)
} catch (e: Exception) {
""
}
} }
} }
} }
} }
} }
@ -133,10 +149,12 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url(mangaUrl(track.media_id).toString()) .url(mangaUrl(track.media_id).toString())
.put(formBodyBuilder.build()) .put(formBodyBuilder.build())
.build() .build()
authClient.newCall(request) with(json) {
.awaitSuccess() authClient.newCall(request)
.parseAs<JsonObject>() .awaitSuccess()
.let { parseMangaItem(it, track) } .parseAs<JsonObject>()
.let { parseMangaItem(it, track) }
}
} }
} }
@ -146,15 +164,17 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendPath(track.media_id.toString()) .appendPath(track.media_id.toString())
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}") .appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
.build() .build()
authClient.newCall(GET(uri.toString())) with(json) {
.awaitSuccess() authClient.newCall(GET(uri.toString()))
.parseAs<JsonObject>() .awaitSuccess()
.let { obj -> .parseAs<JsonObject>()
track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.int .let { obj ->
obj.jsonObject["my_list_status"]?.jsonObject?.let { track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
parseMangaItem(it, track) obj.jsonObject["my_list_status"]?.jsonObject?.let {
parseMangaItem(it, track)
}
} }
} }
} }
} }
@ -198,9 +218,11 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url(urlBuilder.build().toString()) .url(urlBuilder.build().toString())
.get() .get()
.build() .build()
authClient.newCall(request) with(json) {
.awaitSuccess() authClient.newCall(request)
.parseAs() .awaitSuccess()
.parseAs()
}
} }
} }

View file

@ -1,12 +1,16 @@
package eu.kanade.tachiyomi.data.track.myanimelist package eu.kanade.tachiyomi.data.track.myanimelist
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor { class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor {
private val json: Json by injectLazy()
private var oauth: OAuth? = null private var oauth: OAuth? = null
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
@ -69,7 +73,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!)) val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
if (oauthResponse.isSuccessful) { if (oauthResponse.isSuccessful) {
oauthResponse.parseAs<OAuth>() with(json) { oauthResponse.parseAs<OAuth>() }
} else { } else {
oauthResponse.close() oauthResponse.close()
null null

View file

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
@ -24,9 +25,12 @@ import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) { class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
private val json: Json by injectLazy()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun addLibManga(track: Track, user_id: String): Track { suspend fun addLibManga(track: Track, user_id: String): Track {
@ -60,14 +64,16 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
.appendQueryParameter("search", search) .appendQueryParameter("search", search)
.appendQueryParameter("limit", "20") .appendQueryParameter("limit", "20")
.build() .build()
authClient.newCall(GET(url.toString())) with(json) {
.awaitSuccess() authClient.newCall(GET(url.toString()))
.parseAs<JsonArray>() .awaitSuccess()
.let { response -> .parseAs<JsonArray>()
response.map { .let { response ->
jsonToSearch(it.jsonObject) response.map {
jsonToSearch(it.jsonObject)
}
} }
} }
} }
} }
@ -81,7 +87,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
tracking_url = baseUrl + obj["url"]!!.jsonPrimitive.content tracking_url = baseUrl + obj["url"]!!.jsonPrimitive.content
publishing_status = obj["status"]!!.jsonPrimitive.content publishing_status = obj["status"]!!.jsonPrimitive.content
publishing_type = obj["kind"]!!.jsonPrimitive.content publishing_type = obj["kind"]!!.jsonPrimitive.content
start_date = obj.get("aired_on")!!.jsonPrimitive.contentOrNull ?: "" start_date = obj["aired_on"]!!.jsonPrimitive.contentOrNull ?: ""
} }
} }
@ -102,44 +108,52 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
val urlMangas = "$apiUrl/mangas".toUri().buildUpon() val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString()) .appendPath(track.media_id.toString())
.build() .build()
val mangas = authClient.newCall(GET(urlMangas.toString())) val mangas = with(json) {
.awaitSuccess() authClient.newCall(GET(urlMangas.toString()))
.parseAs<JsonObject>() .awaitSuccess()
.parseAs<JsonObject>()
}
val url = "$apiUrl/v2/user_rates".toUri().buildUpon() val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id) .appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString()) .appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga") .appendQueryParameter("target_type", "Manga")
.build() .build()
authClient.newCall(GET(url.toString())) with(json) {
.awaitSuccess() authClient.newCall(GET(url.toString()))
.parseAs<JsonArray>() .awaitSuccess()
.let { response -> .parseAs<JsonArray>()
if (response.size > 1) { .let { response ->
throw Exception("Too much mangas in response") if (response.size > 1) {
throw Exception("Too much mangas in response")
}
val entry = response.map {
jsonToTrack(it.jsonObject, mangas)
}
entry.firstOrNull()
} }
val entry = response.map { }
jsonToTrack(it.jsonObject, mangas)
}
entry.firstOrNull()
}
} }
} }
suspend fun getCurrentUser(): Int { suspend fun getCurrentUser(): Int {
return authClient.newCall(GET("$apiUrl/users/whoami")) return with(json) {
.awaitSuccess() authClient.newCall(GET("$apiUrl/users/whoami"))
.parseAs<JsonObject>() .awaitSuccess()
.let { .parseAs<JsonObject>()
it["id"]!!.jsonPrimitive.int .let {
} it["id"]!!.jsonPrimitive.int
}
}
} }
suspend fun accessToken(code: String): OAuth { suspend fun accessToken(code: String): OAuth {
return withIOContext { return withIOContext {
client.newCall(accessTokenRequest(code)) with(json) {
.awaitSuccess() client.newCall(accessTokenRequest(code))
.parseAs() .awaitSuccess()
.parseAs()
}
} }
} }
@ -164,18 +178,12 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
private const val loginUrl = "$baseUrl/oauth/authorize" private const val loginUrl = "$baseUrl/oauth/authorize"
private const val redirectUrl = "tachiyomi://shikimori-auth" private const val redirectUrl = "tachiyomi://shikimori-auth"
private const val baseMangaUrl = "$apiUrl/mangas"
fun mangaUrl(remoteId: Int): String { fun authUrl() = loginUrl.toUri().buildUpon()
return "$baseMangaUrl/$remoteId" .appendQueryParameter("client_id", clientId)
} .appendQueryParameter("redirect_uri", redirectUrl)
.appendQueryParameter("response_type", "code")
fun authUrl() = .build()
loginUrl.toUri().buildUpon()
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", redirectUrl)
.appendQueryParameter("response_type", "code")
.build()
fun refreshTokenRequest(token: String) = POST( fun refreshTokenRequest(token: String) = POST(
oauthUrl, oauthUrl,

View file

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.PUT import eu.kanade.tachiyomi.network.PUT
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import okhttp3.Credentials import okhttp3.Credentials
import okhttp3.Dns import okhttp3.Dns
import okhttp3.FormBody import okhttp3.FormBody
@ -23,11 +24,15 @@ import java.nio.charset.Charset
import java.security.MessageDigest import java.security.MessageDigest
class TachideskApi { class TachideskApi {
private val network by injectLazy<NetworkHelper>()
private val network: NetworkHelper by injectLazy()
private val json: Json by injectLazy()
val client: OkHttpClient = val client: OkHttpClient =
network.client.newBuilder() network.client.newBuilder()
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing .dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
.build() .build()
fun headersBuilder(): Headers.Builder = Headers.Builder().apply { fun headersBuilder(): Headers.Builder = Headers.Builder().apply {
if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) { if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) {
val credentials = Credentials.basic(baseLogin, basePassword) val credentials = Credentials.basic(baseLogin, basePassword)
@ -50,7 +55,11 @@ class TachideskApi {
trackUrl trackUrl
} }
val manga = client.newCall(GET("$url/full", headers)).awaitSuccess().parseAs<MangaDataClass>() val manga = with(json) {
client.newCall(GET("$url/full", headers))
.awaitSuccess()
.parseAs<MangaDataClass>()
}
TrackSearch.create(TrackManager.SUWAYOMI).apply { TrackSearch.create(TrackManager.SUWAYOMI).apply {
title = manga.title title = manga.title
@ -70,7 +79,11 @@ class TachideskApi {
suspend fun updateProgress(track: Track): Track { suspend fun updateProgress(track: Track): Track {
val url = track.tracking_url val url = track.tracking_url
val chapters = client.newCall(GET("$url/chapters", headers)).awaitSuccess().parseAs<List<ChapterDataClass>>() val chapters = with(json) {
client.newCall(GET("$url/chapters", headers))
.awaitSuccess()
.parseAs<List<ChapterDataClass>>()
}
val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index
client.newCall( client.newCall(

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
import kotlinx.serialization.json.Json
import tachiyomi.core.preference.Preference import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
@ -18,6 +19,8 @@ class AppUpdateChecker {
private val networkService: NetworkHelper by injectLazy() private val networkService: NetworkHelper by injectLazy()
private val preferenceStore: PreferenceStore by injectLazy() private val preferenceStore: PreferenceStore by injectLazy()
private val json: Json by injectLazy()
private val lastAppCheck: Preference<Long> by lazy { private val lastAppCheck: Preference<Long> by lazy {
preferenceStore.getLong("last_app_check", 0) preferenceStore.getLong("last_app_check", 0)
} }
@ -29,24 +32,26 @@ class AppUpdateChecker {
} }
return withIOContext { return withIOContext {
val result = networkService.client val result = with(json) {
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest")) networkService.client
.awaitSuccess() .newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest"))
.parseAs<GithubRelease>() .awaitSuccess()
.let { .parseAs<GithubRelease>()
lastAppCheck.set(Date().time) .let {
lastAppCheck.set(Date().time)
// Check if latest version is different from current version // Check if latest version is different from current version
if (isNewVersion(it.version)) { if (isNewVersion(it.version)) {
if (context.isInstalledFromFDroid()) { if (context.isInstalledFromFDroid()) {
AppUpdateResult.NewUpdateFdroidInstallation AppUpdateResult.NewUpdateFdroidInstallation
} else {
AppUpdateResult.NewUpdate(it)
}
} else { } else {
AppUpdateResult.NewUpdate(it) AppUpdateResult.NoNewUpdate
} }
} else {
AppUpdateResult.NoNewUpdate
} }
} }
when (result) { when (result) {
is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release) is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)

View file

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.preference.Preference import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
@ -24,10 +25,12 @@ internal class ExtensionGithubApi {
private val networkService: NetworkHelper by injectLazy() private val networkService: NetworkHelper by injectLazy()
private val preferenceStore: PreferenceStore by injectLazy() private val preferenceStore: PreferenceStore by injectLazy()
private val extensionManager: ExtensionManager by injectLazy()
private val json: Json by injectLazy()
private val lastExtCheck: Preference<Long> by lazy { private val lastExtCheck: Preference<Long> by lazy {
preferenceStore.getLong("last_ext_check", 0) preferenceStore.getLong("last_ext_check", 0)
} }
private val extensionManager: ExtensionManager by injectLazy()
private var requiresFallbackSource = false private var requiresFallbackSource = false
@ -53,9 +56,11 @@ internal class ExtensionGithubApi {
.awaitSuccess() .awaitSuccess()
} }
val extensions = response val extensions = with(json) {
.parseAs<List<ExtensionJsonObject>>() response
.toExtensions() .parseAs<List<ExtensionJsonObject>>()
.toExtensions()
}
// Sanity check - a small number of extensions probably means something broke // Sanity check - a small number of extensions probably means something broke
// with the repo generator // with the repo generator

View file

@ -120,7 +120,7 @@ class ExtensionDetailsScreenModel(
val cleared = urls.sumOf { val cleared = urls.sumOf {
try { try {
network.cookieManager.remove(it.toHttpUrl()) network.cookieJar.remove(it.toHttpUrl())
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to clear cookies for $it" } logcat(LogPriority.ERROR, e) { "Failed to clear cookies for $it" }
0 0

View file

@ -92,7 +92,7 @@ class WebViewActivity : BaseActivity() {
} }
private fun clearCookies(url: String) { private fun clearCookies(url: String) {
val cleared = network.cookieManager.remove(url.toHttpUrl()) val cleared = network.cookieJar.remove(url.toHttpUrl())
logcat { "Cleared $cleared cookies for: $url" } logcat { "Cleared $cleared cookies for: $url" }
} }

View file

@ -47,7 +47,7 @@ class WebViewScreenModel(
} }
fun clearCookies(url: String) { fun clearCookies(url: String) {
val cleared = network.cookieManager.remove(url.toHttpUrl()) val cleared = network.cookieJar.remove(url.toHttpUrl())
logcat { "Cleared $cleared cookies for: $url" } logcat { "Cleared $cleared cookies for: $url" }
} }
} }

View file

@ -6,6 +6,13 @@ plugins {
android { android {
namespace = "eu.kanade.tachiyomi.core" namespace = "eu.kanade.tachiyomi.core"
kotlinOptions {
freeCompilerArgs += listOf(
"-Xcontext-receivers",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
} }
dependencies { dependencies {
@ -24,12 +31,8 @@ dependencies {
api(kotlinx.serialization.json) api(kotlinx.serialization.json)
api(kotlinx.serialization.json.okio) api(kotlinx.serialization.json.okio)
api(libs.injekt.core)
api(libs.preferencektx) api(libs.preferencektx)
implementation(androidx.corektx)
// JavaScript engine // JavaScript engine
implementation(libs.bundles.js.engine) implementation(libs.bundles.js.engine)
} }

View file

@ -6,26 +6,30 @@ import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class NetworkHelper(context: Context) { class NetworkHelper(
context: Context,
private val preferences: NetworkPreferences by injectLazy() private val preferences: NetworkPreferences,
) {
private val cacheDir = File(context.cacheDir, "network_cache") private val cacheDir = File(context.cacheDir, "network_cache")
private val cacheSize = 5L * 1024 * 1024 // 5 MiB private val cacheSize = 5L * 1024 * 1024 // 5 MiB
val cookieManager = AndroidCookieJar() val cookieJar = AndroidCookieJar()
private val userAgentInterceptor by lazy { UserAgentInterceptor() } private val userAgentInterceptor by lazy {
private val cloudflareInterceptor by lazy { CloudflareInterceptor(context) } UserAgentInterceptor(::defaultUserAgentProvider)
}
private val cloudflareInterceptor by lazy {
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider)
}
private val baseClientBuilder: OkHttpClient.Builder private val baseClientBuilder: OkHttpClient.Builder
get() { get() {
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
.cookieJar(cookieManager) .cookieJar(cookieJar)
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.callTimeout(2, TimeUnit.MINUTES) .callTimeout(2, TimeUnit.MINUTES)
@ -65,7 +69,5 @@ class NetworkHelper(context: Context) {
.build() .build()
} }
val defaultUserAgent by lazy { fun defaultUserAgentProvider() = preferences.defaultUserAgent().get().trim()
preferences.defaultUserAgent().get().trim()
}
} }

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.network
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.okio.decodeFromBufferedSource import kotlinx.serialization.json.okio.decodeFromBufferedSource
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
@ -16,8 +15,6 @@ import okhttp3.Response
import rx.Observable import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@ -131,14 +128,18 @@ fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: Progre
return progressClient.newCall(request) return progressClient.newCall(request)
} }
context(Json)
inline fun <reified T> Response.parseAs(): T { inline fun <reified T> Response.parseAs(): T {
return decodeFromJsonResponse(serializer(), this) return decodeFromJsonResponse(serializer(), this)
} }
@OptIn(ExperimentalSerializationApi::class) context(Json)
fun <T> decodeFromJsonResponse(deserializer: DeserializationStrategy<T>, response: Response): T { fun <T> decodeFromJsonResponse(
deserializer: DeserializationStrategy<T>,
response: Response,
): T {
return response.body.source().use { return response.body.source().use {
Injekt.get<Json>().decodeFromBufferedSource(deserializer, it) decodeFromBufferedSource(deserializer, it)
} }
} }

View file

@ -6,7 +6,7 @@ import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.core.R import eu.kanade.tachiyomi.core.R
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.AndroidCookieJar
import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.isOutdated import eu.kanade.tachiyomi.util.system.isOutdated
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@ -15,16 +15,17 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(context) { class CloudflareInterceptor(
private val context: Context,
private val cookieManager: AndroidCookieJar,
defaultUserAgentProvider: () -> String,
) : WebViewInterceptor(context, defaultUserAgentProvider) {
private val executor = ContextCompat.getMainExecutor(context) private val executor = ContextCompat.getMainExecutor(context)
private val networkHelper: NetworkHelper by injectLazy()
override fun shouldIntercept(response: Response): Boolean { override fun shouldIntercept(response: Response): Boolean {
// Check if Cloudflare anti-bot is on // Check if Cloudflare anti-bot is on
return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
@ -33,8 +34,8 @@ class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(c
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response { override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
try { try {
response.close() response.close()
networkHelper.cookieManager.remove(request.url, COOKIE_NAMES, 0) cookieManager.remove(request.url, COOKIE_NAMES, 0)
val oldCookie = networkHelper.cookieManager.get(request.url) val oldCookie = cookieManager.get(request.url)
.firstOrNull { it.name == "cf_clearance" } .firstOrNull { it.name == "cf_clearance" }
resolveWithWebView(request, oldCookie) resolveWithWebView(request, oldCookie)
@ -70,7 +71,7 @@ class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(c
webview?.webViewClient = object : WebViewClientCompat() { webview?.webViewClient = object : WebViewClientCompat() {
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
fun isCloudFlareBypassed(): Boolean { fun isCloudFlareBypassed(): Boolean {
return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl()) return cookieManager.get(origRequestUrl.toHttpUrl())
.firstOrNull { it.name == "cf_clearance" } .firstOrNull { it.name == "cf_clearance" }
.let { it != null && it != oldCookie } .let { it != null && it != oldCookie }
} }

View file

@ -1,13 +1,11 @@
package eu.kanade.tachiyomi.network.interceptor package eu.kanade.tachiyomi.network.interceptor
import eu.kanade.tachiyomi.network.NetworkHelper
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class UserAgentInterceptor : Interceptor { class UserAgentInterceptor(
private val defaultUserAgentProvider: () -> String,
private val networkHelper: NetworkHelper by injectLazy() ) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
@ -16,7 +14,7 @@ class UserAgentInterceptor : Interceptor {
val newRequest = originalRequest val newRequest = originalRequest
.newBuilder() .newBuilder()
.removeHeader("User-Agent") .removeHeader("User-Agent")
.addHeader("User-Agent", networkHelper.defaultUserAgent) .addHeader("User-Agent", defaultUserAgentProvider())
.build() .build()
chain.proceed(newRequest) chain.proceed(newRequest)
} else { } else {

View file

@ -6,7 +6,6 @@ import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import eu.kanade.tachiyomi.core.R import eu.kanade.tachiyomi.core.R
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
@ -16,14 +15,14 @@ import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import tachiyomi.core.util.lang.launchUI import tachiyomi.core.util.lang.launchUI
import uy.kohesive.injekt.injectLazy
import java.util.Locale import java.util.Locale
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
abstract class WebViewInterceptor(private val context: Context) : Interceptor { abstract class WebViewInterceptor(
private val context: Context,
private val networkHelper: NetworkHelper by injectLazy() private val defaultUserAgentProvider: () -> String,
) : Interceptor {
/** /**
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid * When this is called, it initializes the WebView if it wasn't already. We use this to avoid
@ -85,7 +84,7 @@ abstract class WebViewInterceptor(private val context: Context) : Interceptor {
return WebView(context).apply { return WebView(context).apply {
setDefaultSettings() setDefaultSettings()
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
settings.userAgentString = request.header("User-Agent") ?: networkHelper.defaultUserAgent settings.userAgentString = request.header("User-Agent") ?: defaultUserAgentProvider()
} }
} }
} }

View file

@ -23,6 +23,7 @@ dependencies {
implementation(project(":source-api")) implementation(project(":source-api"))
implementation(project(":domain")) implementation(project(":domain"))
implementation(project(":core")) implementation(project(":core"))
api(libs.sqldelight.android.driver) api(libs.sqldelight.android.driver)
api(libs.sqldelight.coroutines) api(libs.sqldelight.coroutines)
api(libs.sqldelight.android.paging) api(libs.sqldelight.android.paging)

View file

@ -6,12 +6,32 @@ import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.domain.updates.repository.UpdatesRepository import tachiyomi.domain.updates.repository.UpdatesRepository
class UpdatesRepositoryImpl( class UpdatesRepositoryImpl(
val databaseHandler: DatabaseHandler, private val databaseHandler: DatabaseHandler,
) : UpdatesRepository { ) : UpdatesRepository {
override suspend fun awaitWithRead(read: Boolean, after: Long): List<UpdatesWithRelations> {
return databaseHandler.awaitList {
updatesViewQueries.getUpdatesByReadStatus(
read = read,
after = after,
mapper = updateWithRelationMapper,
)
}
}
override fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>> { override fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>> {
return databaseHandler.subscribeToList { return databaseHandler.subscribeToList {
updatesViewQueries.updates(after, updateWithRelationMapper) updatesViewQueries.updates(after, updateWithRelationMapper)
} }
} }
override fun subscribeWithRead(read: Boolean, after: Long): Flow<List<UpdatesWithRelations>> {
return databaseHandler.subscribeToList {
updatesViewQueries.getUpdatesByReadStatus(
read = read,
after = after,
mapper = updateWithRelationMapper,
)
}
}
} }

View file

@ -13,11 +13,11 @@ android {
} }
dependencies { dependencies {
implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines)
implementation(project(":source-api")) implementation(project(":source-api"))
implementation(project(":core")) implementation(project(":core"))
implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines)
testImplementation(libs.junit) testImplementation(libs.junit)
} }

View file

@ -9,9 +9,17 @@ class GetUpdates(
private val repository: UpdatesRepository, private val repository: UpdatesRepository,
) { ) {
suspend fun await(read: Boolean, after: Long): List<UpdatesWithRelations> {
return repository.awaitWithRead(read, after)
}
fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> = subscribe(calendar.time.time) fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> = subscribe(calendar.time.time)
fun subscribe(after: Long): Flow<List<UpdatesWithRelations>> { fun subscribe(after: Long): Flow<List<UpdatesWithRelations>> {
return repository.subscribeAll(after) return repository.subscribeAll(after)
} }
fun subscribe(read: Boolean, after: Long): Flow<List<UpdatesWithRelations>> {
return repository.subscribeWithRead(read, after)
}
} }

View file

@ -5,5 +5,9 @@ import tachiyomi.domain.updates.model.UpdatesWithRelations
interface UpdatesRepository { interface UpdatesRepository {
suspend fun awaitWithRead(read: Boolean, after: Long): List<UpdatesWithRelations>
fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>> fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>>
fun subscribeWithRead(read: Boolean, after: Long): Flow<List<UpdatesWithRelations>>
} }

View file

@ -22,11 +22,11 @@ android {
dependencies { dependencies {
implementation(project(":core")) implementation(project(":core"))
implementation(project(":data"))
implementation(project(":domain")) implementation(project(":domain"))
implementation(project(":presentation-core")) implementation(project(":presentation-core"))
implementation(androidx.glance) implementation(androidx.glance)
implementation(libs.coil.core) implementation(libs.coil.core)
api(libs.injekt.core)
} }

View file

@ -7,21 +7,17 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import tachiyomi.data.DatabaseHandler import tachiyomi.domain.updates.interactor.GetUpdates
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class TachiyomiWidgetManager( class TachiyomiWidgetManager(
private val database: DatabaseHandler = Injekt.get(), private val getUpdates: GetUpdates,
) { ) {
fun Context.init(scope: LifecycleCoroutineScope) { fun Context.init(scope: LifecycleCoroutineScope) {
database.subscribeToList { getUpdates.subscribe(
updatesViewQueries.getUpdatesByReadStatus( read = false,
read = false, after = UpdatesGridGlanceWidget.DateLimit.timeInMillis,
after = UpdatesGridGlanceWidget.DateLimit.timeInMillis, )
)
}
.drop(1) .drop(1)
.distinctUntilChanged() .distinctUntilChanged()
.onEach { .onEach {

View file

@ -25,13 +25,13 @@ import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchIO
import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.updates.interactor.GetUpdates
import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.presentation.widget.components.CoverHeight import tachiyomi.presentation.widget.components.CoverHeight
import tachiyomi.presentation.widget.components.CoverWidth import tachiyomi.presentation.widget.components.CoverWidth
import tachiyomi.presentation.widget.components.LockedWidget import tachiyomi.presentation.widget.components.LockedWidget
import tachiyomi.presentation.widget.components.UpdatesWidget import tachiyomi.presentation.widget.components.UpdatesWidget
import tachiyomi.view.UpdatesView
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -39,6 +39,7 @@ import java.util.Calendar
import java.util.Date import java.util.Date
class UpdatesGridGlanceWidget : GlanceAppWidget() { class UpdatesGridGlanceWidget : GlanceAppWidget() {
private val app: Application by injectLazy() private val app: Application by injectLazy()
private val preferences: SecurityPreferences by injectLazy() private val preferences: SecurityPreferences by injectLazy()
@ -58,7 +59,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
UpdatesWidget(data) UpdatesWidget(data)
} }
fun loadData(list: List<UpdatesView>? = null) { fun loadData(list: List<UpdatesWithRelations>? = null) {
coroutineScope.launchIO { coroutineScope.launchIO {
// Don't show anything when lock is active // Don't show anything when lock is active
if (preferences.useAuthenticator().get()) { if (preferences.useAuthenticator().get()) {
@ -71,13 +72,10 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
if (ids.isEmpty()) return@launchIO if (ids.isEmpty()) return@launchIO
val processList = list val processList = list
?: Injekt.get<DatabaseHandler>() ?: Injekt.get<GetUpdates>().await(
.awaitList { read = false,
updatesViewQueries.getUpdatesByReadStatus( after = DateLimit.timeInMillis,
read = false, )
after = DateLimit.timeInMillis,
)
}
val (rowCount, columnCount) = ids val (rowCount, columnCount) = ids
.flatMap { manager.getAppWidgetSizes(it) } .flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value } .maxBy { it.height.value * it.width.value }
@ -88,7 +86,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
} }
} }
private fun prepareList(processList: List<UpdatesView>, take: Int): List<Pair<Long, Bitmap?>> { private fun prepareList(processList: List<UpdatesWithRelations>, take: Int): List<Pair<Long, Bitmap?>> {
// Resize to cover size // Resize to cover size
val widthPx = CoverWidth.value.toInt().dpToPx val widthPx = CoverWidth.value.toInt().dpToPx
val heightPx = CoverHeight.value.toInt().dpToPx val heightPx = CoverHeight.value.toInt().dpToPx
@ -101,10 +99,10 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
.data( .data(
MangaCover( MangaCover(
mangaId = updatesView.mangaId, mangaId = updatesView.mangaId,
sourceId = updatesView.source, sourceId = updatesView.sourceId,
isMangaFavorite = updatesView.favorite, isMangaFavorite = true,
url = updatesView.thumbnailUrl, url = updatesView.coverData.url,
lastModified = updatesView.coverLastModified, lastModified = updatesView.coverData.lastModified,
), ),
) )
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)

View file

@ -10,20 +10,14 @@ android {
defaultConfig { defaultConfig {
consumerProguardFile("consumer-proguard.pro") consumerProguardFile("consumer-proguard.pro")
} }
} }
dependencies { dependencies {
implementation(project(":core")) implementation(project(":core"))
api(kotlinx.serialization.json) api(kotlinx.serialization.json)
api(libs.injekt.core)
api(libs.rxjava) api(libs.rxjava)
api(libs.preferencektx) api(libs.preferencektx)
api(libs.jsoup) api(libs.jsoup)
implementation(androidx.corektx)
} }

View file

@ -68,7 +68,7 @@ abstract class HttpSource : CatalogueSource {
* Headers builder for requests. Implementations can override this method for custom headers. * Headers builder for requests. Implementations can override this method for custom headers.
*/ */
protected open fun headersBuilder() = Headers.Builder().apply { protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", network.defaultUserAgent) add("User-Agent", network.defaultUserAgentProvider())
} }
/** /**