Compare commits

...

29 Commits

Author SHA1 Message Date
LorgOn
edfd4a1e08
Merge e9b41975da into c5655e8803 2024-12-24 11:21:07 +02:00
AntsyLich
c5655e8803
Revert "Revert "Add option to always use SSIV for image decoding""
This reverts commit 1909126921
2024-12-22 02:38:12 +06:00
Mend Renovate
d3973f4ad8
Update dependency gradle to v8.12 (#1605) 2024-12-22 02:14:44 +06:00
Mend Renovate
bb230fd6a7
Update dependency androidx.compose:compose-bom to v2024.12.01 (#1564) 2024-12-22 01:32:17 +06:00
Mend Renovate
e526fd44c6
Update paging.version to v3.3.5 (#1563) 2024-12-22 01:28:40 +06:00
Mend Renovate
f61f039a45
Update dependency androidx.viewpager:viewpager to v1.1.0 (#1571) 2024-12-22 01:27:28 +06:00
Mend Renovate
79eb02d8f0
Update dependency org.junit.jupiter:junit-jupiter to v5.11.4 (#1580) 2024-12-22 01:25:21 +06:00
Mend Renovate
814584d35b
Update voyager to v1.0.1 (#1595) 2024-12-22 01:24:43 +06:00
Mend Renovate
8751307301
Update dependency com.android.tools:desugar_jdk_libs to v2.1.4 (#1599) 2024-12-22 01:23:53 +06:00
Mend Renovate
bcff2262b3
Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.1 (#1596) 2024-12-22 01:23:41 +06:00
Mend Renovate
04454ecdbe
Update dependency io.mockk:mockk to v1.13.14 (#1601) 2024-12-22 01:21:41 +06:00
Lorg0n
e9b41975da fix: fixed a bug in DTO that caused a data parsing error in manga search 2024-10-30 21:05:06 +03:00
Lorg0n
ef3369be26 fix: fixed a bug in DTO that caused a data parsing error 2024-10-28 03:11:31 +03:00
Lorg0n
4a3209267d fix: remove unused import 2024-10-27 19:49:18 +03:00
Lorg0n
e44666673f feat: changed authorization principle, removed dependence on third-party server 2024-10-27 19:44:47 +03:00
Lorg0n
f480d4cb38 ref: optimized imports and refactor code 2024-10-27 15:31:12 +03:00
Lorg0n
a8f4e63d54 ref: optimized imports and reduced the length of lines 2024-10-27 03:00:28 +03:00
Lorg0n
45cd945f7c fix: fixed a bug where the server returned a 404 error due to receiving data in HikkaApi 2024-10-27 02:25:24 +03:00
Lorg0n
6b8c2dcdc3 fix: recode the rereading logic in HikkaApi 2024-10-27 02:14:17 +03:00
Lorg0n
23a34cd3b7 fix: slightly optimised the function of string to number conversion and fixed the check 2024-10-27 01:02:16 +03:00
Lorg0n
e332a5018e fix: corrected function names and added reread logic 2024-10-22 18:59:57 +03:00
Lorg0n
41db4a5865 ref: added more specificity and removed unnecessary code in Hikka Api 2024-10-17 22:46:26 +03:00
Lorg0n
f39e947ece feat: more optimization Hikka authorization 2024-10-17 22:25:48 +03:00
Lorg0n
9eeed9f050 feat: improve Hikka authorization 2024-10-17 20:33:09 +03:00
Lorg0n
03f9ae8bb2 feat: improve Hikka authorization 2024-10-17 20:32:47 +03:00
LorgOn
32db1da753
Merge branch 'mihonapp:main' into Add-Hikka-Tracker 2024-10-17 18:42:14 +03:00
Lorg0n
06f01de4b5 feat: update AndroidManifest for auth support, modify auth-related classes, and add Hikka to tracker list 2024-10-17 17:55:18 +03:00
Lorg0n
f6d8d415cc feat: add core Hikka tracker classes and DTOs for API requests 2024-10-17 17:21:53 +03:00
Lorg0n
68d24dff27 feat: add Hikka logo to resources 2024-10-17 17:01:07 +03:00
28 changed files with 662 additions and 14 deletions

View File

@ -11,6 +11,8 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- `Other` - for technical stuff.
## [Unreleased]
### Added
- Add option to always decode long strip images with SSIV
## [v0.17.1] - 2024-12-06
### Changed

View File

@ -180,6 +180,7 @@
<data android:host="bangumi-auth" />
<data android:host="myanimelist-auth" />
<data android:host="shikimori-auth" />
<data android:host="hikka-auth" />
</intent-filter>
</activity>

View File

@ -33,4 +33,6 @@ class BasePreferences(
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT)
fun alwaysDecodeLongStripWithSSIV() = preferenceStore.getBoolean("pref_always_decode_long_strip_with_ssiv", false)
}

View File

@ -356,6 +356,11 @@ object SettingsAdvancedScreen : SearchableSettings {
.toMap()
.toImmutableMap(),
),
Preference.PreferenceItem.SwitchPreference(
pref = basePreferences.alwaysDecodeLongStripWithSSIV(),
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv),
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
),
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_display_profile),
subtitle = basePreferences.displayProfile().get(),

View File

@ -48,6 +48,7 @@ import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
import eu.kanade.tachiyomi.data.track.hikka.HikkaApi
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
import eu.kanade.tachiyomi.util.system.openInBrowser
@ -174,6 +175,12 @@ object SettingsTrackingScreen : SearchableSettings {
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.bangumi) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.hikka.name,
tracker = trackerManager.hikka,
login = { context.openInBrowser(HikkaApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.hikka) },
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.tracking_info)),
),
),

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import eu.kanade.tachiyomi.data.track.hikka.Hikka
import eu.kanade.tachiyomi.data.track.kavita.Kavita
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.komga.Komga
@ -28,8 +29,9 @@ class TrackerManager {
val mangaUpdates = MangaUpdates(7L)
val kavita = Kavita(KAVITA)
val suwayomi = Suwayomi(9L)
val hikka = Hikka(10L)
val trackers = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi)
val trackers = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi, hikka)
fun loggedInTrackers() = trackers.filter { it.isLoggedIn }

View File

@ -0,0 +1,163 @@
package eu.kanade.tachiyomi.data.track.hikka
import android.graphics.Color
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import tachiyomi.domain.track.model.Track as DomainTrack
class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker {
companion object {
const val READING = 0L
const val COMPLETED = 1L
const val ON_HOLD = 2L
const val DROPPED = 3L
const val PLAN_TO_READ = 4L
const val REREADING = 5L
private val SCORE_LIST = IntRange(0, 10)
.map(Int::toString)
.toImmutableList()
}
private val json: Json by injectLazy()
private val interceptor by lazy { HikkaInterceptor(this) }
private val api by lazy { HikkaApi(id, client, interceptor) }
override fun getLogoColor(): Int {
return Color.rgb(0, 0, 0)
}
override fun getLogo(): Int {
return R.drawable.ic_tracker_hikka
}
override fun getStatusList(): List<Long> {
return listOf(
READING,
COMPLETED,
ON_HOLD,
DROPPED,
PLAN_TO_READ,
REREADING,
)
}
override fun getStatus(status: Long): StringResource? = when (status) {
READING -> MR.strings.reading
PLAN_TO_READ -> MR.strings.plan_to_read
COMPLETED -> MR.strings.completed
ON_HOLD -> MR.strings.on_hold
DROPPED -> MR.strings.dropped
REREADING -> MR.strings.repeating
else -> null
}
override fun getReadingStatus(): Long {
return READING
}
override fun getRereadingStatus(): Long {
return REREADING
}
override fun getCompletionStatus(): Long {
return COMPLETED
}
override fun getScoreList(): ImmutableList<String> {
return SCORE_LIST
}
override fun displayScore(track: DomainTrack): String {
return track.score.toInt().toString()
}
override suspend fun update(
track: Track,
didReadChapter: Boolean,
): Track {
if (track.status != COMPLETED) {
if (didReadChapter) {
if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) {
track.status = COMPLETED
} else if (track.status != REREADING) {
track.status = READING
}
}
}
return api.updateUserManga(track)
}
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val remoteTrack = api.getManga(track)
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
if (track.status != COMPLETED) {
val isRereading = track.status == REREADING
track.status = if (!isRereading && hasReadChapters) READING else track.status
}
return update(track)
}
override suspend fun search(query: String): List<TrackSearch> {
return api.searchManga(query)
}
override suspend fun refresh(track: Track): Track {
val remoteTrack = api.updateUserManga(track)
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
return track
}
override suspend fun login(username: String, password: String) = login(password)
suspend fun login(reference: String) {
try {
val oauth = api.accessToken(reference)
interceptor.setAuth(oauth)
val user = api.getCurrentUser()
saveCredentials(user.reference, oauth.accessToken)
} catch (e: Throwable) {
logout()
}
}
override suspend fun delete(track: DomainTrack) {
api.deleteUserManga(track)
}
override fun logout() {
super.logout()
trackPreferences.trackToken(this).delete()
interceptor.setAuth(null)
}
fun saveOAuth(oAuth: HKOAuth?) {
trackPreferences.trackToken(this).set(json.encodeToString(oAuth))
}
fun loadOAuth(): HKOAuth? {
return try {
json.decodeFromString<HKOAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) {
null
}
}
}

View File

@ -0,0 +1,217 @@
package eu.kanade.tachiyomi.data.track.hikka
import android.net.Uri
import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.hikka.dto.HKManga
import eu.kanade.tachiyomi.data.track.hikka.dto.HKMangaPagination
import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth
import eu.kanade.tachiyomi.data.track.hikka.dto.HKRead
import eu.kanade.tachiyomi.data.track.hikka.dto.HKUser
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.PUT
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
class HikkaApi(
private val trackId: Long,
private val client: OkHttpClient,
interceptor: HikkaInterceptor,
) {
suspend fun getCurrentUser(): HKUser {
return withIOContext {
val request = Request.Builder()
.url("${BASE_API_URL}/user/me")
.get()
.build()
with(json) {
authClient.newCall(request)
.awaitSuccess()
.parseAs<HKUser>()
}
}
}
suspend fun accessToken(reference: String): HKOAuth {
return withIOContext {
with(json) {
client.newCall(authTokenCreate(reference))
.awaitSuccess()
.parseAs<HKOAuth>()
}
}
}
suspend fun searchManga(query: String): List<TrackSearch> {
return withIOContext {
val url = "$BASE_API_URL/manga".toUri().buildUpon()
.appendQueryParameter("page", "1")
.appendQueryParameter("size", "50")
.build()
val payload = buildJsonObject {
put("media_type", buildJsonArray { })
put("status", buildJsonArray { })
put("only_translated", false)
put("magazines", buildJsonArray { })
put("genres", buildJsonArray { })
put(
"score",
buildJsonArray {
add(0)
add(10)
},
)
put("query", query)
put(
"sort",
buildJsonArray {
add("score:asc")
add("scored_by:asc")
},
)
}
with(json) {
authClient.newCall(POST(url.toString(), body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<HKMangaPagination>()
.list
.map { it.toTrack(trackId) }
}
}
}
private suspend fun getRead(track: Track): HKRead? {
return withIOContext {
val slug = track.tracking_url.split("/")[4]
val url = "$BASE_API_URL/read/manga/$slug".toUri().buildUpon().build()
with(json) {
val response = authClient.newCall(GET(url.toString())).execute()
if (response.code == 404) {
return@withIOContext null
}
response.use {
it.parseAs<HKRead>()
}
}
}
}
suspend fun getManga(track: Track): TrackSearch {
return withIOContext {
val slug = track.tracking_url.split("/")[4]
val url = "$BASE_API_URL/manga/$slug".toUri().buildUpon()
.build()
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<HKManga>()
.toTrack(trackId)
}
}
}
suspend fun deleteUserManga(track: tachiyomi.domain.track.model.Track) {
return withIOContext {
val slug = track.remoteUrl.split("/")[4]
val url = "$BASE_API_URL/read/manga/$slug".toUri().buildUpon()
.build()
authClient.newCall(DELETE(url.toString()))
.awaitSuccess()
}
}
suspend fun addUserManga(track: Track): Track {
return withIOContext {
val slug = track.tracking_url.split("/")[4]
val url = "$BASE_API_URL/read/manga/$slug".toUri().buildUpon()
.build()
var rereads = getRead(track)?.rereads ?: 0
if (track.status == Hikka.REREADING && rereads == 0) {
rereads = 1
}
val payload = buildJsonObject {
put("note", "")
put("chapters", track.last_chapter_read.toInt())
put("volumes", 0)
put("rereads", rereads)
put("score", track.score.toInt())
put("status", track.toApiStatus())
}
with(json) {
authClient.newCall(PUT(url.toString(), body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<HKRead>()
.toTrack(trackId)
}
}
}
suspend fun updateUserManga(track: Track): Track = addUserManga(track)
private val json: Json by injectLazy()
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
companion object {
const val BASE_API_URL = "https://hikka.io/api"
const val BASE_URL = "https://hikka.io"
private const val SCOPE = "readlist,read:user-details"
private const val CLIENT_REFERENCE = "49eda83d-baa6-45f8-9936-b2a41d944da4"
private const val CLIENT_SECRET = "8Zxzs13Pvikx6m_4rwjF7t2BxxnEb0wWtXIRQ_68HyCvmdhGE9hdfz" +
"SL1Pas4h927LaV2ocjVoc--S_vmorHEWWh42Z_z70j-wSFYsraQQ98" +
"hiOeTH2BaDf77ZcA9W5Z"
fun authUrl(): Uri = "$BASE_URL/oauth".toUri().buildUpon()
.appendQueryParameter("reference", CLIENT_REFERENCE)
.appendQueryParameter("scope", SCOPE)
.build()
fun refreshTokenRequest(accessToken: String): Request {
val headers = Headers.Builder()
.add("auth", accessToken)
.build()
return GET("$BASE_API_URL/user/me", headers = headers) // Any request with auth
}
fun authTokenCreate(reference: String): Request {
val payload = buildJsonObject {
put("request_reference", reference)
put("client_secret", CLIENT_SECRET)
}
return POST("$BASE_API_URL/auth/token", body = payload.toString().toRequestBody(jsonMime))
}
fun authTokenInfo(accessToken: String): Request {
val headers = Headers.Builder()
.add("auth", accessToken)
.build()
return GET("$BASE_API_URL/auth/token/info", headers = headers)
}
}
}

View File

@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.data.track.hikka
import eu.kanade.tachiyomi.data.track.hikka.dto.HKAuthTokenInfo
import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class HikkaInterceptor(private val hikka: Hikka) : Interceptor {
private val json: Json by injectLazy()
private var oauth: HKOAuth? = hikka.loadOAuth()
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val currAuth = oauth ?: throw Exception("Hikka: You are not authorized")
if (currAuth.isExpired()) {
val refreshTokenResponse = chain.proceed(HikkaApi.refreshTokenRequest(currAuth.accessToken))
if (!refreshTokenResponse.isSuccessful) {
refreshTokenResponse.close()
hikka.logout()
throw Exception("Hikka: The token is expired")
}
val authTokenInfoResponse = chain.proceed(HikkaApi.authTokenInfo(currAuth.accessToken))
if (!authTokenInfoResponse.isSuccessful) {
authTokenInfoResponse.close()
}
val authTokenInfo = json.decodeFromString<HKAuthTokenInfo>(authTokenInfoResponse.body.string())
setAuth(HKOAuth(oauth!!.accessToken, authTokenInfo.expiration, authTokenInfo.created))
}
val authRequest = originalRequest.newBuilder()
.addHeader("auth", oauth!!.accessToken)
.addHeader("accept", "application/json")
.build()
return chain.proceed(authRequest)
}
fun setAuth(oauth: HKOAuth?) {
this.oauth = oauth
hikka.saveOAuth(oauth)
}
}

View File

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.data.track.hikka
import eu.kanade.tachiyomi.data.database.models.Track
import java.util.UUID
fun Track.toApiStatus() = when (status) {
Hikka.READING -> "reading"
Hikka.COMPLETED -> "completed"
Hikka.ON_HOLD -> "on_hold"
Hikka.DROPPED -> "dropped"
Hikka.PLAN_TO_READ -> "planned"
Hikka.REREADING -> "reading"
else -> throw NotImplementedError("Hikka: Unknown status: $status")
}
fun toTrackStatus(status: String) = when (status) {
"reading" -> Hikka.READING
"completed" -> Hikka.COMPLETED
"on_hold" -> Hikka.ON_HOLD
"dropped" -> Hikka.DROPPED
"planned" -> Hikka.PLAN_TO_READ
else -> throw NotImplementedError("Hikka: Unknown status: $status")
}
fun stringToNumber(input: String): Long {
val uuid = UUID.nameUUIDFromBytes(input.toByteArray())
return uuid.mostSignificantBits and Long.MAX_VALUE
}

View File

@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.data.track.hikka.dto
import kotlinx.serialization.Serializable
@Serializable
data class HKAuthTokenInfo(
val reference: String,
val created: Long,
val client: HKClient,
val scope: List<String>,
val expiration: Long,
val used: Long,
)

View File

@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.data.track.hikka.dto
import kotlinx.serialization.Serializable
@Serializable
data class HKClient(
val reference: String,
val name: String,
val description: String,
val verified: Boolean,
val user: HKUser,
val created: Long,
val updated: Long,
)

View File

@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.data.track.hikka.dto
import eu.kanade.tachiyomi.data.track.hikka.HikkaApi
import eu.kanade.tachiyomi.data.track.hikka.stringToNumber
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class HKManga(
@SerialName("data_type") val dataType: String,
@SerialName("title_original") val titleOriginal: String,
@SerialName("media_type") val mediaType: String,
@SerialName("title_ua") val titleUa: String? = null,
@SerialName("title_en") val titleEn: String? = null,
val chapters: Int? = null,
val volumes: Int? = null,
@SerialName("translated_ua") val translatedUa: Boolean,
val status: String,
val image: String,
val year: Int? = null,
@SerialName("scored_by") val scoredBy: Int,
val score: Double,
val slug: String,
) {
fun toTrack(trackId: Long): TrackSearch {
return TrackSearch.create(trackId).apply {
remote_id = stringToNumber(this@HKManga.slug)
title = this@HKManga.titleUa ?: this@HKManga.titleEn ?: this@HKManga.titleOriginal
total_chapters = this@HKManga.chapters?.toLong() ?: 0
cover_url = this@HKManga.image
summary = ""
score = this@HKManga.score
tracking_url = HikkaApi.BASE_URL + "/manga/${this@HKManga.slug}"
publishing_status = this@HKManga.status
publishing_type = "manga"
start_date = ""
}
}
}

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.data.track.hikka.dto
import kotlinx.serialization.Serializable
@Serializable
data class HKMangaPagination(
val pagination: HKPagination,
val list: List<HKManga>,
)

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.data.track.hikka.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class HKOAuth(
@SerialName("secret") val accessToken: String,
val expiration: Long,
val created: Long,
) {
fun isExpired(): Boolean {
return (expiration - 43200) < (System.currentTimeMillis() / 1000)
}
}

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.data.track.hikka.dto
import kotlinx.serialization.Serializable
@Serializable
data class HKPagination(
val total: Int,
val pages: Int,
val page: Int,
)

View File

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.data.track.hikka.dto
import eu.kanade.tachiyomi.data.track.hikka.HikkaApi
import eu.kanade.tachiyomi.data.track.hikka.stringToNumber
import eu.kanade.tachiyomi.data.track.hikka.toTrackStatus
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.Serializable
@Serializable
data class HKRead(
val reference: String,
val note: String?,
val updated: Long,
val created: Long,
val status: String,
val chapters: Int,
val volumes: Int,
val rereads: Int,
val score: Int,
val content: HKManga,
) {
fun toTrack(trackId: Long): TrackSearch {
return TrackSearch.create(trackId).apply {
title = this@HKRead.content.titleUa ?: this@HKRead.content.titleEn ?: this@HKRead.content.titleOriginal
remote_id = stringToNumber(this@HKRead.content.slug)
total_chapters = this@HKRead.content.chapters?.toLong() ?: 0
library_id = stringToNumber(this@HKRead.content.slug)
last_chapter_read = this@HKRead.chapters.toDouble()
score = this@HKRead.score.toDouble()
status = toTrackStatus(this@HKRead.status)
tracking_url = HikkaApi.BASE_URL + "/manga/${this@HKRead.content.slug}"
}
}
}

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.data.track.hikka.dto
import kotlinx.serialization.Serializable
@Serializable
data class HKUser(
val reference: String,
val updated: Long,
val created: Long,
val description: String,
val username: String,
val cover: String,
val active: Boolean,
val avatar: String,
val role: String,
)

View File

@ -33,6 +33,7 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_OUT_QUAD
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
import com.github.chrisbanes.photoview.PhotoView
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.coil.cropBorders
import eu.kanade.tachiyomi.data.coil.customDecoder
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
@ -40,6 +41,8 @@ import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
import okio.BufferedSource
import tachiyomi.core.common.util.system.ImageUtil
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* A wrapper view for showing page image.
@ -57,6 +60,10 @@ open class ReaderPageImageView @JvmOverloads constructor(
private val isWebtoon: Boolean = false,
) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) {
private val alwaysDecodeLongStripWithSSIV by lazy {
Injekt.get<BasePreferences>().alwaysDecodeLongStripWithSSIV().get()
}
private var pageView: View? = null
private var config: Config? = null
@ -294,7 +301,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
isVisible = true
}
is BufferedSource -> {
if (!isWebtoon) {
if (!isWebtoon || alwaysDecodeLongStripWithSSIV) {
setHardwareConfig(ImageUtil.canUseHardwareBitmap(data))
setImage(ImageSource.inputStream(data.inputStream()))
isVisible = true

View File

@ -12,6 +12,7 @@ class TrackLoginActivity : BaseOAuthLoginActivity() {
"bangumi-auth" -> handleBangumi(data)
"myanimelist-auth" -> handleMyAnimeList(data)
"shikimori-auth" -> handleShikimori(data)
"hikka-auth" -> handleHikka(data)
}
}
@ -67,4 +68,17 @@ class TrackLoginActivity : BaseOAuthLoginActivity() {
returnToSettings()
}
}
private fun handleHikka(data: Uri) {
val reference = data.getQueryParameter("reference")
if (reference != null) {
lifecycleScope.launchIO {
trackerManager.hikka.login(reference)
returnToSettings()
}
} else {
trackerManager.hikka.logout()
returnToSettings()
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -1,7 +1,7 @@
[versions]
agp_version = "8.7.3"
lifecycle_version = "2.8.7"
paging_version = "3.3.4"
paging_version = "3.3.5"
interpolator_version = "1.0.0"
[libraries]
@ -14,7 +14,7 @@ constraintlayout = "androidx.constraintlayout:constraintlayout:2.2.0"
corektx = "androidx.core:core-ktx:1.15.0"
splashscreen = "androidx.core:core-splashscreen:1.0.1"
recyclerview = "androidx.recyclerview:recyclerview:1.3.2"
viewpager = "androidx.viewpager:viewpager:1.1.0-rc01"
viewpager = "androidx.viewpager:viewpager:1.1.0"
profileinstaller = "androidx.profileinstaller:profileinstaller:1.4.1"
lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycle_version" }

View File

@ -1,5 +1,5 @@
[versions]
compose-bom = "2024.10.01"
compose-bom = "2024.12.01"
[libraries]
activity = "androidx.activity:activity-compose:1.9.3"

View File

@ -10,7 +10,7 @@ compose-compiler-gradle = { module = "org.jetbrains.kotlin:compose-compiler-grad
immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.8" }
coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.9.0" }
coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.10.1" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" }

View File

@ -7,13 +7,13 @@ richtext = "0.20.0"
shizuku_version = "13.1.0"
sqldelight = "2.0.2"
sqlite = "2.4.0"
voyager = "1.0.0"
voyager = "1.0.1"
spotless = "7.0.0.BETA4"
ktlint-core = "1.5.0"
firebase-bom = "33.7.0"
[libraries]
desugar = "com.android.tools:desugar_jdk_libs:2.1.3"
desugar = "com.android.tools:desugar_jdk_libs:2.1.4"
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
rxjava = "io.reactivex:rxjava:1.3.8"
@ -89,9 +89,9 @@ sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions-jv
sqldelight-android-paging = { module = "app.cash.sqldelight:androidx-paging3-extensions", version.ref = "sqldelight" }
sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" }
junit = "org.junit.jupiter:junit-jupiter:5.11.3"
junit = "org.junit.jupiter:junit-jupiter:5.11.4"
kotest-assertions = "io.kotest:kotest-assertions-core:5.9.1"
mockk = "io.mockk:mockk:1.13.13"
mockk = "io.mockk:mockk:1.13.14"
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

3
gradlew vendored
View File

@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

View File

@ -391,10 +391,12 @@
<string name="pref_show_page_number">Show page number</string>
<string name="pref_show_reading_mode">Show reading mode</string>
<string name="pref_show_reading_mode_summary">Briefly show current mode when reader is opened</string>
<string name="pref_display_profile">Custom display profile</string>
<string name="pref_hardware_bitmap_threshold">Custom hardware bitmap threshold</string>
<string name="pref_hardware_bitmap_threshold_default">Default (%d)</string>
<string name="pref_hardware_bitmap_threshold_summary">If reader loads a blank image incrementally reduce the threshold.\nSelected: %s</string>
<string name="pref_always_decode_long_strip_with_ssiv">Always decode long strip images with SSIV</string>
<string name="pref_always_decode_long_strip_with_ssiv_summary">Affects performance. Only enable if reducing bitmap threshold doesn\'t fix blank image issues</string>
<string name="pref_display_profile">Custom display profile</string>
<string name="pref_crop_borders">Crop borders</string>
<string name="pref_custom_brightness">Custom brightness</string>
<string name="pref_grayscale">Grayscale</string>