mirror of
https://github.com/mihonapp/mihon.git
synced 2024-12-25 18:38:23 +01:00
Compare commits
29 Commits
15f656fdc9
...
edfd4a1e08
Author | SHA1 | Date | |
---|---|---|---|
|
edfd4a1e08 | ||
|
c5655e8803 | ||
|
d3973f4ad8 | ||
|
bb230fd6a7 | ||
|
e526fd44c6 | ||
|
f61f039a45 | ||
|
79eb02d8f0 | ||
|
814584d35b | ||
|
8751307301 | ||
|
bcff2262b3 | ||
|
04454ecdbe | ||
|
e9b41975da | ||
|
ef3369be26 | ||
|
4a3209267d | ||
|
e44666673f | ||
|
f480d4cb38 | ||
|
a8f4e63d54 | ||
|
45cd945f7c | ||
|
6b8c2dcdc3 | ||
|
23a34cd3b7 | ||
|
e332a5018e | ||
|
41db4a5865 | ||
|
f39e947ece | ||
|
9eeed9f050 | ||
|
03f9ae8bb2 | ||
|
32db1da753 | ||
|
06f01de4b5 | ||
|
f6d8d415cc | ||
|
68d24dff27 |
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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)),
|
||||
),
|
||||
),
|
||||
|
@ -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 }
|
||||
|
||||
|
163
app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt
Normal file
163
app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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,
|
||||
)
|
@ -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,
|
||||
)
|
@ -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 = ""
|
||||
}
|
||||
}
|
||||
}
|
@ -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>,
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
BIN
app/src/main/res/drawable/ic_tracker_hikka.webp
Normal file
BIN
app/src/main/res/drawable/ic_tracker_hikka.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
@ -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" }
|
||||
|
@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
compose-bom = "2024.10.01"
|
||||
compose-bom = "2024.12.01"
|
||||
|
||||
[libraries]
|
||||
activity = "androidx.activity:activity-compose:1.9.3"
|
||||
|
@ -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" }
|
||||
|
@ -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" }
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
3
gradlew
vendored
@ -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
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user