Upstream merge

This commit is contained in:
NerdNumber9 2019-07-27 17:56:31 -04:00
commit 7fe742e6ed
96 changed files with 1402 additions and 425 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
github: inorichi
ko_fi: inorichi

View File

@ -12,7 +12,7 @@ You can find us in the `#support-eh` channel in the [Tachiyomi discord](https://
# Features # Features
**All the features you expect from Tachiyomi:** **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 * Local reading of downloaded manga
* Configurable reader with multiple viewers, reading directions and other settings * 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 * [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), and [Kitsu](https://kitsu.io/explore/anime) support

View File

@ -76,8 +76,8 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".ui.setting.ShikomoriLoginActivity" android:name=".ui.setting.ShikimoriLoginActivity"
android:label="Shikomori"> android:label="Shikimori">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -89,6 +89,20 @@
android:scheme="tachiyomi" /> android:scheme="tachiyomi" />
</intent-filter> </intent-filter>
</activity> </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 <activity
android:name=".extension.util.ExtensionInstallActivity" android:name=".extension.util.ExtensionInstallActivity"

View File

@ -83,6 +83,11 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaViewerPutResolver()) .withPutResolver(MangaViewerPutResolver())
.prepare() .prepare()
fun updateMangaTitle(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaTitlePutResolver())
.prepare()
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare() fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()

View File

@ -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)
}
}

View File

@ -20,10 +20,10 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_VIEWER, manga.viewer) put(MangaTable.COL_VIEWER, manga.viewer)

View File

@ -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)
}
}
}

View File

@ -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.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService 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.library.LibraryUpdateService.Companion.start
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications 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. // Update favorite manga. Destroy service when completed or in case of an error.
subscription = Observable subscription = Observable
.defer { .defer {
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
val mangaList = getMangaToUpdate(intent, target) val mangaList = getMangaToUpdate(intent, target)
.sortedWith(rankingScheme[selectedScheme])
// Update either chapter list or manga details. // Update either chapter list or manga details.
when (target) { when (target) {

View File

@ -29,6 +29,8 @@ object PreferenceKeys {
const val colorFilterValue = "color_filter_value" const val colorFilterValue = "color_filter_value"
const val colorFilterMode = "color_filter_mode"
const val defaultViewer = "pref_default_viewer_key" const val defaultViewer = "pref_default_viewer_key"
const val imageScaleType = "pref_image_scale_type_key" const val imageScaleType = "pref_image_scale_type_key"
@ -85,6 +87,8 @@ object PreferenceKeys {
const val libraryUpdateCategories = "library_update_categories" const val libraryUpdateCategories = "library_update_categories"
const val libraryUpdatePrioritization = "library_update_prioritization"
const val filterDownloaded = "pref_filter_downloaded_key" const val filterDownloaded = "pref_filter_downloaded_key"
const val filterUnread = "pref_filter_unread_key" const val filterUnread = "pref_filter_unread_key"

View File

@ -58,6 +58,8 @@ class PreferencesHelper(val context: Context) {
fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0) fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0)
fun colorFilterMode() = rxPrefs.getInteger(Keys.colorFilterMode, 0)
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1) fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1)
fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 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 libraryUpdateCategories() = rxPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
fun libraryUpdatePrioritization() = rxPrefs.getInteger(Keys.libraryUpdatePrioritization, 0)
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false) fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false) fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false)

View File

@ -4,7 +4,8 @@ import android.content.Context
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist 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) { class TrackManager(private val context: Context) {
@ -12,7 +13,8 @@ class TrackManager(private val context: Context) {
const val MYANIMELIST = 1 const val MYANIMELIST = 1
const val ANILIST = 2 const val ANILIST = 2
const val KITSU = 3 const val KITSU = 3
const val SHIKOMORI = 4 const val SHIKIMORI = 4
const val BANGUMI = 5
} }
val myAnimeList = Myanimelist(context, MYANIMELIST) val myAnimeList = Myanimelist(context, MYANIMELIST)
@ -21,9 +23,11 @@ class TrackManager(private val context: Context) {
val kitsu = Kitsu(context, KITSU) 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 } fun getService(id: Int) = services.find { it.id == id }

View File

@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.data.track.bangumi
data class Avatar(
val large: String? = "",
val medium: String? = "",
val small: String? = ""
)

View File

@ -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)
}
}

View File

@ -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())
}
}

View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -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
)

View File

@ -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)
}

View File

@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.data.track.bangumi
data class Status(
val id: Int? = 0,
val name: String? = "",
val type: String? = ""
)

View File

@ -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? = ""
)

View File

@ -10,11 +10,11 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import okhttp3.HttpUrl import okhttp3.HttpUrl
import rx.Completable import rx.Completable
import rx.Observable import rx.Observable
import java.lang.Exception
class Myanimelist(private val context: Context, id: Int) : TrackService(id) { class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
companion object { companion object {
const val READING = 1 const val READING = 1
const val COMPLETED = 2 const val COMPLETED = 2
const val ON_HOLD = 3 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" 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 override val name: String
get() = "MyAnimeList" get() = "MyAnimeList"
@ -62,7 +63,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
} }
override fun add(track: Track): Observable<Track> { override fun add(track: Track): Observable<Track> {
return api.addLibManga(track, getCSRF()) return api.addLibManga(track)
} }
override fun update(track: Track): Observable<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 track.status = COMPLETED
} }
return api.updateLibManga(track, getCSRF()) return api.updateLibManga(track)
} }
override fun bind(track: Track): Observable<Track> { override fun bind(track: Track): Observable<Track> {
return api.findLibManga(track, getCSRF()) return api.findLibManga(track)
.flatMap { remoteTrack -> .flatMap { remoteTrack ->
if (remoteTrack != null) { if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
@ -93,7 +94,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
} }
override fun refresh(track: Track): Observable<Track> { override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track, getCSRF()) return api.getLibManga(track)
.map { remoteTrack -> .map { remoteTrack ->
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters 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 { override fun login(username: String, password: String): Completable {
logout() logout()
return api.login(username, password) return Observable.fromCallable { api.login(username, password) }
.doOnNext { csrf -> saveCSRF(csrf) } .doOnNext { csrf -> saveCSRF(csrf) }
.doOnNext { saveCredentials(username, password) } .doOnNext { saveCredentials(username, password) }
.doOnError { logout() } .doOnError { logout() }
.toCompletable() .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() { override fun logout() {
super.logout() super.logout()
preferences.trackToken(this).delete() preferences.trackToken(this).delete()
networkService.cookieManager.remove(HttpUrl.parse(BASE_URL)!!) networkService.cookieManager.remove(HttpUrl.parse(BASE_URL)!!)
} }
override val isLogged: Boolean val isAuthorized: Boolean
get() = !getUsername().isEmpty() && get() = super.isLogged &&
!getPassword().isEmpty() && getCSRF().isNotEmpty() &&
checkCookies() && checkCookies()
!getCSRF().isEmpty()
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) private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)

View File

@ -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())
}
}

View File

@ -22,61 +22,122 @@ import java.io.InputStreamReader
import java.util.zip.GZIPInputStream 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> { private val authClient = client.newBuilder().addInterceptor(interceptor).build()
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 }
}
}
fun search(query: String): Observable<List<TrackSearch>> { fun search(query: String): Observable<List<TrackSearch>> {
return client.newCall(GET(getSearchUrl(query))) return if (query.startsWith(PREFIX_MY)) {
.asObservable() val realQuery = query.removePrefix(PREFIX_MY)
.flatMap { response -> getList()
Observable.from(Jsoup.parse(response.consumeBody()) .flatMap { Observable.from(it) }
.select("div.js-categories-seasonal.js-block-list.list") .filter { it.title.contains(realQuery, true) }
.select("table").select("tbody") .toList()
.select("tr").drop(1)) }
} else {
.filter { row -> client.newCall(GET(searchUrl(query)))
row.select(TD)[2].text() != "Novel" .asObservable()
} .flatMap { response ->
.map { row -> Observable.from(Jsoup.parse(response.consumeBody())
TrackSearch.create(TrackManager.MYANIMELIST).apply { .select("div.js-categories-seasonal.js-block-list.list")
title = row.searchTitle() .select("table").select("tbody")
media_id = row.searchMediaId() .select("tr").drop(1))
total_chapters = row.searchTotalChapters()
summary = row.searchSummary()
cover_url = row.searchCoverUrl()
tracking_url = mangaUrl(media_id)
publishing_status = row.searchPublishingStatus()
publishing_type = row.searchPublishingType()
start_date = row.searchStartDate()
} }
} .filter { row ->
.toList() row.select(TD)[2].text() != "Novel"
}
.map { row ->
TrackSearch.create(TrackManager.MYANIMELIST).apply {
title = row.searchTitle()
media_id = row.searchMediaId()
total_chapters = row.searchTotalChapters()
summary = row.searchSummary()
cover_url = row.searchCoverUrl()
tracking_url = mangaUrl(media_id)
publishing_status = row.searchPublishingStatus()
publishing_type = row.searchPublishingType()
start_date = row.searchStartDate()
}
}
.toList()
}
} }
private fun getList(csrf: String): Observable<List<TrackSearch>> { fun addLibManga(track: Track): Observable<Track> {
return getListUrl(csrf) 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 -> .flatMap { url ->
getListXml(url) getListXml(url)
} }
.flatMap { doc -> .flatMap { doc ->
Observable.from(doc.select("manga")) Observable.from(doc.select("manga"))
} }
.map { it -> .map {
TrackSearch.create(TrackManager.MYANIMELIST).apply { TrackSearch.create(TrackManager.MYANIMELIST).apply {
title = it.selectText("manga_title")!! title = it.selectText("manga_title")!!
media_id = it.selectInt("manga_mangadb_id") media_id = it.selectInt("manga_mangadb_id")
@ -90,107 +151,8 @@ class MyanimelistApi(private val client: OkHttpClient) {
.toList() .toList()
} }
private fun getListXml(url: String): Observable<Document> { private fun getListUrl(): Observable<String> {
return client.newCall(GET(url)) return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
.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)))
.asObservable() .asObservable()
.map {response -> .map {response ->
baseUrl + Jsoup.parse(response.consumeBody()) baseUrl + Jsoup.parse(response.consumeBody())
@ -200,17 +162,17 @@ class MyanimelistApi(private val client: OkHttpClient) {
} }
} }
private fun getUpdateUrl() = Uri.parse(baseModifyListUrl).buildUpon() private fun getListXml(url: String): Observable<Document> {
.appendPath("edit.json") return authClient.newCall(GET(url))
.toString() .asObservable()
.map { response ->
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
}
}
private fun getAddUrl() = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath( "add.json")
.toString()
private fun Response.consumeBody(): String? { private fun Response.consumeBody(): String? {
use { use {
if (it.code() != 200) throw Exception("Login error") if (it.code() != 200) throw Exception("HTTP error ${it.code()}")
return it.body()?.string() return it.body()?.string()
} }
} }
@ -229,37 +191,105 @@ class MyanimelistApi(private val client: OkHttpClient) {
} }
companion object { 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 baseMangaUrl = "$baseUrl/manga/"
private const val baseModifyListUrl = "$baseUrl/ownlist/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") .attr("data-src")
.split("\\?")[0] .split("\\?")[0]
.replace("/r/50x70/", "/") .replace("/r/50x70/", "/")
fun Element.searchMediaId() = select("div.picSurround") private fun Element.searchMediaId() = select("div.picSurround")
.select("a").attr("id") .select("a").attr("id")
.replace("sarea", "") .replace("sarea", "")
.toInt() .toInt()
fun Element.searchSummary() = select("div.pt4") private fun Element.searchSummary() = select("div.pt4")
.first() .first()
.ownText()!! .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 "Reading" -> 1
"Completed" -> 2 "Completed" -> 2
"On-Hold" -> 3 "On-Hold" -> 3
@ -267,10 +297,5 @@ class MyanimelistApi(private val client: OkHttpClient) {
"Plan to Read" -> 6 "Plan to Read" -> 6
else -> 1 else -> 1
} }
const val CSRF = "csrf_token"
const val TD = "td"
private const val FINISHED = "Finished"
private const val PUBLISHING = "Publishing"
} }
} }

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.track.shikomori package eu.kanade.tachiyomi.data.track.shikimori
data class OAuth( data class OAuth(
val access_token: String, val access_token: String,

View File

@ -1,7 +1,8 @@
package eu.kanade.tachiyomi.data.track.shikomori package eu.kanade.tachiyomi.data.track.shikimori
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
@ -11,7 +12,7 @@ import rx.Completable
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy 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> { override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString) 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 const val DEFAULT_SCORE = 0
} }
override val name = "Shikomori" override val name = "Shikimori"
private val gson: Gson by injectLazy() 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) override fun getLogoColor() = Color.rgb(40, 40, 40)

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.track.shikomori package eu.kanade.tachiyomi.data.track.shikimori
import android.net.Uri import android.net.Uri
import com.github.salomonbrys.kotson.array import com.github.salomonbrys.kotson.array
@ -18,7 +18,7 @@ import okhttp3.*
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy 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 gson: Gson by injectLazy()
private val parser = JsonParser() private val parser = JsonParser()
@ -33,7 +33,7 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
"target_type" to "Manga", "target_type" to "Manga",
"chapters" to track.last_chapter_read, "chapters" to track.last_chapter_read,
"score" to track.score.toInt(), "score" to track.score.toInt(),
"status" to track.toShikomoriStatus() "status" to track.toShikimoriStatus()
) )
) )
val body = RequestBody.create(jsonime, payload.toString()) 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 { private fun jsonToSearch(obj: JsonObject): TrackSearch {
return TrackSearch.create(TrackManager.SHIKOMORI).apply { return TrackSearch.create(TrackManager.SHIKIMORI).apply {
media_id = obj["id"].asInt media_id = obj["id"].asInt
title = obj["name"].asString title = obj["name"].asString
total_chapters = obj["chapters"].asInt total_chapters = obj["chapters"].asInt
@ -87,14 +87,15 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
} }
} }
private fun jsonToTrack(obj: JsonObject): Track { private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track {
return Track.create(TrackManager.SHIKOMORI).apply { return Track.create(TrackManager.SHIKIMORI).apply {
title = mangas["name"].asString
media_id = obj["id"].asInt media_id = obj["id"].asInt
title = "" total_chapters = mangas["chapters"].asInt
last_chapter_read = obj["chapters"].asInt last_chapter_read = obj["chapters"].asInt
total_chapters = obj["chapters"].asInt
score = (obj["score"].asInt).toFloat() score = (obj["score"].asInt).toFloat()
status = toTrackStatus(obj["status"].asString) status = toTrackStatus(obj["status"].asString)
tracking_url = baseUrl + mangas["url"].asString
} }
} }
@ -108,21 +109,36 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
.url(url.toString()) .url(url.toString())
.get() .get()
.build() .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() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body()?.string().orEmpty() val responseBody = netResponse.body()?.string().orEmpty()
if (responseBody.isEmpty()) { parser.parse(responseBody).obj
throw Exception("Null Response") }.flatMap { mangas ->
} authClient.newCall(request)
val response = parser.parse(responseBody).array .asObservableSuccess()
if (response.size() > 1) { .map { netResponse ->
throw Exception("Too much mangas in response") val responseBody = netResponse.body()?.string().orEmpty()
} if (responseBody.isEmpty()) {
val entry = response.map { throw Exception("Null Response")
jsonToTrack(it.obj) }
} val response = parser.parse(responseBody).array
entry.firstOrNull() if (response.size() > 1) {
throw Exception("Too much mangas in response")
}
val entry = response.map {
jsonToTrack(it.obj, mangas)
}
entry.firstOrNull()
}
} }
} }
@ -156,10 +172,10 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc" private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0" private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
private const val baseUrl = "https://shikimori.org" private const val baseUrl = "https://shikimori.one"
private const val apiUrl = "https://shikimori.org/api" private const val apiUrl = "https://shikimori.one/api"
private const val oauthUrl = "https://shikimori.org/oauth/token" private const val oauthUrl = "https://shikimori.one/oauth/token"
private const val loginUrl = "https://shikimori.org/oauth/authorize" private const val loginUrl = "https://shikimori.one/oauth/authorize"
private const val redirectUrl = "tachiyomi://shikimori-auth" private const val redirectUrl = "tachiyomi://shikimori-auth"
private const val baseMangaUrl = "$apiUrl/mangas" private const val baseMangaUrl = "$apiUrl/mangas"

View File

@ -1,26 +1,26 @@
package eu.kanade.tachiyomi.data.track.shikomori package eu.kanade.tachiyomi.data.track.shikimori
import com.google.gson.Gson import com.google.gson.Gson
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response 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. * 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 { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() 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!! val refreshToken = currAuth.refresh_token!!
// Refresh access token if expired. // Refresh access token if expired.
if (currAuth.isExpired()) { if (currAuth.isExpired()) {
val response = chain.proceed(ShikomoriApi.refreshTokenRequest(refreshToken)) val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken))
if (response.isSuccessful) { if (response.isSuccessful) {
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java)) newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
} else { } else {
@ -38,6 +38,6 @@ class ShikomoriInterceptor(val shikomori: Shikomori, val gson: Gson) : Intercept
fun newAuth(oauth: OAuth?) { fun newAuth(oauth: OAuth?) {
this.oauth = oauth this.oauth = oauth
shikomori.saveToken(oauth) shikimori.saveToken(oauth)
} }
} }

View File

@ -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")
}

View File

@ -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")
}

View File

@ -1,7 +0,0 @@
package eu.kanade.tachiyomi.data.updater
sealed class GithubUpdateResult {
class NewUpdate(val release: GithubRelease): GithubUpdateResult()
class NoNewUpdate : GithubUpdateResult()
}

View File

@ -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
}

View File

@ -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>
}

View File

@ -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()
}

View File

@ -13,10 +13,10 @@ import eu.kanade.tachiyomi.util.notificationManager
class UpdaterJob : Job() { class UpdaterJob : Job() {
override fun onRunJob(params: Params): Result { override fun onRunJob(params: Params): Result {
return GithubUpdateChecker() return UpdateChecker.getUpdateChecker()
.checkForUpdate() .checkForUpdate()
.map { result -> .map { result ->
if (result is GithubUpdateResult.NewUpdate) { if (result is UpdateResult.NewUpdate<*>) {
val url = result.release.downloadLink val url = result.release.downloadLink
val intent = Intent(context, UpdaterService::class.java).apply { val intent = Intent(context, UpdaterService::class.java).apply {
@ -33,9 +33,9 @@ class UpdaterJob : Job() {
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)) 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. // Sadly, the task needs to be synchronous.
.toBlocking() .toBlocking()
.single() .single()
@ -64,4 +64,4 @@ class UpdaterJob : Job() {
} }
} }
} }

View File

@ -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"
}
}

View File

@ -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()
}
}
}
}

View File

@ -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()
}

View File

@ -1,24 +1,25 @@
package eu.kanade.tachiyomi.data.updater package eu.kanade.tachiyomi.data.updater.github
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import eu.kanade.tachiyomi.data.updater.Release
/** /**
* Release object. * Release object.
* Contains information about the latest release from Github. * Contains information about the latest release from Github.
* *
* @param version version of latest release. * @param version version of latest release.
* @param changeLog log of latest release. * @param info log of latest release.
* @param assets assets of latest release. * @param assets assets of latest release.
*/ */
class GithubRelease(@SerializedName("tag_name") val version: String, class GithubRelease(@SerializedName("tag_name") val version: String,
@SerializedName("body") val changeLog: String, @SerializedName("body") override val info: String,
@SerializedName("assets") private val assets: List<Assets>) { @SerializedName("assets") private val assets: List<Assets>): Release {
/** /**
* Get download link of latest release from the assets. * Get download link of latest release from the assets.
* @return download link of latest release. * @return download link of latest release.
*/ */
val downloadLink: String override val downloadLink: String
get() = assets[0].downloadLink get() = assets[0].downloadLink
/** /**

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.updater package eu.kanade.tachiyomi.data.updater.github
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import retrofit2.Retrofit import retrofit2.Retrofit
@ -30,4 +30,4 @@ interface GithubService {
@GET("/repos/NerdNumber9/tachiyomi/releases/latest") @GET("/repos/NerdNumber9/tachiyomi/releases/latest")
fun getLatestVersion(): Observable<GithubRelease> fun getLatestVersion(): Observable<GithubRelease>
} }

View File

@ -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.BuildConfig
import eu.kanade.tachiyomi.data.updater.UpdateChecker
import eu.kanade.tachiyomi.data.updater.UpdateResult
import rx.Observable import rx.Observable
class GithubUpdateChecker { class GithubUpdateChecker : UpdateChecker() {
private val service: GithubService = GithubService.create() private val service: GithubService = GithubService.create()
/** override fun checkForUpdate(): Observable<UpdateResult> {
* Returns observable containing release information
*/
fun checkForUpdate(): Observable<GithubUpdateResult> {
return service.getLatestVersion().map { release -> return service.getLatestVersion().map { release ->
val newVersion = release.version val newVersion = release.version
@ -22,4 +21,5 @@ class GithubUpdateChecker {
} }
} }
} }
}
}

View File

@ -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()
}

View File

@ -47,11 +47,12 @@ class AndroidCookieJar(context: Context) : CookieJar {
} }
fun remove(url: HttpUrl) { fun remove(url: HttpUrl) {
val cookies = manager.getCookie(url.toString()) ?: return val urlString = url.toString()
val domain = ".${url.host()}" val cookies = manager.getCookie(urlString) ?: return
cookies.split(";") cookies.split(";")
.map { it.substringBefore("=") } .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) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
syncManager.sync() syncManager.sync()

View File

@ -33,9 +33,6 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false) recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
recycler.adapter = mangaAdapter recycler.adapter = mangaAdapter
nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp,
view.context.getResourceColor(android.R.attr.textColorHint))
more.setOnClickListener { more.setOnClickListener {
val item = adapter.getItem(adapterPosition) val item = adapter.getItem(adapterPosition)
if (item != null) { if (item != null) {
@ -62,15 +59,15 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
when { when {
results == null -> { results == null -> {
progress.visible() progress.visible()
nothing_found.gone() showHolder()
} }
results.isEmpty() -> { results.isEmpty() -> {
progress.gone() progress.gone()
nothing_found.visible() hideHolder()
} }
else -> { else -> {
progress.gone() progress.gone()
nothing_found.gone() showHolder()
} }
} }
if (results !== lastBoundResults) { if (results !== lastBoundResults) {
@ -104,4 +101,15 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
return null return null
} }
private fun showHolder() {
title.visible()
source_card.visible()
}
private fun hideHolder() {
title.gone()
source_card.gone()
}
} }

View File

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.FilterList 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.model.SManga
import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
@ -157,9 +158,9 @@ open class CatalogueSearchPresenter(
fetchSourcesSubscription?.unsubscribe() fetchSourcesSubscription?.unsubscribe()
fetchSourcesSubscription = Observable.from(sources) fetchSourcesSubscription = Observable.from(sources)
.flatMap({ source -> .flatMap({ source ->
source.fetchSearchManga(1, query, FilterList()) Observable.defer { source.fetchSearchManga(1, query, FilterList()) }
.subscribeOn(Schedulers.io()) .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.mangas.take(10) } // Get at most 10 manga from search result.
.map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga. .map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
.doOnNext { fetchImage(it, source) } // Load manga covers. .doOnNext { fetchImage(it, source) } // Load manga covers.
@ -239,7 +240,7 @@ open class CatalogueSearchPresenter(
* @param sManga the manga from the source. * @param sManga the manga from the source.
* @return a manga from the database. * @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() var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
if (localManga == null) { if (localManga == null) {
val newManga = Manga.create(sManga.url, sManga.title, sourceId) val newManga = Manga.create(sManga.url, sManga.title, sourceId)

View File

@ -190,7 +190,7 @@ class LibraryPresenter(
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
when (sortingMode) { when (sortingMode) {
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title) LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
LibrarySort.LAST_READ -> { LibrarySort.LAST_READ -> {
// Get index of manga, set equal to list if size unknown. // Get index of manga, set equal to list if size unknown.
val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size

View File

@ -21,7 +21,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download 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.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
@ -31,7 +30,6 @@ import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.isEhBasedSource
import kotlinx.android.synthetic.main.chapters_controller.* import kotlinx.android.synthetic.main.chapters_controller.*
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import timber.log.Timber import timber.log.Timber

View File

@ -95,7 +95,9 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
// Set onclickListener to toggle favorite when FAB clicked. // Set onclickListener to toggle favorite when FAB clicked.
fab_favorite.clicks().subscribeUntilDestroy { onFabClick() } fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
fab_favorite.longClicks().subscribeUntilDestroy { onFabLongClick() }
// Set onLongClickListener to manage categories when FAB is clicked.
fab_favorite.longClicks().subscribeUntilDestroy{ onFabLongClick() }
// Set SwipeRefresh to refresh manga data. // Set SwipeRefresh to refresh manga data.
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() } swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
@ -439,7 +441,15 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory) defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory)
categories.size <= 1 -> // default or the one from the user categories.size <= 1 -> // default or the one from the user
presenter.moveMangaToCategory(manga, categories.firstOrNull()) presenter.moveMangaToCategory(manga, categories.firstOrNull())
else -> askCategories(manga, categories) 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)
}
} }
activity?.toast(activity?.getString(R.string.manga_added_library)) activity?.toast(activity?.getString(R.string.manga_added_library))
} else { } else {
@ -447,25 +457,28 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
} }
} }
/**
* Called when the fab is long clicked.
*/
private fun onFabLongClick() { private fun onFabLongClick() {
if(preferences.eh_askCategoryOnLongPress().getOrDefault()) { val manga = presenter.manga
val manga = presenter.manga if (!manga.favorite) {
if(!manga.favorite) toggleFavorite() toggleFavorite()
val categories = presenter.getCategories() activity?.toast(activity?.getString(R.string.manga_added_library))
if(categories.size > 1) {
askCategories(manga, categories)
}
} }
} 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()
private fun askCategories(manga: Manga, categories: List<Category>) { ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
val ids = presenter.getMangaCategoryIds(manga) .showDialog(router)
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>) { override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {

View File

@ -52,27 +52,27 @@ class TrackSearchAdapter(context: Context)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .centerCrop()
.into(view.track_search_cover) .into(view.track_search_cover)
}
if (track.publishing_status.isNullOrBlank()) { if (track.publishing_status.isNullOrBlank()) {
view.track_search_status.gone() view.track_search_status.gone()
view.track_search_status_result.gone() view.track_search_status_result.gone()
} else { } else {
view.track_search_status_result.text = track.publishing_status.capitalize() view.track_search_status_result.text = track.publishing_status.capitalize()
} }
if (track.publishing_type.isNullOrBlank()) { if (track.publishing_type.isNullOrBlank()) {
view.track_search_type.gone() view.track_search_type.gone()
view.track_search_type_result.gone() view.track_search_type_result.gone()
} else { } else {
view.track_search_type_result.text = track.publishing_type.capitalize() view.track_search_type_result.text = track.publishing_type.capitalize()
} }
if (track.start_date.isNullOrBlank()) { if (track.start_date.isNullOrBlank()) {
view.track_search_start.gone() view.track_search_start.gone()
view.track_search_start_result.gone() view.track_search_start_result.gone()
} else { } else {
view.track_search_start_result.text = track.start_date view.track_search_start_result.text = track.start_date
}
} }
} }
} }

View File

@ -146,6 +146,9 @@ class MigrationPresenter(
} }
manga.favorite = true manga.favorite = true
db.updateMangaFavorite(manga).executeAsBlocking() db.updateMangaFavorite(manga).executeAsBlocking()
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
db.updateMangaTitle(manga).executeAsBlocking()
} }
} }
} }

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.migration
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource 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.CatalogueSearchCardItem
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchItem import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchItem
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter 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 //Set the catalogue search item as highlighted if the source matches that of the selected manga
return CatalogueSearchItem(source, results, source.id == manga.source) 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
}
} }

View File

@ -777,6 +777,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
subscriptions += preferences.colorFilter().asObservable() subscriptions += preferences.colorFilter().asObservable()
.subscribe { setColorFilter(it) } .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) { private fun setColorFilterValue(value: Int) {
color_overlay.visibility = View.VISIBLE color_overlay.visibility = View.VISIBLE
color_overlay.setBackgroundColor(value) color_overlay.setFilterColor(value, preferences.colorFilterMode().getOrDefault())
} }
} }

View File

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.util.plusAssign
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
import kotlinx.android.synthetic.main.reader_color_filter.* import kotlinx.android.synthetic.main.reader_color_filter.*
import kotlinx.android.synthetic.main.reader_color_filter_sheet.* import kotlinx.android.synthetic.main.reader_color_filter_sheet.*
@ -54,6 +55,9 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
subscriptions += preferences.colorFilter().asObservable() subscriptions += preferences.colorFilter().asObservable()
.subscribe { setColorFilter(it, view) } .subscribe { setColorFilter(it, view) }
subscriptions += preferences.colorFilterMode().asObservable()
.subscribe { setColorFilter(preferences.colorFilter().getOrDefault(), view) }
subscriptions += preferences.customBrightness().asObservable() subscriptions += preferences.customBrightness().asObservable()
.subscribe { setCustomBrightness(it, view) } .subscribe { setCustomBrightness(it, view) }
@ -84,6 +88,11 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
preferences.customBrightness().set(isChecked) 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() { seekbar_color_filter_alpha.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
if (fromUser) { if (fromUser) {
@ -248,7 +257,7 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
*/ */
private fun setColorFilterValue(@ColorInt color: Int, view: View) = with(view) { private fun setColorFilterValue(@ColorInt color: Int, view: View) = with(view) {
color_overlay.visibility = View.VISIBLE color_overlay.visibility = View.VISIBLE
color_overlay.setBackgroundColor(color) color_overlay.setFilterColor(color, preferences.colorFilterMode().getOrDefault())
setValues(color, view) setValues(color, view)
} }

View File

@ -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)
}
}

View File

@ -151,10 +151,9 @@ class ReaderPresenter(
/** /**
* Called when the user pressed the back button and is going to leave the reader. Used to * 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() { fun onBackPressed() {
updateTrackLastChapterRead()
deletePendingChapters() deletePendingChapters()
} }
@ -323,7 +322,7 @@ class ReaderPresenter(
/** /**
* Called every time a page changes on the reader. Used to mark the flag of chapters being * 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. * [page]'s chapter is different from the currently active.
*/ */
fun onPageSelected(page: ReaderPage) { fun onPageSelected(page: ReaderPage) {
@ -335,6 +334,7 @@ class ReaderPresenter(
selectedChapter.chapter.last_page_read = page.index selectedChapter.chapter.last_page_read = page.index
if (selectedChapter.pages?.lastIndex == page.index) { if (selectedChapter.pages?.lastIndex == page.index) {
selectedChapter.chapter.read = true selectedChapter.chapter.read = true
updateTrackLastChapterRead()
enqueueDeleteReadChapters(selectedChapter) enqueueDeleteReadChapters(selectedChapter)
} }
@ -449,7 +449,8 @@ class ReaderPresenter(
// Build destination file. // Build destination file.
val filename = DiskUtil.buildValidFilename( 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) val destFile = File(directory, filename)
stream().use { input -> stream().use { input ->

View File

@ -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)
}
}

View File

@ -9,8 +9,8 @@ import android.view.View
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker import eu.kanade.tachiyomi.data.updater.UpdateChecker
import eu.kanade.tachiyomi.data.updater.GithubUpdateResult import eu.kanade.tachiyomi.data.updater.UpdateResult
import eu.kanade.tachiyomi.data.updater.UpdaterJob import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.data.updater.UpdaterService import eu.kanade.tachiyomi.data.updater.UpdaterService
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
@ -26,20 +26,19 @@ import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsAboutController : SettingsController() { class SettingsAboutController : SettingsController() {
/** /**
* Checks for new releases * 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 * The subscribtion service of the obtained release object
*/ */
private var releaseSubscription: Subscription? = null 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) { override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
titleRes = R.string.pref_category_about titleRes = R.string.pref_category_about
@ -109,14 +108,14 @@ class SettingsAboutController : SettingsController() {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ result -> .subscribe({ result ->
when (result) { when (result) {
is GithubUpdateResult.NewUpdate -> { is UpdateResult.NewUpdate<*> -> {
val body = result.release.changeLog val body = result.release.info
val url = result.release.downloadLink val url = result.release.downloadLink
// Create confirmation window // Create confirmation window
NewUpdateDialogController(body, url).showDialog(router) NewUpdateDialogController(body, url).showDialog(router)
} }
is GithubUpdateResult.NoNewUpdate -> { is UpdateResult.NoNewUpdate -> {
activity?.toast(R.string.update_check_no_new_updates) activity?.toast(R.string.update_check_no_new_updates)
} }
} }

View File

@ -162,6 +162,22 @@ class SettingsGeneralController : SettingsController() {
selectedCategories.joinToString { it.name } 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 { intListPreference {
key = Keys.defaultCategory key = Keys.defaultCategory
titleRes = R.string.default_category titleRes = R.string.default_category

View File

@ -8,7 +8,8 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi 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.util.getResourceColor
import eu.kanade.tachiyomi.widget.preference.LoginPreference import eu.kanade.tachiyomi.widget.preference.LoginPreference
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
@ -54,13 +55,22 @@ class SettingsTrackingController : SettingsController(),
dialog.showDialog(router) dialog.showDialog(router)
} }
} }
trackPreference(trackManager.shikomori) { trackPreference(trackManager.shikimori) {
onClick { onClick {
val tabsIntent = CustomTabsIntent.Builder() val tabsIntent = CustomTabsIntent.Builder()
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) .setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
.build() .build()
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) 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) super.onActivityResumed(activity)
// Manually refresh anilist holder // Manually refresh anilist holder
updatePreference(trackManager.aniList.id) updatePreference(trackManager.aniList.id)
updatePreference(trackManager.shikomori.id) updatePreference(trackManager.shikimori.id)
} }
private fun updatePreference(id: Int) { private fun updatePreference(id: Int) {

View File

@ -13,7 +13,7 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class ShikomoriLoginActivity : AppCompatActivity() { class ShikimoriLoginActivity : AppCompatActivity() {
private val trackManager: TrackManager by injectLazy() private val trackManager: TrackManager by injectLazy()
@ -25,7 +25,7 @@ class ShikomoriLoginActivity : AppCompatActivity() {
val code = intent.data?.getQueryParameter("code") val code = intent.data?.getQueryParameter("code")
if (code != null) { if (code != null) {
trackManager.shikomori.login(code) trackManager.shikimori.login(code)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ .subscribe({
@ -34,7 +34,7 @@ class ShikomoriLoginActivity : AppCompatActivity() {
returnToSettings() returnToSettings()
}) })
} else { } else {
trackManager.shikomori.logout() trackManager.shikimori.logout()
returnToSettings() returnToSettings()
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -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>

View File

@ -29,7 +29,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" /> android:visibility="gone" />
<View <eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
android:id="@+id/color_overlay" android:id="@+id/color_overlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -47,41 +47,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" /> 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.support.v7.widget.RecyclerView
android:id="@+id/recycler" android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -205,7 +205,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone"/> android:visibility="gone"/>
<View <eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
android:id="@+id/color_overlay" android:id="@+id/color_overlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -6,6 +6,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="16dp"> 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 --> <!-- Color filter -->
<android.support.v7.widget.SwitchCompat <android.support.v7.widget.SwitchCompat
@ -157,6 +163,27 @@
app:layout_constraintBottom_toBottomOf="@id/seekbar_color_filter_alpha" app:layout_constraintBottom_toBottomOf="@id/seekbar_color_filter_alpha"
app:layout_constraintRight_toRightOf="parent"/> 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 --> <!-- Brightness -->
<android.support.v7.widget.SwitchCompat <android.support.v7.widget.SwitchCompat
@ -165,7 +192,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/pref_custom_brightness" 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 --> <!-- Brightness value -->
@ -202,4 +229,11 @@
app:layout_constraintBottom_toBottomOf="@id/brightness_seekbar" app:layout_constraintBottom_toBottomOf="@id/brightness_seekbar"
app:layout_constraintRight_toRightOf="parent"/> 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> </android.support.constraint.ConstraintLayout>

View File

@ -21,7 +21,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" /> android:visibility="gone" />
<View <eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
android:id="@+id/color_overlay" android:id="@+id/color_overlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -40,5 +40,10 @@
android:icon="@drawable/ic_settings_black_24dp" android:icon="@drawable/ic_settings_black_24dp"
android:title="@string/label_settings" android:title="@string/label_settings"
android:checkable="false" /> 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> </group>
</menu> </menu>

View File

@ -266,7 +266,6 @@
<string name="invalid_combination">لا يمكن تحديد الإعداد الافتراضي مع الفئات الأخرى</string> <string name="invalid_combination">لا يمكن تحديد الإعداد الافتراضي مع الفئات الأخرى</string>
<string name="added_to_library">تم إضافة المانجا إلى مكتبتك</string> <string name="added_to_library">تم إضافة المانجا إلى مكتبتك</string>
<string name="action_global_search_hint">البحث الشامل…</string> <string name="action_global_search_hint">البحث الشامل…</string>
<string name="no_results">لا توجد نتائج!</string>
<string name="latest">اﻷخيرة</string> <string name="latest">اﻷخيرة</string>
<string name="browse">تصفح</string> <string name="browse">تصفح</string>

View File

@ -397,7 +397,6 @@
<string name="action_login">Вход</string> <string name="action_login">Вход</string>
<string name="other_source">Други</string> <string name="other_source">Други</string>
<string name="action_global_search_hint">Глобално търсене…</string> <string name="action_global_search_hint">Глобално търсене…</string>
<string name="no_results">Не бяха открити резултати!</string>
<string name="latest">Последни</string> <string name="latest">Последни</string>
<string name="browse">Търсене</string> <string name="browse">Търсене</string>
<string name="shortcut_created">Прекият път беше добавен към началния екран.</string> <string name="shortcut_created">Прекият път беше добавен към началния екран.</string>

View File

@ -266,7 +266,6 @@
<string name="invalid_combination">নির্ধারিতগুলো অন্যান্য ধরণের সাথে নির্বাচন করা যাবে না</string> <string name="invalid_combination">নির্ধারিতগুলো অন্যান্য ধরণের সাথে নির্বাচন করা যাবে না</string>
<string name="added_to_library">মাংগাটি আপনার মাংগাশালায় যোগ হয়েছে</string> <string name="added_to_library">মাংগাটি আপনার মাংগাশালায় যোগ হয়েছে</string>
<string name="action_global_search_hint">সার্বজনীন খোঁজ…</string> <string name="action_global_search_hint">সার্বজনীন খোঁজ…</string>
<string name="no_results">কোন ফলাফল পাওয়া যায়নি!</string>
<string name="latest">সর্বশেষ</string> <string name="latest">সর্বশেষ</string>
<string name="browse">ব্রাউজ</string> <string name="browse">ব্রাউজ</string>

View File

@ -134,7 +134,6 @@
<string name="no_more_results">Žádné další výsledky</string> <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="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="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_not_in_db">Manga byla odstraněna z databáze!</string>
<string name="manga_detail_tab">Info</string> <string name="manga_detail_tab">Info</string>
<string name="description">Popis</string> <string name="description">Popis</string>

View File

@ -398,7 +398,6 @@
<string name="other_source">Andere</string> <string name="other_source">Andere</string>
<string name="action_global_search_hint">Globale Suche…</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="latest">Letzte</string>
<string name="browse">Umsehen</string> <string name="browse">Umsehen</string>

View File

@ -301,7 +301,6 @@
<string name="added_to_library">Το manga έχει προστεθεί στη βιβλιοθήκη σας <string name="added_to_library">Το manga έχει προστεθεί στη βιβλιοθήκη σας
\n</string> \n</string>
<string name="action_global_search_hint">Καθολική αναζήτηση…</string> <string name="action_global_search_hint">Καθολική αναζήτηση…</string>
<string name="no_results">Δεν βρέθηκαν αποτελέσματα!</string>
<string name="latest">Τελευταίο</string> <string name="latest">Τελευταίο</string>
<string name="browse">Ξεφύλλισμα</string> <string name="browse">Ξεφύλλισμα</string>

View File

@ -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="local_source_badge">Local</string>
<string name="other_source">Otros</string> <string name="other_source">Otros</string>
<string name="action_global_search_hint">Búsqueda global…</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="latest">Recientes</string>
<string name="browse">Explorar</string> <string name="browse">Explorar</string>
<string name="shortcut_created">Acceso directo fue agregado a la pantalla de inicio.</string> <string name="shortcut_created">Acceso directo fue agregado a la pantalla de inicio.</string>

View File

@ -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="action_login">Connexion</string>
<string name="other_source">Autre</string> <string name="other_source">Autre</string>
<string name="action_global_search_hint">Recherche globale…</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="latest">Récents</string>
<string name="browse">Explorer</string> <string name="browse">Explorer</string>
<string name="shortcut_created">Un raccourci a été ajouté à la page d\'accueil.</string> <string name="shortcut_created">Un raccourci a été ajouté à la page d\'accueil.</string>

View File

@ -250,7 +250,6 @@
<string name="invalid_combination">डिफ़ॉल्ट को अन्य श्रेणियों के साथ नहीं चुना जा सकता है</string> <string name="invalid_combination">डिफ़ॉल्ट को अन्य श्रेणियों के साथ नहीं चुना जा सकता है</string>
<string name="added_to_library">मंगा को आपकी लाइब्रेरी में जोड़ा गया है</string> <string name="added_to_library">मंगा को आपकी लाइब्रेरी में जोड़ा गया है</string>
<string name="action_global_search_hint">वैश्विक खोज …</string> <string name="action_global_search_hint">वैश्विक खोज …</string>
<string name="no_results">कोई परिणाम नहीं मिला!</string>
<string name="latest">नवीनतम</string> <string name="latest">नवीनतम</string>
<string name="browse">ब्राउज</string> <string name="browse">ब्राउज</string>
<string name="manga_not_in_db">यह मंगा डेटाबेस से हटा दिया गया था!</string> <string name="manga_not_in_db">यह मंगा डेटाबेस से हटा दिया गया था!</string>

View File

@ -400,7 +400,6 @@
<string name="local_source_badge">Lokal</string> <string name="local_source_badge">Lokal</string>
<string name="other_source">Lainnya</string> <string name="other_source">Lainnya</string>
<string name="action_global_search_hint">Pencarian global…</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="latest">Terbaru</string>
<string name="browse">Jelajahi</string> <string name="browse">Jelajahi</string>

View File

@ -476,7 +476,6 @@
<string name="other_source">Altro</string> <string name="other_source">Altro</string>
<string name="invalid_combination">Predefinito non può essere selezionato con altre categorie</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="action_global_search_hint">Ricerca globale…</string>
<string name="no_results">Nessun risultato!</string>
<string name="latest">Ultimi</string> <string name="latest">Ultimi</string>
<string name="browse">Sfoglia</string> <string name="browse">Sfoglia</string>

View File

@ -237,7 +237,6 @@
<string name="no_valid_sources">최소한 1개의 유효한 소스를 선택해주세요</string> <string name="no_valid_sources">최소한 1개의 유효한 소스를 선택해주세요</string>
<string name="no_more_results">더이상 결과 없음</string> <string name="no_more_results">더이상 결과 없음</string>
<string name="action_global_search_hint">전역 검색…</string> <string name="action_global_search_hint">전역 검색…</string>
<string name="no_results">결과가 없습니다!</string>
<string name="latest">최신</string> <string name="latest">최신</string>
<string name="manga_detail_tab">정보</string> <string name="manga_detail_tab">정보</string>
<string name="description">설명</string> <string name="description">설명</string>

View File

@ -268,7 +268,6 @@
<string name="invalid_combination">Lalai tidak boleh dipilih bersama kategori lain</string> <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="added_to_library">Manga ini telah ditambahkan ke koleksi anda</string>
<string name="action_global_search_hint">Carian keseluruhan…</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="latest">Terkini</string>
<string name="browse">Semak imbas</string> <string name="browse">Semak imbas</string>

View File

@ -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="local_source_badge">Lokaal</string>
<string name="other_source">Alternatief</string> <string name="other_source">Alternatief</string>
<string name="action_global_search_hint">Globaal zoeken…</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="latest">Recent</string>
<string name="shortcut_created">Snelkoppeling toegevoegd aan startscherm.</string> <string name="shortcut_created">Snelkoppeling toegevoegd aan startscherm.</string>
<string name="channel_library">Bibliotheek</string> <string name="channel_library">Bibliotheek</string>

View File

@ -398,7 +398,6 @@ Nie znaleziono źródła %1$s</string>
<string name="other_source">Inne</string> <string name="other_source">Inne</string>
<string name="action_global_search_hint">Wyszukiwanie globalne…</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="browse">Przeglądaj</string>
<string name="shortcut_created">Skrót został dodany do ekranu głównego.</string> <string name="shortcut_created">Skrót został dodany do ekranu głównego.</string>

View File

@ -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="action_login">Entrar</string>
<string name="other_source">Outras</string> <string name="other_source">Outras</string>
<string name="action_global_search_hint">Pesquisa global…</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="latest">Mais recente</string>
<string name="browse">Navegar</string> <string name="browse">Navegar</string>
<string name="shortcut_created">O atalho foi adicionado à sua tela inicial.</string> <string name="shortcut_created">O atalho foi adicionado à sua tela inicial.</string>

View File

@ -299,7 +299,6 @@
<string name="invalid_combination">Modul implicit nu poate fi selectat cu alte categorii</string> <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="added_to_library">Manga-ul a fost adăugat bibliotecii tale</string>
<string name="action_global_search_hint">Căutare globală…</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="latest">Cel mai recent</string>
<string name="browse">Caută</string> <string name="browse">Caută</string>

View File

@ -360,7 +360,6 @@
<string name="local_source_badge">Локальная</string> <string name="local_source_badge">Локальная</string>
<string name="other_source">Другие</string> <string name="other_source">Другие</string>
<string name="action_global_search_hint">Глобальный поиск…</string> <string name="action_global_search_hint">Глобальный поиск…</string>
<string name="no_results">Результат не найден!</string>
<string name="latest">Последняя</string> <string name="latest">Последняя</string>
<string name="browse">Смотреть</string> <string name="browse">Смотреть</string>
<string name="shortcut_created">Ярлык был добавлен на главный экран.</string> <string name="shortcut_created">Ярлык был добавлен на главный экран.</string>

View File

@ -283,7 +283,6 @@
<string name="invalid_combination">Predefinidu non podet èssere ischertadu cun àteras categorias</string> <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="added_to_library">Su manga est istadu annantu a sa biblioteca tua</string>
<string name="action_global_search_hint">Chirca globale…</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="latest">Ùrtimos</string>
<string name="browse">Esplora</string> <string name="browse">Esplora</string>
<string name="manga_not_in_db">Custu manga est istadu bogadu dae sa base de datos!</string> <string name="manga_not_in_db">Custu manga est istadu bogadu dae sa base de datos!</string>

View File

@ -283,7 +283,6 @@
<string name="invalid_combination">Opšte je nemoguće označiti sa ostalim kategorijama</string> <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="added_to_library">Ova manga je dodata u biblioteku</string>
<string name="action_global_search_hint">Globalno pretraživanje…</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="latest">Poslednje</string>
<string name="browse">Pretraži</string> <string name="browse">Pretraži</string>
<string name="manga_not_in_db">Ova manga je uklonjena iz baze podataka!</string> <string name="manga_not_in_db">Ova manga je uklonjena iz baze podataka!</string>

View File

@ -300,7 +300,6 @@
<string name="invalid_combination">Standard inte kan väljas med andra kategorier</string> <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="added_to_library">Mangan har lagts till i din bibliotek</string>
<string name="action_global_search_hint">Global sökning…</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="latest">Senaste</string>
<string name="browse">Bläddra</string> <string name="browse">Bläddra</string>

View File

@ -281,7 +281,6 @@
<string name="invalid_combination">Öntanımlı diğer kategorilerle birlikte seçilemez</string> <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="added_to_library">Manga kitaplığınıza eklendi</string>
<string name="action_global_search_hint">Genel arama…</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="latest">En son</string>
<string name="browse">Göz at</string> <string name="browse">Göz at</string>
<string name="manga_not_in_db">Bu manga veritabanından kaldırıldı!</string> <string name="manga_not_in_db">Bu manga veritabanından kaldırıldı!</string>

View File

@ -282,7 +282,6 @@
<string name="invalid_combination">Категорія за замовчуванням не може бути вибраною разом з іншими категоріями</string> <string name="invalid_combination">Категорія за замовчуванням не може бути вибраною разом з іншими категоріями</string>
<string name="added_to_library">Цю мангу уже додано до бібліотеки</string> <string name="added_to_library">Цю мангу уже додано до бібліотеки</string>
<string name="action_global_search_hint">Глобальний пошук…</string> <string name="action_global_search_hint">Глобальний пошук…</string>
<string name="no_results">Результатів не знайдено!</string>
<string name="latest">Остання</string> <string name="latest">Остання</string>
<string name="browse">Переглянути</string> <string name="browse">Переглянути</string>
<string name="manga_not_in_db">Ця манга було видалена з бази даних!</string> <string name="manga_not_in_db">Ця манга було видалена з бази даних!</string>

View 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>

View File

@ -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="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="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="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="latest">Mới nhất</string>
<string name="browse">Duyệt</string> <string name="browse">Duyệt</string>
<string name="manga_info_full_title_label">Tiêu đề</string> <string name="manga_info_full_title_label">Tiêu đề</string>

View File

@ -281,7 +281,6 @@
<string name="invalid_combination">默认标签不能与其它标签一起选择</string> <string name="invalid_combination">默认标签不能与其它标签一起选择</string>
<string name="added_to_library">已将此漫画添加至书架</string> <string name="added_to_library">已将此漫画添加至书架</string>
<string name="action_global_search_hint">全局搜索…</string> <string name="action_global_search_hint">全局搜索…</string>
<string name="no_results">找不到!</string>
<string name="latest">最近更新</string> <string name="latest">最近更新</string>
<string name="browse">浏览</string> <string name="browse">浏览</string>
<string name="manga_not_in_db">漫画已被移出数据库!</string> <string name="manga_not_in_db">漫画已被移出数据库!</string>

View File

@ -101,4 +101,11 @@
<item>1</item> <item>1</item>
<item>2</item> <item>2</item>
</string-array> </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> </resources>

View File

@ -24,6 +24,7 @@
<string name="label_migration">Source migration</string> <string name="label_migration">Source migration</string>
<string name="label_extensions">Extensions</string> <string name="label_extensions">Extensions</string>
<string name="label_extension_info">Extension info</string> <string name="label_extension_info">Extension info</string>
<string name="label_help">Help</string>
<!-- Actions --> <!-- Actions -->
@ -131,6 +132,8 @@
<string name="update_monthly">Monthly</string> <string name="update_monthly">Monthly</string>
<string name="pref_library_update_categories">Categories to include in global update</string> <string name="pref_library_update_categories">Categories to include in global update</string>
<string name="all">All</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">Library update restrictions</string>
<string name="pref_library_update_restriction_summary">Update only when the conditions are met</string> <string name="pref_library_update_restriction_summary">Update only when the conditions are met</string>
<string name="wifi">Wi-Fi</string> <string name="wifi">Wi-Fi</string>
@ -178,6 +181,13 @@
<string name="pref_crop_borders">Crop borders</string> <string name="pref_crop_borders">Crop borders</string>
<string name="pref_custom_brightness">Use custom brightness</string> <string name="pref_custom_brightness">Use custom brightness</string>
<string name="pref_custom_color_filter">Use custom color filter</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_keep_screen_on">Keep screen on</string>
<string name="pref_skip_read_chapters">Skip chapters marked read</string> <string name="pref_skip_read_chapters">Skip chapters marked read</string>
<string name="pref_reader_navigation">Navigation</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="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="added_to_library">The manga has been added to your library</string>
<string name="action_global_search_hint">Global search…</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="latest">Latest</string>
<string name="browse">Browse</string> <string name="browse">Browse</string>