diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d11579ed8..d6a848e13 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -180,6 +180,7 @@ + diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt index 2a7231112..4eadac04c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt @@ -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)), ), ), diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt index 1071fa7ee..1935a6b1e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt @@ -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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt new file mode 100644 index 000000000..3bd43a574 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt @@ -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 { + 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 { + 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 { + 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(trackPreferences.trackToken(this).get()) + } catch (e: Exception) { + null + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt new file mode 100644 index 000000000..6cb9fd9d1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt @@ -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() + } + } + } + + suspend fun accessToken(reference: String): HKOAuth { + return withIOContext { + with(json) { + client.newCall(authTokenCreate(reference)) + .awaitSuccess() + .parseAs() + } + } + } + + suspend fun searchManga(query: String): List { + 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() + .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() + } + } + } + } + + 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() + .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() + .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) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt new file mode 100644 index 000000000..062b88861 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt @@ -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(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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt new file mode 100644 index 000000000..a31b692c9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt @@ -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 +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKAuthTokenInfo.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKAuthTokenInfo.kt new file mode 100644 index 000000000..7f0e096a6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKAuthTokenInfo.kt @@ -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, + val expiration: Long, + val used: Long, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKClient.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKClient.kt new file mode 100644 index 000000000..824cffbb9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKClient.kt @@ -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, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt new file mode 100644 index 000000000..40928aa6e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt @@ -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 = "" + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKMangaPagination.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKMangaPagination.kt new file mode 100644 index 000000000..a8245fe65 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKMangaPagination.kt @@ -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, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt new file mode 100644 index 000000000..fbccb8d22 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt @@ -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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKPagination.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKPagination.kt new file mode 100644 index 000000000..3c7e5f2c7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKPagination.kt @@ -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, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt new file mode 100644 index 000000000..213ac2951 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt @@ -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}" + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKUser.kt new file mode 100644 index 000000000..4210c6b16 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKUser.kt @@ -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, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt index 3f742cfe0..9487e57f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt @@ -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() + } + } } diff --git a/app/src/main/res/drawable/ic_tracker_hikka.webp b/app/src/main/res/drawable/ic_tracker_hikka.webp new file mode 100644 index 000000000..6ff0e77f3 Binary files /dev/null and b/app/src/main/res/drawable/ic_tracker_hikka.webp differ