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