Upstream merge
This commit is contained in:
commit
7fe742e6ed
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
github: inorichi
|
||||
ko_fi: inorichi
|
@ -12,7 +12,7 @@ You can find us in the `#support-eh` channel in the [Tachiyomi discord](https://
|
||||
# Features
|
||||
|
||||
**All the features you expect from Tachiyomi:**
|
||||
* Online reading from sources such as KissManga, MangaFox, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
||||
* Online reading from sources such as KissManga, MangaDex, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
||||
* Local reading of downloaded manga
|
||||
* Configurable reader with multiple viewers, reading directions and other settings
|
||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), and [Kitsu](https://kitsu.io/explore/anime) support
|
||||
|
@ -76,8 +76,8 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.ShikomoriLoginActivity"
|
||||
android:label="Shikomori">
|
||||
android:name=".ui.setting.ShikimoriLoginActivity"
|
||||
android:label="Shikimori">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@ -89,6 +89,20 @@
|
||||
android:scheme="tachiyomi" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.BangumiLoginActivity"
|
||||
android:label="Bangumi">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="bangumi-auth"
|
||||
android:scheme="tachiyomi" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".extension.util.ExtensionInstallActivity"
|
||||
|
@ -83,6 +83,11 @@ interface MangaQueries : DbProvider {
|
||||
.withPutResolver(MangaViewerPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateMangaTitle(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaTitlePutResolver())
|
||||
.prepare()
|
||||
|
||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||
|
||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||
|
@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
class MangaTitlePutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_TITLE, manga.title)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package eu.kanade.tachiyomi.data.library
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
||||
/**
|
||||
* This class will provide various functions to Rank mangas to efficiently schedule mangas to update.
|
||||
*/
|
||||
object LibraryUpdateRanker {
|
||||
|
||||
val rankingScheme = listOf(
|
||||
(this::lexicographicRanking)(),
|
||||
(this::latestFirstRanking)())
|
||||
|
||||
/**
|
||||
* Provides a total ordering over all the Mangas.
|
||||
*
|
||||
* Assumption: An active [Manga] mActive is expected to have been last updated after an
|
||||
* inactive [Manga] mInactive.
|
||||
*
|
||||
* Using this insight, function returns a Comparator for which mActive appears before mInactive.
|
||||
* @return a Comparator that ranks manga based on relevance.
|
||||
*/
|
||||
fun latestFirstRanking(): Comparator<Manga> {
|
||||
return Comparator { mangaFirst: Manga,
|
||||
mangaSecond: Manga ->
|
||||
compareValues(mangaSecond.last_update, mangaFirst.last_update)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a total ordering over all the Mangas.
|
||||
*
|
||||
* Order the manga lexicographically.
|
||||
* @return a Comparator that ranks manga lexicographically based on the title.
|
||||
*/
|
||||
fun lexicographicRanking(): Comparator<Manga> {
|
||||
return Comparator { mangaFirst: Manga,
|
||||
mangaSecond: Manga ->
|
||||
compareValues(mangaFirst.title, mangaSecond.title)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
@ -206,7 +207,9 @@ class LibraryUpdateService(
|
||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||
subscription = Observable
|
||||
.defer {
|
||||
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
||||
val mangaList = getMangaToUpdate(intent, target)
|
||||
.sortedWith(rankingScheme[selectedScheme])
|
||||
|
||||
// Update either chapter list or manga details.
|
||||
when (target) {
|
||||
|
@ -29,6 +29,8 @@ object PreferenceKeys {
|
||||
|
||||
const val colorFilterValue = "color_filter_value"
|
||||
|
||||
const val colorFilterMode = "color_filter_mode"
|
||||
|
||||
const val defaultViewer = "pref_default_viewer_key"
|
||||
|
||||
const val imageScaleType = "pref_image_scale_type_key"
|
||||
@ -85,6 +87,8 @@ object PreferenceKeys {
|
||||
|
||||
const val libraryUpdateCategories = "library_update_categories"
|
||||
|
||||
const val libraryUpdatePrioritization = "library_update_prioritization"
|
||||
|
||||
const val filterDownloaded = "pref_filter_downloaded_key"
|
||||
|
||||
const val filterUnread = "pref_filter_unread_key"
|
||||
|
@ -58,6 +58,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0)
|
||||
|
||||
fun colorFilterMode() = rxPrefs.getInteger(Keys.colorFilterMode, 0)
|
||||
|
||||
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1)
|
||||
|
||||
fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 1)
|
||||
@ -142,6 +144,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun libraryUpdateCategories() = rxPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
||||
|
||||
fun libraryUpdatePrioritization() = rxPrefs.getInteger(Keys.libraryUpdatePrioritization, 0)
|
||||
|
||||
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
|
||||
|
||||
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false)
|
||||
|
@ -4,7 +4,8 @@ import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
||||
import eu.kanade.tachiyomi.data.track.shikomori.Shikomori
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||
|
||||
class TrackManager(private val context: Context) {
|
||||
|
||||
@ -12,7 +13,8 @@ class TrackManager(private val context: Context) {
|
||||
const val MYANIMELIST = 1
|
||||
const val ANILIST = 2
|
||||
const val KITSU = 3
|
||||
const val SHIKOMORI = 4
|
||||
const val SHIKIMORI = 4
|
||||
const val BANGUMI = 5
|
||||
}
|
||||
|
||||
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
||||
@ -21,9 +23,11 @@ class TrackManager(private val context: Context) {
|
||||
|
||||
val kitsu = Kitsu(context, KITSU)
|
||||
|
||||
val shikomori = Shikomori(context, SHIKOMORI)
|
||||
val shikimori = Shikimori(context, SHIKIMORI)
|
||||
|
||||
val services = listOf(myAnimeList, aniList, kitsu, shikomori)
|
||||
val bangumi = Bangumi(context, BANGUMI)
|
||||
|
||||
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi)
|
||||
|
||||
fun getService(id: Int) = services.find { it.id == id }
|
||||
|
||||
|
@ -0,0 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
data class Avatar(
|
||||
val large: String? = "",
|
||||
val medium: String? = "",
|
||||
val small: String? = ""
|
||||
)
|
@ -0,0 +1,144 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
override fun getScoreList(): List<String> {
|
||||
return IntRange(0, 10).map(Int::toString)
|
||||
}
|
||||
|
||||
override fun displayScore(track: Track): String {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return api.addLibManga(track)
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = COMPLETED
|
||||
}
|
||||
return api.updateLibManga(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.statusLibManga(track)
|
||||
.flatMap {
|
||||
api.findLibManga(track).flatMap { remoteTrack ->
|
||||
if (remoteTrack != null && it != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
track.status = remoteTrack.status
|
||||
track.last_chapter_read = remoteTrack.last_chapter_read
|
||||
update(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.score = DEFAULT_SCORE.toFloat()
|
||||
track.status = DEFAULT_STATUS
|
||||
add(track)
|
||||
update(track)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.statusLibManga(track)
|
||||
.flatMap {
|
||||
track.copyPersonalFrom(it!!)
|
||||
api.findLibManga(track)
|
||||
.map { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track.status = remoteTrack.status
|
||||
}
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val READING = 3
|
||||
const val COMPLETED = 2
|
||||
const val ON_HOLD = 4
|
||||
const val DROPPED = 5
|
||||
const val PLANNING = 1
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
}
|
||||
|
||||
override val name = "Bangumi"
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
|
||||
private val interceptor by lazy { BangumiInterceptor(this, gson) }
|
||||
|
||||
private val api by lazy { BangumiApi(client, interceptor) }
|
||||
|
||||
override fun getLogo() = R.drawable.bangumi
|
||||
|
||||
override fun getLogoColor() = Color.rgb(0xF0, 0x91, 0x99)
|
||||
|
||||
override fun getStatusList(): List<Int> {
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING)
|
||||
}
|
||||
|
||||
override fun getStatus(status: Int): String = with(context) {
|
||||
when (status) {
|
||||
READING -> getString(R.string.reading)
|
||||
COMPLETED -> getString(R.string.completed)
|
||||
ON_HOLD -> getString(R.string.on_hold)
|
||||
DROPPED -> getString(R.string.dropped)
|
||||
PLANNING -> getString(R.string.plan_to_read)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String) = login(password)
|
||||
|
||||
fun login(code: String): Completable {
|
||||
return api.accessToken(code).map { oauth: OAuth? ->
|
||||
interceptor.newAuth(oauth)
|
||||
if (oauth != null) {
|
||||
saveCredentials(oauth.user_id.toString(), oauth.access_token)
|
||||
}
|
||||
}.doOnError {
|
||||
logout()
|
||||
}.toCompletable()
|
||||
}
|
||||
|
||||
fun saveToken(oauth: OAuth?) {
|
||||
val json = gson.toJson(oauth)
|
||||
preferences.trackToken(this).set(json)
|
||||
}
|
||||
|
||||
fun restoreToken(): OAuth? {
|
||||
return try {
|
||||
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
preferences.trackToken(this).set(null)
|
||||
interceptor.newAuth(null)
|
||||
}
|
||||
}
|
@ -0,0 +1,208 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.obj
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
|
||||
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
private val parser = JsonParser()
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
val body = FormBody.Builder()
|
||||
.add("rating", track.score.toInt().toString())
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/collection/${track.media_id}/update")
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
// chapter update
|
||||
val body = FormBody.Builder()
|
||||
.add("watched_eps", track.last_chapter_read.toString())
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/subject/${track.media_id}/update/watched_eps")
|
||||
.post(body)
|
||||
.build()
|
||||
|
||||
// read status update
|
||||
val sbody = FormBody.Builder()
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
val srequest = Request.Builder()
|
||||
.url("$apiUrl/collection/${track.media_id}/update")
|
||||
.post(sbody)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}.flatMap {
|
||||
authClient.newCall(srequest)
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun search(search: String): Observable<List<TrackSearch>> {
|
||||
val url = Uri.parse(
|
||||
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}").buildUpon()
|
||||
.appendQueryParameter("max_results", "20")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(url.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).obj["list"]?.array
|
||||
response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||
return TrackSearch.create(TrackManager.BANGUMI).apply {
|
||||
media_id = obj["id"].asInt
|
||||
title = obj["name_cn"].asString
|
||||
cover_url = obj["images"].obj["common"].asString
|
||||
summary = obj["name"].asString
|
||||
tracking_url = obj["url"].asString
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToTrack(mangas: JsonObject): Track {
|
||||
return Track.create(TrackManager.BANGUMI).apply {
|
||||
title = mangas["name"].asString
|
||||
media_id = mangas["id"].asInt
|
||||
score = if (mangas["rating"] != null)
|
||||
(if (mangas["rating"].isJsonObject) mangas["rating"].obj["score"].asFloat else 0f)
|
||||
else 0f
|
||||
status = Bangumi.DEFAULT_STATUS
|
||||
tracking_url = mangas["url"].asString
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track): Observable<Track?> {
|
||||
val urlMangas = "$apiUrl/subject/${track.media_id}"
|
||||
val requestMangas = Request.Builder()
|
||||
.url(urlMangas)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
return authClient.newCall(requestMangas)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
// get comic info
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
jsonToTrack(parser.parse(responseBody).obj)
|
||||
}
|
||||
}
|
||||
|
||||
fun statusLibManga(track: Track): Observable<Track?> {
|
||||
val urlUserRead = "$apiUrl/collection/${track.media_id}"
|
||||
val requestUserRead = Request.Builder()
|
||||
.url(urlUserRead)
|
||||
.cacheControl(CacheControl.FORCE_NETWORK)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
// todo get user readed chapter here
|
||||
return authClient.newCall(requestUserRead)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val resp = netResponse.body()?.string()
|
||||
val coll = gson.fromJson(resp, Collection::class.java)
|
||||
track.status = coll.status?.id!!
|
||||
track.last_chapter_read = coll.ep_status!!
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun accessToken(code: String): Observable<OAuth> {
|
||||
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
gson.fromJson(responseBody, OAuth::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun accessTokenRequest(code: String) = POST(oauthUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("code", code)
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.build()
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val clientId = "bgm10555cda0762e80ca"
|
||||
private const val clientSecret = "8fff394a8627b4c388cbf349ec865775"
|
||||
|
||||
private const val baseUrl = "https://bangumi.org"
|
||||
private const val apiUrl = "https://api.bgm.tv"
|
||||
private const val oauthUrl = "https://bgm.tv/oauth/access_token"
|
||||
private const val loginUrl = "https://bgm.tv/oauth/authorize"
|
||||
|
||||
private const val redirectUrl = "tachiyomi://bangumi-auth"
|
||||
private const val baseMangaUrl = "$apiUrl/mangas"
|
||||
|
||||
fun mangaUrl(remoteId: Int): String {
|
||||
return "$baseMangaUrl/$remoteId"
|
||||
}
|
||||
|
||||
fun authUrl() =
|
||||
Uri.parse(loginUrl).buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||
.build()
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST(oauthUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("refresh_token", token)
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.build())
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
|
||||
|
||||
/**
|
||||
* OAuth object used for authenticated requests.
|
||||
*/
|
||||
private var oauth: OAuth? = bangumi.restoreToken()
|
||||
|
||||
fun addTocken(tocken: String, oidFormBody: FormBody): FormBody {
|
||||
val newFormBody = FormBody.Builder()
|
||||
for (i in 0 until oidFormBody.size()) {
|
||||
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
|
||||
}
|
||||
newFormBody.add("access_token", tocken)
|
||||
return newFormBody.build()
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
val currAuth = oauth ?: throw Exception("Not authenticated with Bangumi")
|
||||
|
||||
if (currAuth.isExpired()) {
|
||||
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refresh_token!!))
|
||||
if (response.isSuccessful) {
|
||||
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
|
||||
} else {
|
||||
response.close()
|
||||
}
|
||||
}
|
||||
|
||||
var authRequest = if (originalRequest.method() == "GET") originalRequest.newBuilder()
|
||||
.header("User-Agent", "Tachiyomi")
|
||||
.url(originalRequest.url().newBuilder()
|
||||
.addQueryParameter("access_token", currAuth.access_token).build())
|
||||
.build() else originalRequest.newBuilder()
|
||||
.post(addTocken(currAuth.access_token, originalRequest.body() as FormBody))
|
||||
.header("User-Agent", "Tachiyomi")
|
||||
.build()
|
||||
|
||||
return chain.proceed(authRequest)
|
||||
}
|
||||
|
||||
fun newAuth(oauth: OAuth?) {
|
||||
this.oauth = if (oauth == null) null else OAuth(
|
||||
oauth.access_token,
|
||||
oauth.token_type,
|
||||
System.currentTimeMillis() / 1000,
|
||||
oauth.expires_in,
|
||||
oauth.refresh_token,
|
||||
this.oauth?.user_id)
|
||||
|
||||
bangumi.saveToken(oauth)
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
|
||||
fun Track.toBangumiStatus() = when (status) {
|
||||
Bangumi.READING -> "do"
|
||||
Bangumi.COMPLETED -> "collect"
|
||||
Bangumi.ON_HOLD -> "on_hold"
|
||||
Bangumi.DROPPED -> "dropped"
|
||||
Bangumi.PLANNING -> "wish"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
|
||||
fun toTrackStatus(status: String) = when (status) {
|
||||
"do" -> Bangumi.READING
|
||||
"collect" -> Bangumi.COMPLETED
|
||||
"on_hold" -> Bangumi.ON_HOLD
|
||||
"dropped" -> Bangumi.DROPPED
|
||||
"wish" -> Bangumi.PLANNING
|
||||
|
||||
else -> throw Exception("Unknown status")
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
data class Collection(
|
||||
val `private`: Int? = 0,
|
||||
val comment: String? = "",
|
||||
val ep_status: Int? = 0,
|
||||
val lasttouch: Int? = 0,
|
||||
val rating: Int? = 0,
|
||||
val status: Status? = Status(),
|
||||
val tag: List<String?>? = listOf(),
|
||||
val user: User? = User(),
|
||||
val vol_status: Int? = 0
|
||||
)
|
@ -0,0 +1,16 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
data class OAuth(
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val created_at: Long,
|
||||
val expires_in: Long,
|
||||
val refresh_token: String?,
|
||||
val user_id: Long?
|
||||
) {
|
||||
|
||||
// Access token refersh before expired
|
||||
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
data class Status(
|
||||
val id: Int? = 0,
|
||||
val name: String? = "",
|
||||
val type: String? = ""
|
||||
)
|
@ -0,0 +1,11 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
data class User(
|
||||
val avatar: Avatar? = Avatar(),
|
||||
val id: Int? = 0,
|
||||
val nickname: String? = "",
|
||||
val sign: String? = "",
|
||||
val url: String? = "",
|
||||
val usergroup: Int? = 0,
|
||||
val username: String? = ""
|
||||
)
|
@ -10,11 +10,11 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import okhttp3.HttpUrl
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import java.lang.Exception
|
||||
|
||||
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
companion object {
|
||||
|
||||
const val READING = 1
|
||||
const val COMPLETED = 2
|
||||
const val ON_HOLD = 3
|
||||
@ -29,7 +29,8 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||
const val LOGGED_IN_COOKIE = "is_logged_in"
|
||||
}
|
||||
|
||||
private val api by lazy { MyanimelistApi(client) }
|
||||
private val interceptor by lazy { MyAnimeListInterceptor(this) }
|
||||
private val api by lazy { MyanimelistApi(client, interceptor) }
|
||||
|
||||
override val name: String
|
||||
get() = "MyAnimeList"
|
||||
@ -62,7 +63,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return api.addLibManga(track, getCSRF())
|
||||
return api.addLibManga(track)
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
@ -70,11 +71,11 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||
track.status = COMPLETED
|
||||
}
|
||||
|
||||
return api.updateLibManga(track, getCSRF())
|
||||
return api.updateLibManga(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getCSRF())
|
||||
return api.findLibManga(track)
|
||||
.flatMap { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
@ -93,7 +94,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.getLibManga(track, getCSRF())
|
||||
return api.getLibManga(track)
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
@ -104,26 +105,44 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||
override fun login(username: String, password: String): Completable {
|
||||
logout()
|
||||
|
||||
return api.login(username, password)
|
||||
return Observable.fromCallable { api.login(username, password) }
|
||||
.doOnNext { csrf -> saveCSRF(csrf) }
|
||||
.doOnNext { saveCredentials(username, password) }
|
||||
.doOnError { logout() }
|
||||
.toCompletable()
|
||||
}
|
||||
|
||||
// Attempt to login again if cookies have been cleared but credentials are still filled
|
||||
fun ensureLoggedIn() {
|
||||
if (isAuthorized) return
|
||||
if (!isLogged) throw Exception("MAL Login Credentials not found")
|
||||
|
||||
val username = getUsername()
|
||||
val password = getPassword()
|
||||
logout()
|
||||
|
||||
try {
|
||||
val csrf = api.login(username, password)
|
||||
saveCSRF(csrf)
|
||||
saveCredentials(username, password)
|
||||
} catch (e: Exception) {
|
||||
logout()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
preferences.trackToken(this).delete()
|
||||
networkService.cookieManager.remove(HttpUrl.parse(BASE_URL)!!)
|
||||
}
|
||||
|
||||
override val isLogged: Boolean
|
||||
get() = !getUsername().isEmpty() &&
|
||||
!getPassword().isEmpty() &&
|
||||
checkCookies() &&
|
||||
!getCSRF().isEmpty()
|
||||
val isAuthorized: Boolean
|
||||
get() = super.isLogged &&
|
||||
getCSRF().isNotEmpty() &&
|
||||
checkCookies()
|
||||
|
||||
private fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
|
||||
fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
|
||||
|
||||
private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
|
||||
|
||||
|
@ -0,0 +1,49 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.Response
|
||||
import okio.Buffer
|
||||
import org.json.JSONObject
|
||||
|
||||
class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
myanimelist.ensureLoggedIn()
|
||||
|
||||
var request = chain.request()
|
||||
request.body()?.let {
|
||||
val contentType = it.contentType().toString()
|
||||
val updatedBody = when {
|
||||
contentType.contains("x-www-form-urlencoded") -> updateFormBody(it)
|
||||
contentType.contains("json") -> updateJsonBody(it)
|
||||
else -> it
|
||||
}
|
||||
request = request.newBuilder().post(updatedBody).build()
|
||||
}
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
private fun bodyToString(requestBody: RequestBody): String {
|
||||
Buffer().use {
|
||||
requestBody.writeTo(it)
|
||||
return it.readUtf8()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFormBody(requestBody: RequestBody): RequestBody {
|
||||
val formString = bodyToString(requestBody)
|
||||
|
||||
return RequestBody.create(requestBody.contentType(),
|
||||
"$formString${if (formString.isNotEmpty()) "&" else ""}${MyanimelistApi.CSRF}=${myanimelist.getCSRF()}")
|
||||
}
|
||||
|
||||
private fun updateJsonBody(requestBody: RequestBody): RequestBody {
|
||||
val jsonString = bodyToString(requestBody)
|
||||
val newBody = JSONObject(jsonString)
|
||||
.put(MyanimelistApi.CSRF, myanimelist.getCSRF())
|
||||
|
||||
return RequestBody.create(requestBody.contentType(), newBody.toString())
|
||||
}
|
||||
}
|
@ -22,26 +22,20 @@ import java.io.InputStreamReader
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
|
||||
class MyanimelistApi(private val client: OkHttpClient) {
|
||||
class MyanimelistApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
||||
|
||||
fun addLibManga(track: Track, csrf: String): Observable<Track> {
|
||||
return Observable.defer {
|
||||
client.newCall(POST(url = getAddUrl(), body = getMangaPostPayload(track, csrf)))
|
||||
.asObservableSuccess()
|
||||
.map { track }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track, csrf: String): Observable<Track> {
|
||||
return Observable.defer {
|
||||
client.newCall(POST(url = getUpdateUrl(), body = getMangaPostPayload(track, csrf)))
|
||||
.asObservableSuccess()
|
||||
.map { track }
|
||||
}
|
||||
}
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun search(query: String): Observable<List<TrackSearch>> {
|
||||
return client.newCall(GET(getSearchUrl(query)))
|
||||
return if (query.startsWith(PREFIX_MY)) {
|
||||
val realQuery = query.removePrefix(PREFIX_MY)
|
||||
getList()
|
||||
.flatMap { Observable.from(it) }
|
||||
.filter { it.title.contains(realQuery, true) }
|
||||
.toList()
|
||||
}
|
||||
else {
|
||||
client.newCall(GET(searchUrl(query)))
|
||||
.asObservable()
|
||||
.flatMap { response ->
|
||||
Observable.from(Jsoup.parse(response.consumeBody())
|
||||
@ -67,16 +61,83 @@ class MyanimelistApi(private val client: OkHttpClient) {
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getList(csrf: String): Observable<List<TrackSearch>> {
|
||||
return getListUrl(csrf)
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track)))
|
||||
.asObservableSuccess()
|
||||
.map { track }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track)))
|
||||
.asObservableSuccess()
|
||||
.map { track }
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track): Observable<Track?> {
|
||||
return authClient.newCall(GET(url = listEntryUrl(track.media_id)))
|
||||
.asObservable()
|
||||
.map {response ->
|
||||
var libTrack: Track? = null
|
||||
response.use {
|
||||
if (it.priorResponse()?.isRedirect != true) {
|
||||
val trackForm = Jsoup.parse(it.consumeBody())
|
||||
|
||||
libTrack = Track.create(TrackManager.MYANIMELIST).apply {
|
||||
last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
|
||||
total_chapters = trackForm.select("#totalChap").text().toInt()
|
||||
status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
|
||||
score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull() ?: 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
libTrack
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track): Observable<Track> {
|
||||
return findLibManga(track)
|
||||
.map { it ?: throw Exception("Could not find manga") }
|
||||
}
|
||||
|
||||
fun login(username: String, password: String): String {
|
||||
val csrf = getSessionInfo()
|
||||
|
||||
login(username, password, csrf)
|
||||
|
||||
return csrf
|
||||
}
|
||||
|
||||
private fun getSessionInfo(): String {
|
||||
val response = client.newCall(GET(loginUrl())).execute()
|
||||
|
||||
return Jsoup.parse(response.consumeBody())
|
||||
.select("meta[name=csrf_token]")
|
||||
.attr("content")
|
||||
}
|
||||
|
||||
private fun login(username: String, password: String, csrf: String) {
|
||||
val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute()
|
||||
|
||||
response.use {
|
||||
if (response.priorResponse()?.code() != 302) throw Exception("Authentication error")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getList(): Observable<List<TrackSearch>> {
|
||||
return getListUrl()
|
||||
.flatMap { url ->
|
||||
getListXml(url)
|
||||
}
|
||||
.flatMap { doc ->
|
||||
Observable.from(doc.select("manga"))
|
||||
}
|
||||
.map { it ->
|
||||
.map {
|
||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||
title = it.selectText("manga_title")!!
|
||||
media_id = it.selectInt("manga_mangadb_id")
|
||||
@ -90,107 +151,8 @@ class MyanimelistApi(private val client: OkHttpClient) {
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun getListXml(url: String): Observable<Document> {
|
||||
return client.newCall(GET(url))
|
||||
.asObservable()
|
||||
.map { response ->
|
||||
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, csrf: String): Observable<Track?> {
|
||||
return getList(csrf)
|
||||
.map { list -> list.find { it.media_id == track.media_id } }
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track, csrf: String): Observable<Track> {
|
||||
return findLibManga(track, csrf)
|
||||
.map { it ?: throw Exception("Could not find manga") }
|
||||
}
|
||||
|
||||
fun login(username: String, password: String): Observable<String> {
|
||||
return getSessionInfo()
|
||||
.flatMap { csrf ->
|
||||
login(username, password, csrf)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSessionInfo(): Observable<String> {
|
||||
return client.newCall(GET(getLoginUrl()))
|
||||
.asObservable()
|
||||
.map { response ->
|
||||
Jsoup.parse(response.consumeBody())
|
||||
.select("meta[name=csrf_token]")
|
||||
.attr("content")
|
||||
}
|
||||
}
|
||||
|
||||
private fun login(username: String, password: String, csrf: String): Observable<String> {
|
||||
return client.newCall(POST(url = getLoginUrl(), body = getLoginPostBody(username, password, csrf)))
|
||||
.asObservable()
|
||||
.map { response ->
|
||||
response.use {
|
||||
if (response.priorResponse()?.code() != 302) throw Exception("Authentication error")
|
||||
}
|
||||
csrf
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLoginPostBody(username: String, password: String, csrf: String): RequestBody {
|
||||
return FormBody.Builder()
|
||||
.add("user_name", username)
|
||||
.add("password", password)
|
||||
.add("cookie", "1")
|
||||
.add("sublogin", "Login")
|
||||
.add("submit", "1")
|
||||
.add(CSRF, csrf)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getExportPostBody(csrf: String): RequestBody {
|
||||
return FormBody.Builder()
|
||||
.add("type", "2")
|
||||
.add("subexport", "Export My List")
|
||||
.add(CSRF, csrf)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getMangaPostPayload(track: Track, csrf: String): RequestBody {
|
||||
val body = JSONObject()
|
||||
.put("manga_id", track.media_id)
|
||||
.put("status", track.status)
|
||||
.put("score", track.score)
|
||||
.put("num_read_chapters", track.last_chapter_read)
|
||||
.put(CSRF, csrf)
|
||||
|
||||
return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), body.toString())
|
||||
}
|
||||
|
||||
private fun getLoginUrl() = Uri.parse(baseUrl).buildUpon()
|
||||
.appendPath("login.php")
|
||||
.toString()
|
||||
|
||||
private fun getSearchUrl(query: String): String {
|
||||
val col = "c[]"
|
||||
return Uri.parse(baseUrl).buildUpon()
|
||||
.appendPath("manga.php")
|
||||
.appendQueryParameter("q", query)
|
||||
.appendQueryParameter(col, "a")
|
||||
.appendQueryParameter(col, "b")
|
||||
.appendQueryParameter(col, "c")
|
||||
.appendQueryParameter(col, "d")
|
||||
.appendQueryParameter(col, "e")
|
||||
.appendQueryParameter(col, "g")
|
||||
.toString()
|
||||
}
|
||||
|
||||
private fun getExportListUrl() = Uri.parse(baseUrl).buildUpon()
|
||||
.appendPath("panel.php")
|
||||
.appendQueryParameter("go", "export")
|
||||
.toString()
|
||||
|
||||
private fun getListUrl(csrf: String): Observable<String> {
|
||||
return client.newCall(POST(url = getExportListUrl(), body = getExportPostBody(csrf)))
|
||||
private fun getListUrl(): Observable<String> {
|
||||
return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
|
||||
.asObservable()
|
||||
.map {response ->
|
||||
baseUrl + Jsoup.parse(response.consumeBody())
|
||||
@ -200,17 +162,17 @@ class MyanimelistApi(private val client: OkHttpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUpdateUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||
.appendPath("edit.json")
|
||||
.toString()
|
||||
|
||||
private fun getAddUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||
.appendPath( "add.json")
|
||||
.toString()
|
||||
private fun getListXml(url: String): Observable<Document> {
|
||||
return authClient.newCall(GET(url))
|
||||
.asObservable()
|
||||
.map { response ->
|
||||
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Response.consumeBody(): String? {
|
||||
use {
|
||||
if (it.code() != 200) throw Exception("Login error")
|
||||
if (it.code() != 200) throw Exception("HTTP error ${it.code()}")
|
||||
return it.body()?.string()
|
||||
}
|
||||
}
|
||||
@ -229,37 +191,105 @@ class MyanimelistApi(private val client: OkHttpClient) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val baseUrl = "https://myanimelist.net"
|
||||
const val CSRF = "csrf_token"
|
||||
|
||||
private const val baseUrl = "https://myanimelist.net"
|
||||
private const val baseMangaUrl = "$baseUrl/manga/"
|
||||
private const val baseModifyListUrl = "$baseUrl/ownlist/manga"
|
||||
private const val PREFIX_MY = "my:"
|
||||
private const val TD = "td"
|
||||
|
||||
fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
||||
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
||||
|
||||
fun Element.searchTitle() = select("strong").text()!!
|
||||
private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
|
||||
.appendPath("login.php")
|
||||
.toString()
|
||||
|
||||
fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
||||
private fun searchUrl(query: String): String {
|
||||
val col = "c[]"
|
||||
return Uri.parse(baseUrl).buildUpon()
|
||||
.appendPath("manga.php")
|
||||
.appendQueryParameter("q", query)
|
||||
.appendQueryParameter(col, "a")
|
||||
.appendQueryParameter(col, "b")
|
||||
.appendQueryParameter(col, "c")
|
||||
.appendQueryParameter(col, "d")
|
||||
.appendQueryParameter(col, "e")
|
||||
.appendQueryParameter(col, "g")
|
||||
.toString()
|
||||
}
|
||||
|
||||
fun Element.searchCoverUrl() = select("img")
|
||||
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
|
||||
.appendPath("panel.php")
|
||||
.appendQueryParameter("go", "export")
|
||||
.toString()
|
||||
|
||||
private fun updateUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||
.appendPath("edit.json")
|
||||
.toString()
|
||||
|
||||
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||
.appendPath( "add.json")
|
||||
.toString()
|
||||
|
||||
private fun listEntryUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
|
||||
.appendPath(mediaId.toString())
|
||||
.appendPath("edit")
|
||||
.toString()
|
||||
|
||||
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
|
||||
return FormBody.Builder()
|
||||
.add("user_name", username)
|
||||
.add("password", password)
|
||||
.add("cookie", "1")
|
||||
.add("sublogin", "Login")
|
||||
.add("submit", "1")
|
||||
.add(CSRF, csrf)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun exportPostBody(): RequestBody {
|
||||
return FormBody.Builder()
|
||||
.add("type", "2")
|
||||
.add("subexport", "Export My List")
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun mangaPostPayload(track: Track): RequestBody {
|
||||
val body = JSONObject()
|
||||
.put("manga_id", track.media_id)
|
||||
.put("status", track.status)
|
||||
.put("score", track.score)
|
||||
.put("num_read_chapters", track.last_chapter_read)
|
||||
|
||||
return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), body.toString())
|
||||
}
|
||||
|
||||
private fun Element.searchTitle() = select("strong").text()!!
|
||||
|
||||
private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
||||
|
||||
private fun Element.searchCoverUrl() = select("img")
|
||||
.attr("data-src")
|
||||
.split("\\?")[0]
|
||||
.replace("/r/50x70/", "/")
|
||||
|
||||
fun Element.searchMediaId() = select("div.picSurround")
|
||||
private fun Element.searchMediaId() = select("div.picSurround")
|
||||
.select("a").attr("id")
|
||||
.replace("sarea", "")
|
||||
.toInt()
|
||||
|
||||
fun Element.searchSummary() = select("div.pt4")
|
||||
private fun Element.searchSummary() = select("div.pt4")
|
||||
.first()
|
||||
.ownText()!!
|
||||
|
||||
fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") PUBLISHING else FINISHED
|
||||
private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished"
|
||||
|
||||
fun Element.searchPublishingType() = select(TD)[2].text()!!
|
||||
private fun Element.searchPublishingType() = select(TD)[2].text()!!
|
||||
|
||||
fun Element.searchStartDate() = select(TD)[6].text()!!
|
||||
private fun Element.searchStartDate() = select(TD)[6].text()!!
|
||||
|
||||
fun getStatus(status: String) = when (status) {
|
||||
private fun getStatus(status: String) = when (status) {
|
||||
"Reading" -> 1
|
||||
"Completed" -> 2
|
||||
"On-Hold" -> 3
|
||||
@ -267,10 +297,5 @@ class MyanimelistApi(private val client: OkHttpClient) {
|
||||
"Plan to Read" -> 6
|
||||
else -> 1
|
||||
}
|
||||
|
||||
const val CSRF = "csrf_token"
|
||||
const val TD = "td"
|
||||
private const val FINISHED = "Finished"
|
||||
private const val PUBLISHING = "Publishing"
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
data class OAuth(
|
||||
val access_token: String,
|
@ -1,7 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@ -11,7 +12,7 @@ import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Shikomori(private val context: Context, id: Int) : TrackService(id) {
|
||||
class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
override fun getScoreList(): List<String> {
|
||||
return IntRange(0, 10).map(Int::toString)
|
||||
@ -75,15 +76,15 @@ class Shikomori(private val context: Context, id: Int) : TrackService(id) {
|
||||
const val DEFAULT_SCORE = 0
|
||||
}
|
||||
|
||||
override val name = "Shikomori"
|
||||
override val name = "Shikimori"
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
|
||||
private val interceptor by lazy { ShikomoriInterceptor(this, gson) }
|
||||
private val interceptor by lazy { ShikimoriInterceptor(this, gson) }
|
||||
|
||||
private val api by lazy { ShikomoriApi(client, interceptor) }
|
||||
private val api by lazy { ShikimoriApi(client, interceptor) }
|
||||
|
||||
override fun getLogo() = R.drawable.shikomori
|
||||
override fun getLogo() = R.drawable.shikimori
|
||||
|
||||
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.array
|
||||
@ -18,7 +18,7 @@ import okhttp3.*
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInterceptor) {
|
||||
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
private val parser = JsonParser()
|
||||
@ -33,7 +33,7 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
||||
"target_type" to "Manga",
|
||||
"chapters" to track.last_chapter_read,
|
||||
"score" to track.score.toInt(),
|
||||
"status" to track.toShikomoriStatus()
|
||||
"status" to track.toShikimoriStatus()
|
||||
)
|
||||
)
|
||||
val body = RequestBody.create(jsonime, payload.toString())
|
||||
@ -74,7 +74,7 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
||||
}
|
||||
|
||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||
return TrackSearch.create(TrackManager.SHIKOMORI).apply {
|
||||
return TrackSearch.create(TrackManager.SHIKIMORI).apply {
|
||||
media_id = obj["id"].asInt
|
||||
title = obj["name"].asString
|
||||
total_chapters = obj["chapters"].asInt
|
||||
@ -87,14 +87,15 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToTrack(obj: JsonObject): Track {
|
||||
return Track.create(TrackManager.SHIKOMORI).apply {
|
||||
private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track {
|
||||
return Track.create(TrackManager.SHIKIMORI).apply {
|
||||
title = mangas["name"].asString
|
||||
media_id = obj["id"].asInt
|
||||
title = ""
|
||||
total_chapters = mangas["chapters"].asInt
|
||||
last_chapter_read = obj["chapters"].asInt
|
||||
total_chapters = obj["chapters"].asInt
|
||||
score = (obj["score"].asInt).toFloat()
|
||||
status = toTrackStatus(obj["status"].asString)
|
||||
tracking_url = baseUrl + mangas["url"].asString
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,7 +109,21 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
||||
.url(url.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
|
||||
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||
.appendPath(track.media_id.toString())
|
||||
.build()
|
||||
val requestMangas = Request.Builder()
|
||||
.url(urlMangas.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(requestMangas)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
parser.parse(responseBody).obj
|
||||
}.flatMap { mangas ->
|
||||
authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
@ -120,11 +135,12 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
||||
throw Exception("Too much mangas in response")
|
||||
}
|
||||
val entry = response.map {
|
||||
jsonToTrack(it.obj)
|
||||
jsonToTrack(it.obj, mangas)
|
||||
}
|
||||
entry.firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentUser(): Int {
|
||||
val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body()?.string()
|
||||
@ -156,10 +172,10 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
||||
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
|
||||
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
|
||||
|
||||
private const val baseUrl = "https://shikimori.org"
|
||||
private const val apiUrl = "https://shikimori.org/api"
|
||||
private const val oauthUrl = "https://shikimori.org/oauth/token"
|
||||
private const val loginUrl = "https://shikimori.org/oauth/authorize"
|
||||
private const val baseUrl = "https://shikimori.one"
|
||||
private const val apiUrl = "https://shikimori.one/api"
|
||||
private const val oauthUrl = "https://shikimori.one/oauth/token"
|
||||
private const val loginUrl = "https://shikimori.one/oauth/authorize"
|
||||
|
||||
private const val redirectUrl = "tachiyomi://shikimori-auth"
|
||||
private const val baseMangaUrl = "$apiUrl/mangas"
|
@ -1,26 +1,26 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class ShikomoriInterceptor(val shikomori: Shikomori, val gson: Gson) : Interceptor {
|
||||
class ShikimoriInterceptor(val shikimori: Shikimori, val gson: Gson) : Interceptor {
|
||||
|
||||
/**
|
||||
* OAuth object used for authenticated requests.
|
||||
*/
|
||||
private var oauth: OAuth? = shikomori.restoreToken()
|
||||
private var oauth: OAuth? = shikimori.restoreToken()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
val currAuth = oauth ?: throw Exception("Not authenticated with Shikomori")
|
||||
val currAuth = oauth ?: throw Exception("Not authenticated with Shikimori")
|
||||
|
||||
val refreshToken = currAuth.refresh_token!!
|
||||
|
||||
// Refresh access token if expired.
|
||||
if (currAuth.isExpired()) {
|
||||
val response = chain.proceed(ShikomoriApi.refreshTokenRequest(refreshToken))
|
||||
val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken))
|
||||
if (response.isSuccessful) {
|
||||
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
|
||||
} else {
|
||||
@ -38,6 +38,6 @@ class ShikomoriInterceptor(val shikomori: Shikomori, val gson: Gson) : Intercept
|
||||
|
||||
fun newAuth(oauth: OAuth?) {
|
||||
this.oauth = oauth
|
||||
shikomori.saveToken(oauth)
|
||||
shikimori.saveToken(oauth)
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
|
||||
fun Track.toShikimoriStatus() = when (status) {
|
||||
Shikimori.READING -> "watching"
|
||||
Shikimori.COMPLETED -> "completed"
|
||||
Shikimori.ON_HOLD -> "on_hold"
|
||||
Shikimori.DROPPED -> "dropped"
|
||||
Shikimori.PLANNING -> "planned"
|
||||
Shikimori.REPEATING -> "rewatching"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
|
||||
fun toTrackStatus(status: String) = when (status) {
|
||||
"watching" -> Shikimori.READING
|
||||
"completed" -> Shikimori.COMPLETED
|
||||
"on_hold" -> Shikimori.ON_HOLD
|
||||
"dropped" -> Shikimori.DROPPED
|
||||
"planned" -> Shikimori.PLANNING
|
||||
"rewatching" -> Shikimori.REPEATING
|
||||
|
||||
else -> throw Exception("Unknown status")
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
|
||||
fun Track.toShikomoriStatus() = when (status) {
|
||||
Shikomori.READING -> "watching"
|
||||
Shikomori.COMPLETED -> "completed"
|
||||
Shikomori.ON_HOLD -> "on_hold"
|
||||
Shikomori.DROPPED -> "dropped"
|
||||
Shikomori.PLANNING -> "planned"
|
||||
Shikomori.REPEATING -> "rewatching"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
|
||||
fun toTrackStatus(status: String) = when (status) {
|
||||
"watching" -> Shikomori.READING
|
||||
"completed" -> Shikomori.COMPLETED
|
||||
"on_hold" -> Shikomori.ON_HOLD
|
||||
"dropped" -> Shikomori.DROPPED
|
||||
"planned" -> Shikomori.PLANNING
|
||||
"rewatching" -> Shikomori.REPEATING
|
||||
|
||||
else -> throw Exception("Unknown status")
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
sealed class GithubUpdateResult {
|
||||
|
||||
class NewUpdate(val release: GithubRelease): GithubUpdateResult()
|
||||
class NoNewUpdate : GithubUpdateResult()
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
interface Release {
|
||||
|
||||
val info: String
|
||||
|
||||
/**
|
||||
* Get download link of latest release.
|
||||
* @return download link of latest release.
|
||||
*/
|
||||
val downloadLink: String
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.updater.devrepo.DevRepoUpdateChecker
|
||||
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
||||
import rx.Observable
|
||||
|
||||
abstract class UpdateChecker {
|
||||
|
||||
companion object {
|
||||
fun getUpdateChecker(): UpdateChecker {
|
||||
return if (BuildConfig.DEBUG) {
|
||||
DevRepoUpdateChecker()
|
||||
} else {
|
||||
GithubUpdateChecker()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns observable containing release information
|
||||
*/
|
||||
abstract fun checkForUpdate(): Observable<UpdateResult>
|
||||
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
abstract class UpdateResult {
|
||||
|
||||
open class NewUpdate<T : Release>(val release: T): UpdateResult()
|
||||
open class NoNewUpdate: UpdateResult()
|
||||
|
||||
}
|
@ -13,10 +13,10 @@ import eu.kanade.tachiyomi.util.notificationManager
|
||||
class UpdaterJob : Job() {
|
||||
|
||||
override fun onRunJob(params: Params): Result {
|
||||
return GithubUpdateChecker()
|
||||
return UpdateChecker.getUpdateChecker()
|
||||
.checkForUpdate()
|
||||
.map { result ->
|
||||
if (result is GithubUpdateResult.NewUpdate) {
|
||||
if (result is UpdateResult.NewUpdate<*>) {
|
||||
val url = result.release.downloadLink
|
||||
|
||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||
@ -33,9 +33,9 @@ class UpdaterJob : Job() {
|
||||
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
}
|
||||
}
|
||||
Job.Result.SUCCESS
|
||||
Result.SUCCESS
|
||||
}
|
||||
.onErrorReturn { Job.Result.FAILURE }
|
||||
.onErrorReturn { Result.FAILURE }
|
||||
// Sadly, the task needs to be synchronous.
|
||||
.toBlocking()
|
||||
.single()
|
||||
|
@ -0,0 +1,14 @@
|
||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
||||
|
||||
import eu.kanade.tachiyomi.data.updater.Release
|
||||
|
||||
class DevRepoRelease(override val info: String) : Release {
|
||||
|
||||
override val downloadLink: String
|
||||
get() = LATEST_URL
|
||||
|
||||
companion object {
|
||||
const val LATEST_URL = "https://tachiyomi.kanade.eu/latest"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
import okhttp3.OkHttpClient
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class DevRepoUpdateChecker : UpdateChecker() {
|
||||
|
||||
private val client: OkHttpClient by lazy {
|
||||
Injekt.get<NetworkHelper>().client.newBuilder()
|
||||
.followRedirects(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val versionRegex: Regex by lazy {
|
||||
Regex("tachiyomi-r(\\d+).apk")
|
||||
}
|
||||
|
||||
override fun checkForUpdate(): Observable<UpdateResult> {
|
||||
return client.newCall(GET(DevRepoRelease.LATEST_URL)).asObservable()
|
||||
.map { response ->
|
||||
// Get latest repo version number from header in format "Location: tachiyomi-r1512.apk"
|
||||
val latestVersionNumber: String = versionRegex.find(response.header("Location")!!)!!.groupValues[1]
|
||||
|
||||
if (latestVersionNumber.toInt() > BuildConfig.COMMIT_COUNT.toInt()) {
|
||||
DevRepoUpdateResult.NewUpdate(DevRepoRelease("v$latestVersionNumber"))
|
||||
} else {
|
||||
DevRepoUpdateResult.NoNewUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
||||
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
|
||||
sealed class DevRepoUpdateResult : UpdateResult() {
|
||||
|
||||
class NewUpdate(release: DevRepoRelease): UpdateResult.NewUpdate<DevRepoRelease>(release)
|
||||
class NoNewUpdate: UpdateResult.NoNewUpdate()
|
||||
|
||||
}
|
@ -1,24 +1,25 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
package eu.kanade.tachiyomi.data.updater.github
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import eu.kanade.tachiyomi.data.updater.Release
|
||||
|
||||
/**
|
||||
* Release object.
|
||||
* Contains information about the latest release from Github.
|
||||
*
|
||||
* @param version version of latest release.
|
||||
* @param changeLog log of latest release.
|
||||
* @param info log of latest release.
|
||||
* @param assets assets of latest release.
|
||||
*/
|
||||
class GithubRelease(@SerializedName("tag_name") val version: String,
|
||||
@SerializedName("body") val changeLog: String,
|
||||
@SerializedName("assets") private val assets: List<Assets>) {
|
||||
@SerializedName("body") override val info: String,
|
||||
@SerializedName("assets") private val assets: List<Assets>): Release {
|
||||
|
||||
/**
|
||||
* Get download link of latest release from the assets.
|
||||
* @return download link of latest release.
|
||||
*/
|
||||
val downloadLink: String
|
||||
override val downloadLink: String
|
||||
get() = assets[0].downloadLink
|
||||
|
||||
/**
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
package eu.kanade.tachiyomi.data.updater.github
|
||||
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import retrofit2.Retrofit
|
@ -1,16 +1,15 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
package eu.kanade.tachiyomi.data.updater.github
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
import rx.Observable
|
||||
|
||||
class GithubUpdateChecker {
|
||||
class GithubUpdateChecker : UpdateChecker() {
|
||||
|
||||
private val service: GithubService = GithubService.create()
|
||||
|
||||
/**
|
||||
* Returns observable containing release information
|
||||
*/
|
||||
fun checkForUpdate(): Observable<GithubUpdateResult> {
|
||||
override fun checkForUpdate(): Observable<UpdateResult> {
|
||||
return service.getLatestVersion().map { release ->
|
||||
val newVersion = release.version
|
||||
|
||||
@ -22,4 +21,5 @@ class GithubUpdateChecker {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package eu.kanade.tachiyomi.data.updater.github
|
||||
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
|
||||
sealed class GithubUpdateResult : UpdateResult() {
|
||||
|
||||
class NewUpdate(release: GithubRelease): UpdateResult.NewUpdate<GithubRelease>(release)
|
||||
class NoNewUpdate : UpdateResult.NoNewUpdate()
|
||||
|
||||
}
|
@ -47,11 +47,12 @@ class AndroidCookieJar(context: Context) : CookieJar {
|
||||
}
|
||||
|
||||
fun remove(url: HttpUrl) {
|
||||
val cookies = manager.getCookie(url.toString()) ?: return
|
||||
val domain = ".${url.host()}"
|
||||
val urlString = url.toString()
|
||||
val cookies = manager.getCookie(urlString) ?: return
|
||||
|
||||
cookies.split(";")
|
||||
.map { it.substringBefore("=") }
|
||||
.onEach { manager.setCookie(domain, "$it=;Max-Age=-1") }
|
||||
.onEach { manager.setCookie(urlString, "$it=;Max-Age=-1") }
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
syncManager.sync()
|
||||
|
@ -33,9 +33,6 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
|
||||
recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
|
||||
recycler.adapter = mangaAdapter
|
||||
|
||||
nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp,
|
||||
view.context.getResourceColor(android.R.attr.textColorHint))
|
||||
|
||||
more.setOnClickListener {
|
||||
val item = adapter.getItem(adapterPosition)
|
||||
if (item != null) {
|
||||
@ -62,15 +59,15 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
|
||||
when {
|
||||
results == null -> {
|
||||
progress.visible()
|
||||
nothing_found.gone()
|
||||
showHolder()
|
||||
}
|
||||
results.isEmpty() -> {
|
||||
progress.gone()
|
||||
nothing_found.visible()
|
||||
hideHolder()
|
||||
}
|
||||
else -> {
|
||||
progress.gone()
|
||||
nothing_found.gone()
|
||||
showHolder()
|
||||
}
|
||||
}
|
||||
if (results !== lastBoundResults) {
|
||||
@ -104,4 +101,15 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun showHolder() {
|
||||
title.visible()
|
||||
source_card.visible()
|
||||
}
|
||||
|
||||
private fun hideHolder() {
|
||||
title.gone()
|
||||
source_card.gone()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
@ -157,9 +158,9 @@ open class CatalogueSearchPresenter(
|
||||
fetchSourcesSubscription?.unsubscribe()
|
||||
fetchSourcesSubscription = Observable.from(sources)
|
||||
.flatMap({ source ->
|
||||
source.fetchSearchManga(1, query, FilterList())
|
||||
Observable.defer { source.fetchSearchManga(1, query, FilterList()) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.onExceptionResumeNext(Observable.empty()) // Ignore timeouts.
|
||||
.onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions
|
||||
.map { it.mangas.take(10) } // Get at most 10 manga from search result.
|
||||
.map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
|
||||
.doOnNext { fetchImage(it, source) } // Load manga covers.
|
||||
@ -239,7 +240,7 @@ open class CatalogueSearchPresenter(
|
||||
* @param sManga the manga from the source.
|
||||
* @return a manga from the database.
|
||||
*/
|
||||
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||
protected open fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
|
||||
if (localManga == null) {
|
||||
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
||||
|
@ -190,7 +190,7 @@ class LibraryPresenter(
|
||||
|
||||
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
|
||||
when (sortingMode) {
|
||||
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title)
|
||||
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
|
||||
LibrarySort.LAST_READ -> {
|
||||
// Get index of manga, set equal to list if size unknown.
|
||||
val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size
|
||||
|
@ -21,7 +21,6 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
@ -31,7 +30,6 @@ import eu.kanade.tachiyomi.util.snack
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.isEhBasedSource
|
||||
import kotlinx.android.synthetic.main.chapters_controller.*
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import timber.log.Timber
|
||||
|
@ -95,6 +95,8 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
|
||||
// Set onclickListener to toggle favorite when FAB clicked.
|
||||
fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
|
||||
|
||||
// Set onLongClickListener to manage categories when FAB is clicked.
|
||||
fab_favorite.longClicks().subscribeUntilDestroy{ onFabLongClick() }
|
||||
|
||||
// Set SwipeRefresh to refresh manga data.
|
||||
@ -439,26 +441,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory)
|
||||
categories.size <= 1 -> // default or the one from the user
|
||||
presenter.moveMangaToCategory(manga, categories.firstOrNull())
|
||||
else -> askCategories(manga, categories)
|
||||
}
|
||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||
} else {
|
||||
activity?.toast(activity?.getString(R.string.manga_removed_library))
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFabLongClick() {
|
||||
if(preferences.eh_askCategoryOnLongPress().getOrDefault()) {
|
||||
val manga = presenter.manga
|
||||
if(!manga.favorite) toggleFavorite()
|
||||
val categories = presenter.getCategories()
|
||||
if(categories.size > 1) {
|
||||
askCategories(manga, categories)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun askCategories(manga: Manga, categories: List<Category>) {
|
||||
else -> {
|
||||
val ids = presenter.getMangaCategoryIds(manga)
|
||||
val preselected = ids.mapNotNull { id ->
|
||||
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
||||
@ -467,6 +450,36 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||
.showDialog(router)
|
||||
}
|
||||
}
|
||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||
} else {
|
||||
activity?.toast(activity?.getString(R.string.manga_removed_library))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the fab is long clicked.
|
||||
*/
|
||||
private fun onFabLongClick() {
|
||||
val manga = presenter.manga
|
||||
if (!manga.favorite) {
|
||||
toggleFavorite()
|
||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||
}
|
||||
val categories = presenter.getCategories()
|
||||
if (categories.size <= 1) {
|
||||
// default or the one from the user then just add to favorite.
|
||||
presenter.moveMangaToCategory(manga, categories.firstOrNull())
|
||||
} else {
|
||||
val ids = presenter.getMangaCategoryIds(manga)
|
||||
val preselected = ids.mapNotNull { id ->
|
||||
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
||||
}.toTypedArray()
|
||||
|
||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||
.showDialog(router)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
||||
val manga = mangas.firstOrNull() ?: return
|
||||
|
@ -52,6 +52,7 @@ class TrackSearchAdapter(context: Context)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.into(view.track_search_cover)
|
||||
}
|
||||
|
||||
if (track.publishing_status.isNullOrBlank()) {
|
||||
view.track_search_status.gone()
|
||||
@ -76,4 +77,3 @@ class TrackSearchAdapter(context: Context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -146,6 +146,9 @@ class MigrationPresenter(
|
||||
}
|
||||
manga.favorite = true
|
||||
db.updateMangaFavorite(manga).executeAsBlocking()
|
||||
|
||||
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
|
||||
db.updateMangaTitle(manga).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.migration
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchCardItem
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchItem
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
|
||||
@ -21,4 +22,11 @@ class SearchPresenter(
|
||||
//Set the catalogue search item as highlighted if the source matches that of the selected manga
|
||||
return CatalogueSearchItem(source, results, source.id == manga.source)
|
||||
}
|
||||
|
||||
override fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||
val localManga = super.networkToLocalManga(sManga, sourceId)
|
||||
// For migration, displayed title should always match source rather than local DB
|
||||
localManga.title = sManga.title
|
||||
return localManga
|
||||
}
|
||||
}
|
||||
|
@ -777,6 +777,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
|
||||
subscriptions += preferences.colorFilter().asObservable()
|
||||
.subscribe { setColorFilter(it) }
|
||||
|
||||
subscriptions += preferences.colorFilterMode().asObservable()
|
||||
.subscribe { setColorFilter(preferences.colorFilter().getOrDefault()) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -925,7 +928,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
*/
|
||||
private fun setColorFilterValue(value: Int) {
|
||||
color_overlay.visibility = View.VISIBLE
|
||||
color_overlay.setBackgroundColor(value)
|
||||
color_overlay.setFilterColor(value, preferences.colorFilterMode().getOrDefault())
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
|
||||
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
|
||||
import kotlinx.android.synthetic.main.reader_color_filter.*
|
||||
import kotlinx.android.synthetic.main.reader_color_filter_sheet.*
|
||||
@ -54,6 +55,9 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
|
||||
subscriptions += preferences.colorFilter().asObservable()
|
||||
.subscribe { setColorFilter(it, view) }
|
||||
|
||||
subscriptions += preferences.colorFilterMode().asObservable()
|
||||
.subscribe { setColorFilter(preferences.colorFilter().getOrDefault(), view) }
|
||||
|
||||
subscriptions += preferences.customBrightness().asObservable()
|
||||
.subscribe { setCustomBrightness(it, view) }
|
||||
|
||||
@ -84,6 +88,11 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
|
||||
preferences.customBrightness().set(isChecked)
|
||||
}
|
||||
|
||||
color_filter_mode.onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
|
||||
preferences.colorFilterMode().set(position)
|
||||
}
|
||||
color_filter_mode.setSelection(preferences.colorFilterMode().getOrDefault(), false)
|
||||
|
||||
seekbar_color_filter_alpha.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
|
||||
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
@ -248,7 +257,7 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
|
||||
*/
|
||||
private fun setColorFilterValue(@ColorInt color: Int, view: View) = with(view) {
|
||||
color_overlay.visibility = View.VISIBLE
|
||||
color_overlay.setBackgroundColor(color)
|
||||
color_overlay.setFilterColor(color, preferences.colorFilterMode().getOrDefault())
|
||||
setValues(color, view)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
|
||||
class ReaderColorFilterView(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : View(context, attrs) {
|
||||
|
||||
private val colorFilterPaint: Paint = Paint()
|
||||
|
||||
fun setFilterColor(color: Int, filterMode: Int) {
|
||||
colorFilterPaint.setColor(color)
|
||||
colorFilterPaint.xfermode = PorterDuffXfermode(when (filterMode) {
|
||||
1 -> PorterDuff.Mode.MULTIPLY
|
||||
2 -> PorterDuff.Mode.SCREEN
|
||||
3 -> PorterDuff.Mode.OVERLAY
|
||||
4 -> PorterDuff.Mode.LIGHTEN
|
||||
5 -> PorterDuff.Mode.DARKEN
|
||||
else -> PorterDuff.Mode.SRC_OVER
|
||||
})
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
canvas.drawPaint(colorFilterPaint)
|
||||
}
|
||||
}
|
@ -151,10 +151,9 @@ class ReaderPresenter(
|
||||
|
||||
/**
|
||||
* Called when the user pressed the back button and is going to leave the reader. Used to
|
||||
* update tracking services and trigger deletion of the downloaded chapters.
|
||||
* trigger deletion of the downloaded chapters.
|
||||
*/
|
||||
fun onBackPressed() {
|
||||
updateTrackLastChapterRead()
|
||||
deletePendingChapters()
|
||||
}
|
||||
|
||||
@ -323,7 +322,7 @@ class ReaderPresenter(
|
||||
|
||||
/**
|
||||
* Called every time a page changes on the reader. Used to mark the flag of chapters being
|
||||
* read, enqueue downloaded chapter deletion, and updating the active chapter if this
|
||||
* read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this
|
||||
* [page]'s chapter is different from the currently active.
|
||||
*/
|
||||
fun onPageSelected(page: ReaderPage) {
|
||||
@ -335,6 +334,7 @@ class ReaderPresenter(
|
||||
selectedChapter.chapter.last_page_read = page.index
|
||||
if (selectedChapter.pages?.lastIndex == page.index) {
|
||||
selectedChapter.chapter.read = true
|
||||
updateTrackLastChapterRead()
|
||||
enqueueDeleteReadChapters(selectedChapter)
|
||||
}
|
||||
|
||||
@ -449,7 +449,8 @@ class ReaderPresenter(
|
||||
|
||||
// Build destination file.
|
||||
val filename = DiskUtil.buildValidFilename(
|
||||
"${manga.title} - ${chapter.name}") + " - ${page.number}.${type.extension}"
|
||||
"${manga.title} - ${chapter.name}".take(225)
|
||||
) + " - ${page.number}.${type.extension}"
|
||||
|
||||
val destFile = File(directory, filename)
|
||||
stream().use { input ->
|
||||
|
@ -0,0 +1,50 @@
|
||||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.Gravity.CENTER
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class BangumiLoginActivity : AppCompatActivity() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
val view = ProgressBar(this)
|
||||
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
|
||||
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
trackManager.bangumi.login(code)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
returnToSettings()
|
||||
}, {
|
||||
returnToSettings()
|
||||
})
|
||||
} else {
|
||||
trackManager.bangumi.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun returnToSettings() {
|
||||
finish()
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
@ -9,8 +9,8 @@ import android.view.View
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker
|
||||
import eu.kanade.tachiyomi.data.updater.GithubUpdateResult
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterService
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
@ -26,20 +26,19 @@ import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
|
||||
|
||||
class SettingsAboutController : SettingsController() {
|
||||
|
||||
/**
|
||||
* Checks for new releases
|
||||
*/
|
||||
private val updateChecker by lazy { GithubUpdateChecker() }
|
||||
private val updateChecker by lazy { UpdateChecker.getUpdateChecker() }
|
||||
|
||||
/**
|
||||
* The subscribtion service of the obtained release object
|
||||
*/
|
||||
private var releaseSubscription: Subscription? = null
|
||||
|
||||
private val isUpdaterEnabled = !BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER
|
||||
private val isUpdaterEnabled = BuildConfig.INCLUDE_UPDATER
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
|
||||
titleRes = R.string.pref_category_about
|
||||
@ -109,14 +108,14 @@ class SettingsAboutController : SettingsController() {
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ result ->
|
||||
when (result) {
|
||||
is GithubUpdateResult.NewUpdate -> {
|
||||
val body = result.release.changeLog
|
||||
is UpdateResult.NewUpdate<*> -> {
|
||||
val body = result.release.info
|
||||
val url = result.release.downloadLink
|
||||
|
||||
// Create confirmation window
|
||||
NewUpdateDialogController(body, url).showDialog(router)
|
||||
}
|
||||
is GithubUpdateResult.NoNewUpdate -> {
|
||||
is UpdateResult.NoNewUpdate -> {
|
||||
activity?.toast(R.string.update_check_no_new_updates)
|
||||
}
|
||||
}
|
||||
|
@ -162,6 +162,22 @@ class SettingsGeneralController : SettingsController() {
|
||||
selectedCategories.joinToString { it.name }
|
||||
}
|
||||
}
|
||||
intListPreference{
|
||||
key = Keys.libraryUpdatePrioritization
|
||||
titleRes = R.string.pref_library_update_prioritization
|
||||
// The following arrays are to be lined up with the list rankingScheme in:
|
||||
// ../../data/library/LibraryUpdateRanker.kt
|
||||
entriesRes = arrayOf(
|
||||
R.string.action_sort_alpha,
|
||||
R.string.action_sort_last_updated
|
||||
)
|
||||
entryValues = arrayOf(
|
||||
"0",
|
||||
"1"
|
||||
)
|
||||
defaultValue = "0"
|
||||
summaryRes = R.string.pref_library_update_prioritization_summary
|
||||
}
|
||||
intListPreference {
|
||||
key = Keys.defaultCategory
|
||||
titleRes = R.string.default_category
|
||||
|
@ -8,7 +8,8 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||
import eu.kanade.tachiyomi.data.track.shikomori.ShikomoriApi
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
|
||||
@ -54,13 +55,22 @@ class SettingsTrackingController : SettingsController(),
|
||||
dialog.showDialog(router)
|
||||
}
|
||||
}
|
||||
trackPreference(trackManager.shikomori) {
|
||||
trackPreference(trackManager.shikimori) {
|
||||
onClick {
|
||||
val tabsIntent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
|
||||
.build()
|
||||
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
tabsIntent.launchUrl(activity, ShikomoriApi.authUrl())
|
||||
tabsIntent.launchUrl(activity, ShikimoriApi.authUrl())
|
||||
}
|
||||
}
|
||||
trackPreference(trackManager.bangumi) {
|
||||
onClick {
|
||||
val tabsIntent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
|
||||
.build()
|
||||
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
tabsIntent.launchUrl(activity, BangumiApi.authUrl())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,7 +90,7 @@ class SettingsTrackingController : SettingsController(),
|
||||
super.onActivityResumed(activity)
|
||||
// Manually refresh anilist holder
|
||||
updatePreference(trackManager.aniList.id)
|
||||
updatePreference(trackManager.shikomori.id)
|
||||
updatePreference(trackManager.shikimori.id)
|
||||
}
|
||||
|
||||
private fun updatePreference(id: Int) {
|
||||
|
@ -13,7 +13,7 @@ import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ShikomoriLoginActivity : AppCompatActivity() {
|
||||
class ShikimoriLoginActivity : AppCompatActivity() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
@ -25,7 +25,7 @@ class ShikomoriLoginActivity : AppCompatActivity() {
|
||||
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
trackManager.shikomori.login(code)
|
||||
trackManager.shikimori.login(code)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
@ -34,7 +34,7 @@ class ShikomoriLoginActivity : AppCompatActivity() {
|
||||
returnToSettings()
|
||||
})
|
||||
} else {
|
||||
trackManager.shikomori.logout()
|
||||
trackManager.shikimori.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
BIN
app/src/main/res/drawable-xxxhdpi/bangumi.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/bangumi.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="112dp"
|
||||
android:height="112dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
|
||||
</vector>
|
@ -29,7 +29,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
<eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
|
||||
android:id="@+id/color_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -47,41 +47,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<android.support.constraint.ConstraintLayout
|
||||
android:id="@+id/nothing_found"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/nothing_found_icon"
|
||||
android:layout_width="112dp"
|
||||
android:layout_height="112dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@mipmap/ic_launcher" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nothing_found_text"
|
||||
style="@style/TextAppearance.Regular.Caption.Hint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="0dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/no_results"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/nothing_found_icon" />
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -205,7 +205,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<View
|
||||
<eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
|
||||
android:id="@+id/color_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -6,6 +6,12 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:padding="16dp">
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:id="@+id/spinner_end"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintLeft_toRightOf="parent" />
|
||||
|
||||
<!-- Color filter -->
|
||||
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
@ -157,6 +163,27 @@
|
||||
app:layout_constraintBottom_toBottomOf="@id/seekbar_color_filter_alpha"
|
||||
app:layout_constraintRight_toRightOf="parent"/>
|
||||
|
||||
<!-- Filter mode -->
|
||||
|
||||
<TextView
|
||||
android:id="@+id/color_filter_mode_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pref_color_filter_mode"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/color_filter_mode"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/color_filter_mode"/>
|
||||
|
||||
<android.support.v7.widget.AppCompatSpinner
|
||||
android:id="@+id/color_filter_mode"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:entries="@array/color_filter_modes"
|
||||
app:layout_constraintTop_toBottomOf="@id/seekbar_color_filter_alpha"
|
||||
app:layout_constraintLeft_toRightOf="@id/verticalcenter"
|
||||
app:layout_constraintRight_toRightOf="@id/spinner_end" />
|
||||
|
||||
<!-- Brightness -->
|
||||
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
@ -165,7 +192,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/pref_custom_brightness"
|
||||
app:layout_constraintTop_toBottomOf="@id/seekbar_color_filter_alpha"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/color_filter_mode_text"/>
|
||||
|
||||
<!-- Brightness value -->
|
||||
|
||||
@ -202,4 +229,11 @@
|
||||
app:layout_constraintBottom_toBottomOf="@id/brightness_seekbar"
|
||||
app:layout_constraintRight_toRightOf="parent"/>
|
||||
|
||||
<android.support.constraint.Guideline
|
||||
android:id="@+id/verticalcenter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.5" />
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
|
@ -21,7 +21,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
<eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
|
||||
android:id="@+id/color_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -40,5 +40,10 @@
|
||||
android:icon="@drawable/ic_settings_black_24dp"
|
||||
android:title="@string/label_settings"
|
||||
android:checkable="false" />
|
||||
<item
|
||||
android:id="@+id/nav_drawer_help"
|
||||
android:icon="@drawable/ic_help_black_24dp"
|
||||
android:title="@string/label_help"
|
||||
android:checkable="false" />
|
||||
</group>
|
||||
</menu>
|
||||
|
@ -266,7 +266,6 @@
|
||||
<string name="invalid_combination">لا يمكن تحديد الإعداد الافتراضي مع الفئات الأخرى</string>
|
||||
<string name="added_to_library">تم إضافة المانجا إلى مكتبتك</string>
|
||||
<string name="action_global_search_hint">البحث الشامل…</string>
|
||||
<string name="no_results">لا توجد نتائج!</string>
|
||||
<string name="latest">اﻷخيرة</string>
|
||||
<string name="browse">تصفح</string>
|
||||
|
||||
|
@ -397,7 +397,6 @@
|
||||
<string name="action_login">Вход</string>
|
||||
<string name="other_source">Други</string>
|
||||
<string name="action_global_search_hint">Глобално търсене…</string>
|
||||
<string name="no_results">Не бяха открити резултати!</string>
|
||||
<string name="latest">Последни</string>
|
||||
<string name="browse">Търсене</string>
|
||||
<string name="shortcut_created">Прекият път беше добавен към началния екран.</string>
|
||||
|
@ -266,7 +266,6 @@
|
||||
<string name="invalid_combination">নির্ধারিতগুলো অন্যান্য ধরণের সাথে নির্বাচন করা যাবে না</string>
|
||||
<string name="added_to_library">মাংগাটি আপনার মাংগাশালায় যোগ হয়েছে</string>
|
||||
<string name="action_global_search_hint">সার্বজনীন খোঁজ…</string>
|
||||
<string name="no_results">কোন ফলাফল পাওয়া যায়নি!</string>
|
||||
<string name="latest">সর্বশেষ</string>
|
||||
<string name="browse">ব্রাউজ</string>
|
||||
|
||||
|
@ -134,7 +134,6 @@
|
||||
<string name="no_more_results">Žádné další výsledky</string>
|
||||
<string name="added_to_library">Manga byla přidána do vaší knihovny</string>
|
||||
<string name="action_global_search_hint">Globální vyhledávání…</string>
|
||||
<string name="no_results">Žádné výsledky!</string>
|
||||
<string name="manga_not_in_db">Manga byla odstraněna z databáze!</string>
|
||||
<string name="manga_detail_tab">Info</string>
|
||||
<string name="description">Popis</string>
|
||||
|
@ -398,7 +398,6 @@
|
||||
|
||||
<string name="other_source">Andere</string>
|
||||
<string name="action_global_search_hint">Globale Suche…</string>
|
||||
<string name="no_results">Keine Treffer gefunden!</string>
|
||||
<string name="latest">Letzte</string>
|
||||
<string name="browse">Umsehen</string>
|
||||
|
||||
|
@ -301,7 +301,6 @@
|
||||
<string name="added_to_library">Το manga έχει προστεθεί στη βιβλιοθήκη σας
|
||||
\n</string>
|
||||
<string name="action_global_search_hint">Καθολική αναζήτηση…</string>
|
||||
<string name="no_results">Δεν βρέθηκαν αποτελέσματα!</string>
|
||||
<string name="latest">Τελευταίο</string>
|
||||
<string name="browse">Ξεφύλλισμα</string>
|
||||
|
||||
|
@ -395,7 +395,6 @@ También asegúrese de haber iniciado sesión en las fuentes que lo requieren an
|
||||
<string name="local_source_badge">Local</string>
|
||||
<string name="other_source">Otros</string>
|
||||
<string name="action_global_search_hint">Búsqueda global…</string>
|
||||
<string name="no_results">Ningún resultado encontrado!</string>
|
||||
<string name="latest">Recientes</string>
|
||||
<string name="browse">Explorar</string>
|
||||
<string name="shortcut_created">Acceso directo fue agregado a la pantalla de inicio.</string>
|
||||
|
@ -396,7 +396,6 @@ Assurez-vous que vous êtes connecté à des sources qui le demande avant de com
|
||||
<string name="action_login">Connexion</string>
|
||||
<string name="other_source">Autre</string>
|
||||
<string name="action_global_search_hint">Recherche globale…</string>
|
||||
<string name="no_results">Aucun résultat !</string>
|
||||
<string name="latest">Récents</string>
|
||||
<string name="browse">Explorer</string>
|
||||
<string name="shortcut_created">Un raccourci a été ajouté à la page d\'accueil.</string>
|
||||
|
@ -250,7 +250,6 @@
|
||||
<string name="invalid_combination">डिफ़ॉल्ट को अन्य श्रेणियों के साथ नहीं चुना जा सकता है</string>
|
||||
<string name="added_to_library">मंगा को आपकी लाइब्रेरी में जोड़ा गया है</string>
|
||||
<string name="action_global_search_hint">वैश्विक खोज …</string>
|
||||
<string name="no_results">कोई परिणाम नहीं मिला!</string>
|
||||
<string name="latest">नवीनतम</string>
|
||||
<string name="browse">ब्राउज</string>
|
||||
<string name="manga_not_in_db">यह मंगा डेटाबेस से हटा दिया गया था!</string>
|
||||
|
@ -400,7 +400,6 @@
|
||||
<string name="local_source_badge">Lokal</string>
|
||||
<string name="other_source">Lainnya</string>
|
||||
<string name="action_global_search_hint">Pencarian global…</string>
|
||||
<string name="no_results">Tidak ada hasil!</string>
|
||||
<string name="latest">Terbaru</string>
|
||||
<string name="browse">Jelajahi</string>
|
||||
|
||||
|
@ -476,7 +476,6 @@
|
||||
<string name="other_source">Altro</string>
|
||||
<string name="invalid_combination">Predefinito non può essere selezionato con altre categorie</string>
|
||||
<string name="action_global_search_hint">Ricerca globale…</string>
|
||||
<string name="no_results">Nessun risultato!</string>
|
||||
<string name="latest">Ultimi</string>
|
||||
<string name="browse">Sfoglia</string>
|
||||
|
||||
|
@ -237,7 +237,6 @@
|
||||
<string name="no_valid_sources">최소한 1개의 유효한 소스를 선택해주세요</string>
|
||||
<string name="no_more_results">더이상 결과 없음</string>
|
||||
<string name="action_global_search_hint">전역 검색…</string>
|
||||
<string name="no_results">결과가 없습니다!</string>
|
||||
<string name="latest">최신</string>
|
||||
<string name="manga_detail_tab">정보</string>
|
||||
<string name="description">설명</string>
|
||||
|
@ -268,7 +268,6 @@
|
||||
<string name="invalid_combination">Lalai tidak boleh dipilih bersama kategori lain</string>
|
||||
<string name="added_to_library">Manga ini telah ditambahkan ke koleksi anda</string>
|
||||
<string name="action_global_search_hint">Carian keseluruhan…</string>
|
||||
<string name="no_results">Tiada sebarang hasil!</string>
|
||||
<string name="latest">Terkini</string>
|
||||
<string name="browse">Semak imbas</string>
|
||||
|
||||
|
@ -365,7 +365,6 @@ Zorg ook dat je ingelogd bent voor bronnen die dit vereisen alvorens je het teru
|
||||
<string name="local_source_badge">Lokaal</string>
|
||||
<string name="other_source">Alternatief</string>
|
||||
<string name="action_global_search_hint">Globaal zoeken…</string>
|
||||
<string name="no_results">Geen resultaten gevonden!</string>
|
||||
<string name="latest">Recent</string>
|
||||
<string name="shortcut_created">Snelkoppeling toegevoegd aan startscherm.</string>
|
||||
<string name="channel_library">Bibliotheek</string>
|
||||
|
@ -398,7 +398,6 @@ Nie znaleziono źródła %1$s</string>
|
||||
|
||||
<string name="other_source">Inne</string>
|
||||
<string name="action_global_search_hint">Wyszukiwanie globalne…</string>
|
||||
<string name="no_results">Brak wyników!</string>
|
||||
<string name="browse">Przeglądaj</string>
|
||||
|
||||
<string name="shortcut_created">Skrót został dodany do ekranu głównego.</string>
|
||||
|
@ -362,7 +362,6 @@ Além disso, verifique se as fontes que requerem uma conta foram configuradas co
|
||||
<string name="action_login">Entrar</string>
|
||||
<string name="other_source">Outras</string>
|
||||
<string name="action_global_search_hint">Pesquisa global…</string>
|
||||
<string name="no_results">Nenhum resultado encontrado!</string>
|
||||
<string name="latest">Mais recente</string>
|
||||
<string name="browse">Navegar</string>
|
||||
<string name="shortcut_created">O atalho foi adicionado à sua tela inicial.</string>
|
||||
|
@ -299,7 +299,6 @@
|
||||
<string name="invalid_combination">Modul implicit nu poate fi selectat cu alte categorii</string>
|
||||
<string name="added_to_library">Manga-ul a fost adăugat bibliotecii tale</string>
|
||||
<string name="action_global_search_hint">Căutare globală…</string>
|
||||
<string name="no_results">Nici un rezultat găsit!</string>
|
||||
<string name="latest">Cel mai recent</string>
|
||||
<string name="browse">Caută</string>
|
||||
|
||||
|
@ -360,7 +360,6 @@
|
||||
<string name="local_source_badge">Локальная</string>
|
||||
<string name="other_source">Другие</string>
|
||||
<string name="action_global_search_hint">Глобальный поиск…</string>
|
||||
<string name="no_results">Результат не найден!</string>
|
||||
<string name="latest">Последняя</string>
|
||||
<string name="browse">Смотреть</string>
|
||||
<string name="shortcut_created">Ярлык был добавлен на главный экран.</string>
|
||||
|
@ -283,7 +283,6 @@
|
||||
<string name="invalid_combination">Predefinidu non podet èssere ischertadu cun àteras categorias</string>
|
||||
<string name="added_to_library">Su manga est istadu annantu a sa biblioteca tua</string>
|
||||
<string name="action_global_search_hint">Chirca globale…</string>
|
||||
<string name="no_results">Perunu risultadu agatadu!</string>
|
||||
<string name="latest">Ùrtimos</string>
|
||||
<string name="browse">Esplora</string>
|
||||
<string name="manga_not_in_db">Custu manga est istadu bogadu dae sa base de datos!</string>
|
||||
|
@ -283,7 +283,6 @@
|
||||
<string name="invalid_combination">Opšte je nemoguće označiti sa ostalim kategorijama</string>
|
||||
<string name="added_to_library">Ova manga je dodata u biblioteku</string>
|
||||
<string name="action_global_search_hint">Globalno pretraživanje…</string>
|
||||
<string name="no_results">Nema pronađenih rezultata!</string>
|
||||
<string name="latest">Poslednje</string>
|
||||
<string name="browse">Pretraži</string>
|
||||
<string name="manga_not_in_db">Ova manga je uklonjena iz baze podataka!</string>
|
||||
|
@ -300,7 +300,6 @@
|
||||
<string name="invalid_combination">Standard inte kan väljas med andra kategorier</string>
|
||||
<string name="added_to_library">Mangan har lagts till i din bibliotek</string>
|
||||
<string name="action_global_search_hint">Global sökning…</string>
|
||||
<string name="no_results">Inga resultat hittades!</string>
|
||||
<string name="latest">Senaste</string>
|
||||
<string name="browse">Bläddra</string>
|
||||
|
||||
|
@ -281,7 +281,6 @@
|
||||
<string name="invalid_combination">Öntanımlı diğer kategorilerle birlikte seçilemez</string>
|
||||
<string name="added_to_library">Manga kitaplığınıza eklendi</string>
|
||||
<string name="action_global_search_hint">Genel arama…</string>
|
||||
<string name="no_results">Sonuç bulunmadı!</string>
|
||||
<string name="latest">En son</string>
|
||||
<string name="browse">Göz at</string>
|
||||
<string name="manga_not_in_db">Bu manga veritabanından kaldırıldı!</string>
|
||||
|
@ -282,7 +282,6 @@
|
||||
<string name="invalid_combination">Категорія за замовчуванням не може бути вибраною разом з іншими категоріями</string>
|
||||
<string name="added_to_library">Цю мангу уже додано до бібліотеки</string>
|
||||
<string name="action_global_search_hint">Глобальний пошук…</string>
|
||||
<string name="no_results">Результатів не знайдено!</string>
|
||||
<string name="latest">Остання</string>
|
||||
<string name="browse">Переглянути</string>
|
||||
<string name="manga_not_in_db">Ця манга було видалена з бази даних!</string>
|
||||
|
15
app/src/main/res/values-v28/arrays.xml
Normal file
15
app/src/main/res/values-v28/arrays.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string-array name="color_filter_modes">
|
||||
<item>@string/filter_mode_default</item>
|
||||
<item>@string/filter_mode_multiply</item>
|
||||
<item>@string/filter_mode_screen</item>
|
||||
|
||||
<!-- Attributes specific for SDK 28 and up -->
|
||||
<item>@string/filter_mode_overlay</item>
|
||||
<item>@string/filter_mode_lighten</item>
|
||||
<item>@string/filter_mode_darken</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
@ -461,7 +461,6 @@
|
||||
<string name="invalid_combination">Mặc định không thể chọn với các danh mục khác</string>
|
||||
<string name="added_to_library">Truyện này đã được thêm vào thư viện</string>
|
||||
<string name="action_global_search_hint">Tìm kiếm toàn cầu…</string>
|
||||
<string name="no_results">Không tìm thấy kết quả!</string>
|
||||
<string name="latest">Mới nhất</string>
|
||||
<string name="browse">Duyệt</string>
|
||||
<string name="manga_info_full_title_label">Tiêu đề</string>
|
||||
|
@ -281,7 +281,6 @@
|
||||
<string name="invalid_combination">默认标签不能与其它标签一起选择</string>
|
||||
<string name="added_to_library">已将此漫画添加至书架</string>
|
||||
<string name="action_global_search_hint">全局搜索…</string>
|
||||
<string name="no_results">找不到!</string>
|
||||
<string name="latest">最近更新</string>
|
||||
<string name="browse">浏览</string>
|
||||
<string name="manga_not_in_db">漫画已被移出数据库!</string>
|
||||
|
@ -101,4 +101,11 @@
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="color_filter_modes">
|
||||
<item>@string/filter_mode_default</item>
|
||||
<item>@string/filter_mode_multiply</item>
|
||||
<item>@string/filter_mode_screen</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
@ -24,6 +24,7 @@
|
||||
<string name="label_migration">Source migration</string>
|
||||
<string name="label_extensions">Extensions</string>
|
||||
<string name="label_extension_info">Extension info</string>
|
||||
<string name="label_help">Help</string>
|
||||
|
||||
|
||||
<!-- Actions -->
|
||||
@ -131,6 +132,8 @@
|
||||
<string name="update_monthly">Monthly</string>
|
||||
<string name="pref_library_update_categories">Categories to include in global update</string>
|
||||
<string name="all">All</string>
|
||||
<string name="pref_library_update_prioritization">Library update order</string>
|
||||
<string name="pref_library_update_prioritization_summary">Select the order in which Tachiyomi checks for update</string>
|
||||
<string name="pref_library_update_restriction">Library update restrictions</string>
|
||||
<string name="pref_library_update_restriction_summary">Update only when the conditions are met</string>
|
||||
<string name="wifi">Wi-Fi</string>
|
||||
@ -178,6 +181,13 @@
|
||||
<string name="pref_crop_borders">Crop borders</string>
|
||||
<string name="pref_custom_brightness">Use custom brightness</string>
|
||||
<string name="pref_custom_color_filter">Use custom color filter</string>
|
||||
<string name="pref_color_filter_mode">Color filter blend mode</string>
|
||||
<string name="filter_mode_default">Default</string>
|
||||
<string name="filter_mode_overlay">Overlay</string>
|
||||
<string name="filter_mode_multiply">Multiply</string>
|
||||
<string name="filter_mode_screen">Screen</string>
|
||||
<string name="filter_mode_lighten">Dodge / Lighten</string>
|
||||
<string name="filter_mode_darken">Burn / Darken</string>
|
||||
<string name="pref_keep_screen_on">Keep screen on</string>
|
||||
<string name="pref_skip_read_chapters">Skip chapters marked read</string>
|
||||
<string name="pref_reader_navigation">Navigation</string>
|
||||
@ -318,7 +328,6 @@
|
||||
<string name="invalid_combination">Default can\'t be selected with other categories</string>
|
||||
<string name="added_to_library">The manga has been added to your library</string>
|
||||
<string name="action_global_search_hint">Global search…</string>
|
||||
<string name="no_results">No results found!</string>
|
||||
<string name="latest">Latest</string>
|
||||
<string name="browse">Browse</string>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user