Upstream merge

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

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

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

View File

@ -12,7 +12,7 @@ You can find us in the `#support-eh` channel in the [Tachiyomi discord](https://
# Features
**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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class MangaTitlePutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_TITLE, manga.title)
}
}

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.data.library
import eu.kanade.tachiyomi.data.database.models.Manga
/**
* This class will provide various functions to Rank mangas to efficiently schedule mangas to update.
*/
object LibraryUpdateRanker {
val rankingScheme = listOf(
(this::lexicographicRanking)(),
(this::latestFirstRanking)())
/**
* Provides a total ordering over all the Mangas.
*
* Assumption: An active [Manga] mActive is expected to have been last updated after an
* inactive [Manga] mInactive.
*
* Using this insight, function returns a Comparator for which mActive appears before mInactive.
* @return a Comparator that ranks manga based on relevance.
*/
fun latestFirstRanking(): Comparator<Manga> {
return Comparator { mangaFirst: Manga,
mangaSecond: Manga ->
compareValues(mangaSecond.last_update, mangaFirst.last_update)
}
}
/**
* Provides a total ordering over all the Mangas.
*
* Order the manga lexicographically.
* @return a Comparator that ranks manga lexicographically based on the title.
*/
fun lexicographicRanking(): Comparator<Manga> {
return Comparator { mangaFirst: Manga,
mangaSecond: Manga ->
compareValues(mangaFirst.title, mangaSecond.title)
}
}
}

View File

@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.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) {

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,144 @@
package eu.kanade.tachiyomi.data.track.bangumi
import android.content.Context
import android.graphics.Color
import com.google.gson.Gson
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
class Bangumi(private val context: Context, id: Int) : TrackService(id) {
override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString)
}
override fun displayScore(track: Track): String {
return track.score.toInt().toString()
}
override fun add(track: Track): Observable<Track> {
return api.addLibManga(track)
}
override fun update(track: Track): Observable<Track> {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED
}
return api.updateLibManga(track)
}
override fun bind(track: Track): Observable<Track> {
return api.statusLibManga(track)
.flatMap {
api.findLibManga(track).flatMap { remoteTrack ->
if (remoteTrack != null && it != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
track.status = remoteTrack.status
track.last_chapter_read = remoteTrack.last_chapter_read
update(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
update(track)
}
}
}
}
override fun search(query: String): Observable<List<TrackSearch>> {
return api.search(query)
}
override fun refresh(track: Track): Observable<Track> {
return api.statusLibManga(track)
.flatMap {
track.copyPersonalFrom(it!!)
api.findLibManga(track)
.map { remoteTrack ->
if (remoteTrack != null) {
track.total_chapters = remoteTrack.total_chapters
track.status = remoteTrack.status
}
track
}
}
}
companion object {
const val READING = 3
const val COMPLETED = 2
const val ON_HOLD = 4
const val DROPPED = 5
const val PLANNING = 1
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
}
override val name = "Bangumi"
private val gson: Gson by injectLazy()
private val interceptor by lazy { BangumiInterceptor(this, gson) }
private val api by lazy { BangumiApi(client, interceptor) }
override fun getLogo() = R.drawable.bangumi
override fun getLogoColor() = Color.rgb(0xF0, 0x91, 0x99)
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING)
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLANNING -> getString(R.string.plan_to_read)
else -> ""
}
}
override fun login(username: String, password: String) = login(password)
fun login(code: String): Completable {
return api.accessToken(code).map { oauth: OAuth? ->
interceptor.newAuth(oauth)
if (oauth != null) {
saveCredentials(oauth.user_id.toString(), oauth.access_token)
}
}.doOnError {
logout()
}.toCompletable()
}
fun saveToken(oauth: OAuth?) {
val json = gson.toJson(oauth)
preferences.trackToken(this).set(json)
}
fun restoreToken(): OAuth? {
return try {
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
} catch (e: Exception) {
null
}
}
override fun logout() {
super.logout()
preferences.trackToken(this).set(null)
interceptor.newAuth(null)
}
}

View File

@ -0,0 +1,208 @@
package eu.kanade.tachiyomi.data.track.bangumi
import android.net.Uri
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.obj
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
private val gson: Gson by injectLazy()
private val parser = JsonParser()
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track): Observable<Track> {
val body = FormBody.Builder()
.add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus())
.build()
val request = Request.Builder()
.url("$apiUrl/collection/${track.media_id}/update")
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map {
track
}
}
fun updateLibManga(track: Track): Observable<Track> {
// chapter update
val body = FormBody.Builder()
.add("watched_eps", track.last_chapter_read.toString())
.build()
val request = Request.Builder()
.url("$apiUrl/subject/${track.media_id}/update/watched_eps")
.post(body)
.build()
// read status update
val sbody = FormBody.Builder()
.add("status", track.toBangumiStatus())
.build()
val srequest = Request.Builder()
.url("$apiUrl/collection/${track.media_id}/update")
.post(sbody)
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map {
track
}.flatMap {
authClient.newCall(srequest)
.asObservableSuccess()
.map {
track
}
}
}
fun search(search: String): Observable<List<TrackSearch>> {
val url = Uri.parse(
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}").buildUpon()
.appendQueryParameter("max_results", "20")
.build()
val request = Request.Builder()
.url(url.toString())
.get()
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body()?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = parser.parse(responseBody).obj["list"]?.array
response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) }
}
}
private fun jsonToSearch(obj: JsonObject): TrackSearch {
return TrackSearch.create(TrackManager.BANGUMI).apply {
media_id = obj["id"].asInt
title = obj["name_cn"].asString
cover_url = obj["images"].obj["common"].asString
summary = obj["name"].asString
tracking_url = obj["url"].asString
}
}
private fun jsonToTrack(mangas: JsonObject): Track {
return Track.create(TrackManager.BANGUMI).apply {
title = mangas["name"].asString
media_id = mangas["id"].asInt
score = if (mangas["rating"] != null)
(if (mangas["rating"].isJsonObject) mangas["rating"].obj["score"].asFloat else 0f)
else 0f
status = Bangumi.DEFAULT_STATUS
tracking_url = mangas["url"].asString
}
}
fun findLibManga(track: Track): Observable<Track?> {
val urlMangas = "$apiUrl/subject/${track.media_id}"
val requestMangas = Request.Builder()
.url(urlMangas)
.get()
.build()
return authClient.newCall(requestMangas)
.asObservableSuccess()
.map { netResponse ->
// get comic info
val responseBody = netResponse.body()?.string().orEmpty()
jsonToTrack(parser.parse(responseBody).obj)
}
}
fun statusLibManga(track: Track): Observable<Track?> {
val urlUserRead = "$apiUrl/collection/${track.media_id}"
val requestUserRead = Request.Builder()
.url(urlUserRead)
.cacheControl(CacheControl.FORCE_NETWORK)
.get()
.build()
// todo get user readed chapter here
return authClient.newCall(requestUserRead)
.asObservableSuccess()
.map { netResponse ->
val resp = netResponse.body()?.string()
val coll = gson.fromJson(resp, Collection::class.java)
track.status = coll.status?.id!!
track.last_chapter_read = coll.ep_status!!
track
}
}
fun accessToken(code: String): Observable<OAuth> {
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
val responseBody = netResponse.body()?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
gson.fromJson(responseBody, OAuth::class.java)
}
}
private fun accessTokenRequest(code: String) = POST(oauthUrl,
body = FormBody.Builder()
.add("grant_type", "authorization_code")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("code", code)
.add("redirect_uri", redirectUrl)
.build()
)
companion object {
private const val clientId = "bgm10555cda0762e80ca"
private const val clientSecret = "8fff394a8627b4c388cbf349ec865775"
private const val baseUrl = "https://bangumi.org"
private const val apiUrl = "https://api.bgm.tv"
private const val oauthUrl = "https://bgm.tv/oauth/access_token"
private const val loginUrl = "https://bgm.tv/oauth/authorize"
private const val redirectUrl = "tachiyomi://bangumi-auth"
private const val baseMangaUrl = "$apiUrl/mangas"
fun mangaUrl(remoteId: Int): String {
return "$baseMangaUrl/$remoteId"
}
fun authUrl() =
Uri.parse(loginUrl).buildUpon()
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "code")
.appendQueryParameter("redirect_uri", redirectUrl)
.build()
fun refreshTokenRequest(token: String) = POST(oauthUrl,
body = FormBody.Builder()
.add("grant_type", "refresh_token")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("refresh_token", token)
.add("redirect_uri", redirectUrl)
.build())
}
}

View File

@ -0,0 +1,61 @@
package eu.kanade.tachiyomi.data.track.bangumi
import com.google.gson.Gson
import okhttp3.FormBody
import okhttp3.Interceptor
import okhttp3.Response
class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
/**
* OAuth object used for authenticated requests.
*/
private var oauth: OAuth? = bangumi.restoreToken()
fun addTocken(tocken: String, oidFormBody: FormBody): FormBody {
val newFormBody = FormBody.Builder()
for (i in 0 until oidFormBody.size()) {
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
}
newFormBody.add("access_token", tocken)
return newFormBody.build()
}
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val currAuth = oauth ?: throw Exception("Not authenticated with Bangumi")
if (currAuth.isExpired()) {
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refresh_token!!))
if (response.isSuccessful) {
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
} else {
response.close()
}
}
var authRequest = if (originalRequest.method() == "GET") originalRequest.newBuilder()
.header("User-Agent", "Tachiyomi")
.url(originalRequest.url().newBuilder()
.addQueryParameter("access_token", currAuth.access_token).build())
.build() else originalRequest.newBuilder()
.post(addTocken(currAuth.access_token, originalRequest.body() as FormBody))
.header("User-Agent", "Tachiyomi")
.build()
return chain.proceed(authRequest)
}
fun newAuth(oauth: OAuth?) {
this.oauth = if (oauth == null) null else OAuth(
oauth.access_token,
oauth.token_type,
System.currentTimeMillis() / 1000,
oauth.expires_in,
oauth.refresh_token,
this.oauth?.user_id)
bangumi.saveToken(oauth)
}
}

View File

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.data.track.bangumi
import eu.kanade.tachiyomi.data.database.models.Track
fun Track.toBangumiStatus() = when (status) {
Bangumi.READING -> "do"
Bangumi.COMPLETED -> "collect"
Bangumi.ON_HOLD -> "on_hold"
Bangumi.DROPPED -> "dropped"
Bangumi.PLANNING -> "wish"
else -> throw NotImplementedError("Unknown status")
}
fun toTrackStatus(status: String) = when (status) {
"do" -> Bangumi.READING
"collect" -> Bangumi.COMPLETED
"on_hold" -> Bangumi.ON_HOLD
"dropped" -> Bangumi.DROPPED
"wish" -> Bangumi.PLANNING
else -> throw Exception("Unknown status")
}

View File

@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.data.track.bangumi
data class Collection(
val `private`: Int? = 0,
val comment: String? = "",
val ep_status: Int? = 0,
val lasttouch: Int? = 0,
val rating: Int? = 0,
val status: Status? = Status(),
val tag: List<String?>? = listOf(),
val user: User? = User(),
val vol_status: Int? = 0
)

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.data.track.bangumi
data class OAuth(
val access_token: String,
val token_type: String,
val created_at: Long,
val expires_in: Long,
val refresh_token: String?,
val user_id: Long?
) {
// Access token refersh before expired
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
}

View File

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

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.data.track.bangumi
data class User(
val avatar: Avatar? = Avatar(),
val id: Int? = 0,
val nickname: String? = "",
val sign: String? = "",
val url: String? = "",
val usergroup: Int? = 0,
val username: String? = ""
)

View File

@ -10,11 +10,11 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import okhttp3.HttpUrl
import 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)

View File

@ -0,0 +1,49 @@
package eu.kanade.tachiyomi.data.track.myanimelist
import okhttp3.Interceptor
import okhttp3.RequestBody
import okhttp3.Response
import okio.Buffer
import org.json.JSONObject
class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
myanimelist.ensureLoggedIn()
var request = chain.request()
request.body()?.let {
val contentType = it.contentType().toString()
val updatedBody = when {
contentType.contains("x-www-form-urlencoded") -> updateFormBody(it)
contentType.contains("json") -> updateJsonBody(it)
else -> it
}
request = request.newBuilder().post(updatedBody).build()
}
return chain.proceed(request)
}
private fun bodyToString(requestBody: RequestBody): String {
Buffer().use {
requestBody.writeTo(it)
return it.readUtf8()
}
}
private fun updateFormBody(requestBody: RequestBody): RequestBody {
val formString = bodyToString(requestBody)
return RequestBody.create(requestBody.contentType(),
"$formString${if (formString.isNotEmpty()) "&" else ""}${MyanimelistApi.CSRF}=${myanimelist.getCSRF()}")
}
private fun updateJsonBody(requestBody: RequestBody): RequestBody {
val jsonString = bodyToString(requestBody)
val newBody = JSONObject(jsonString)
.put(MyanimelistApi.CSRF, myanimelist.getCSRF())
return RequestBody.create(requestBody.contentType(), newBody.toString())
}
}

View File

@ -22,26 +22,20 @@ import java.io.InputStreamReader
import java.util.zip.GZIPInputStream
class MyanimelistApi(private val client: OkHttpClient) {
class MyanimelistApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
fun addLibManga(track: Track, csrf: String): Observable<Track> {
return Observable.defer {
client.newCall(POST(url = getAddUrl(), body = getMangaPostPayload(track, csrf)))
.asObservableSuccess()
.map { track }
}
}
fun updateLibManga(track: Track, csrf: String): Observable<Track> {
return Observable.defer {
client.newCall(POST(url = getUpdateUrl(), body = getMangaPostPayload(track, csrf)))
.asObservableSuccess()
.map { track }
}
}
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun search(query: String): Observable<List<TrackSearch>> {
return client.newCall(GET(getSearchUrl(query)))
return if (query.startsWith(PREFIX_MY)) {
val realQuery = query.removePrefix(PREFIX_MY)
getList()
.flatMap { Observable.from(it) }
.filter { it.title.contains(realQuery, true) }
.toList()
}
else {
client.newCall(GET(searchUrl(query)))
.asObservable()
.flatMap { response ->
Observable.from(Jsoup.parse(response.consumeBody())
@ -67,16 +61,83 @@ class MyanimelistApi(private val client: OkHttpClient) {
}
.toList()
}
}
private fun getList(csrf: String): Observable<List<TrackSearch>> {
return getListUrl(csrf)
fun addLibManga(track: Track): Observable<Track> {
return Observable.defer {
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track)))
.asObservableSuccess()
.map { track }
}
}
fun updateLibManga(track: Track): Observable<Track> {
return Observable.defer {
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track)))
.asObservableSuccess()
.map { track }
}
}
fun findLibManga(track: Track): Observable<Track?> {
return authClient.newCall(GET(url = listEntryUrl(track.media_id)))
.asObservable()
.map {response ->
var libTrack: Track? = null
response.use {
if (it.priorResponse()?.isRedirect != true) {
val trackForm = Jsoup.parse(it.consumeBody())
libTrack = Track.create(TrackManager.MYANIMELIST).apply {
last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
total_chapters = trackForm.select("#totalChap").text().toInt()
status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull() ?: 0f
}
}
}
libTrack
}
}
fun getLibManga(track: Track): Observable<Track> {
return findLibManga(track)
.map { it ?: throw Exception("Could not find manga") }
}
fun login(username: String, password: String): String {
val csrf = getSessionInfo()
login(username, password, csrf)
return csrf
}
private fun getSessionInfo(): String {
val response = client.newCall(GET(loginUrl())).execute()
return Jsoup.parse(response.consumeBody())
.select("meta[name=csrf_token]")
.attr("content")
}
private fun login(username: String, password: String, csrf: String) {
val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute()
response.use {
if (response.priorResponse()?.code() != 302) throw Exception("Authentication error")
}
}
private fun getList(): Observable<List<TrackSearch>> {
return getListUrl()
.flatMap { url ->
getListXml(url)
}
.flatMap { doc ->
Observable.from(doc.select("manga"))
}
.map { it ->
.map {
TrackSearch.create(TrackManager.MYANIMELIST).apply {
title = it.selectText("manga_title")!!
media_id = it.selectInt("manga_mangadb_id")
@ -90,107 +151,8 @@ class MyanimelistApi(private val client: OkHttpClient) {
.toList()
}
private fun getListXml(url: String): Observable<Document> {
return client.newCall(GET(url))
.asObservable()
.map { response ->
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
}
}
fun findLibManga(track: Track, csrf: String): Observable<Track?> {
return getList(csrf)
.map { list -> list.find { it.media_id == track.media_id } }
}
fun getLibManga(track: Track, csrf: String): Observable<Track> {
return findLibManga(track, csrf)
.map { it ?: throw Exception("Could not find manga") }
}
fun login(username: String, password: String): Observable<String> {
return getSessionInfo()
.flatMap { csrf ->
login(username, password, csrf)
}
}
private fun getSessionInfo(): Observable<String> {
return client.newCall(GET(getLoginUrl()))
.asObservable()
.map { response ->
Jsoup.parse(response.consumeBody())
.select("meta[name=csrf_token]")
.attr("content")
}
}
private fun login(username: String, password: String, csrf: String): Observable<String> {
return client.newCall(POST(url = getLoginUrl(), body = getLoginPostBody(username, password, csrf)))
.asObservable()
.map { response ->
response.use {
if (response.priorResponse()?.code() != 302) throw Exception("Authentication error")
}
csrf
}
}
private fun getLoginPostBody(username: String, password: String, csrf: String): RequestBody {
return FormBody.Builder()
.add("user_name", username)
.add("password", password)
.add("cookie", "1")
.add("sublogin", "Login")
.add("submit", "1")
.add(CSRF, csrf)
.build()
}
private fun getExportPostBody(csrf: String): RequestBody {
return FormBody.Builder()
.add("type", "2")
.add("subexport", "Export My List")
.add(CSRF, csrf)
.build()
}
private fun getMangaPostPayload(track: Track, csrf: String): RequestBody {
val body = JSONObject()
.put("manga_id", track.media_id)
.put("status", track.status)
.put("score", track.score)
.put("num_read_chapters", track.last_chapter_read)
.put(CSRF, csrf)
return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), body.toString())
}
private fun getLoginUrl() = Uri.parse(baseUrl).buildUpon()
.appendPath("login.php")
.toString()
private fun getSearchUrl(query: String): String {
val col = "c[]"
return Uri.parse(baseUrl).buildUpon()
.appendPath("manga.php")
.appendQueryParameter("q", query)
.appendQueryParameter(col, "a")
.appendQueryParameter(col, "b")
.appendQueryParameter(col, "c")
.appendQueryParameter(col, "d")
.appendQueryParameter(col, "e")
.appendQueryParameter(col, "g")
.toString()
}
private fun getExportListUrl() = Uri.parse(baseUrl).buildUpon()
.appendPath("panel.php")
.appendQueryParameter("go", "export")
.toString()
private fun getListUrl(csrf: String): Observable<String> {
return client.newCall(POST(url = getExportListUrl(), body = getExportPostBody(csrf)))
private fun getListUrl(): Observable<String> {
return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
.asObservable()
.map {response ->
baseUrl + Jsoup.parse(response.consumeBody())
@ -200,17 +162,17 @@ class MyanimelistApi(private val client: OkHttpClient) {
}
}
private fun getUpdateUrl() = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath("edit.json")
.toString()
private fun getAddUrl() = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath( "add.json")
.toString()
private fun getListXml(url: String): Observable<Document> {
return authClient.newCall(GET(url))
.asObservable()
.map { response ->
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
}
}
private fun Response.consumeBody(): String? {
use {
if (it.code() != 200) throw Exception("Login error")
if (it.code() != 200) throw Exception("HTTP error ${it.code()}")
return it.body()?.string()
}
}
@ -229,37 +191,105 @@ class MyanimelistApi(private val client: OkHttpClient) {
}
companion object {
const val baseUrl = "https://myanimelist.net"
const val CSRF = "csrf_token"
private const val baseUrl = "https://myanimelist.net"
private const val baseMangaUrl = "$baseUrl/manga/"
private const val baseModifyListUrl = "$baseUrl/ownlist/manga"
private const val PREFIX_MY = "my:"
private const val TD = "td"
fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
fun Element.searchTitle() = select("strong").text()!!
private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
.appendPath("login.php")
.toString()
fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
private fun searchUrl(query: String): String {
val col = "c[]"
return Uri.parse(baseUrl).buildUpon()
.appendPath("manga.php")
.appendQueryParameter("q", query)
.appendQueryParameter(col, "a")
.appendQueryParameter(col, "b")
.appendQueryParameter(col, "c")
.appendQueryParameter(col, "d")
.appendQueryParameter(col, "e")
.appendQueryParameter(col, "g")
.toString()
}
fun Element.searchCoverUrl() = select("img")
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
.appendPath("panel.php")
.appendQueryParameter("go", "export")
.toString()
private fun updateUrl() = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath("edit.json")
.toString()
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath( "add.json")
.toString()
private fun listEntryUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath(mediaId.toString())
.appendPath("edit")
.toString()
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
return FormBody.Builder()
.add("user_name", username)
.add("password", password)
.add("cookie", "1")
.add("sublogin", "Login")
.add("submit", "1")
.add(CSRF, csrf)
.build()
}
private fun exportPostBody(): RequestBody {
return FormBody.Builder()
.add("type", "2")
.add("subexport", "Export My List")
.build()
}
private fun mangaPostPayload(track: Track): RequestBody {
val body = JSONObject()
.put("manga_id", track.media_id)
.put("status", track.status)
.put("score", track.score)
.put("num_read_chapters", track.last_chapter_read)
return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), body.toString())
}
private fun Element.searchTitle() = select("strong").text()!!
private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
private fun Element.searchCoverUrl() = select("img")
.attr("data-src")
.split("\\?")[0]
.replace("/r/50x70/", "/")
fun Element.searchMediaId() = select("div.picSurround")
private fun Element.searchMediaId() = select("div.picSurround")
.select("a").attr("id")
.replace("sarea", "")
.toInt()
fun Element.searchSummary() = select("div.pt4")
private fun Element.searchSummary() = select("div.pt4")
.first()
.ownText()!!
fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") PUBLISHING else FINISHED
private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished"
fun Element.searchPublishingType() = select(TD)[2].text()!!
private fun Element.searchPublishingType() = select(TD)[2].text()!!
fun Element.searchStartDate() = select(TD)[6].text()!!
private fun Element.searchStartDate() = select(TD)[6].text()!!
fun getStatus(status: String) = when (status) {
private fun getStatus(status: String) = when (status) {
"Reading" -> 1
"Completed" -> 2
"On-Hold" -> 3
@ -267,10 +297,5 @@ class MyanimelistApi(private val client: OkHttpClient) {
"Plan to Read" -> 6
else -> 1
}
const val CSRF = "csrf_token"
const val TD = "td"
private const val FINISHED = "Finished"
private const val PUBLISHING = "Publishing"
}
}

View File

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

View File

@ -1,7 +1,8 @@
package eu.kanade.tachiyomi.data.track.shikomori
package eu.kanade.tachiyomi.data.track.shikimori
import android.content.Context
import android.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)

View File

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

View File

@ -1,26 +1,26 @@
package eu.kanade.tachiyomi.data.track.shikomori
package eu.kanade.tachiyomi.data.track.shikimori
import com.google.gson.Gson
import 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)
}
}

View File

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.data.track.shikimori
import eu.kanade.tachiyomi.data.database.models.Track
fun Track.toShikimoriStatus() = when (status) {
Shikimori.READING -> "watching"
Shikimori.COMPLETED -> "completed"
Shikimori.ON_HOLD -> "on_hold"
Shikimori.DROPPED -> "dropped"
Shikimori.PLANNING -> "planned"
Shikimori.REPEATING -> "rewatching"
else -> throw NotImplementedError("Unknown status")
}
fun toTrackStatus(status: String) = when (status) {
"watching" -> Shikimori.READING
"completed" -> Shikimori.COMPLETED
"on_hold" -> Shikimori.ON_HOLD
"dropped" -> Shikimori.DROPPED
"planned" -> Shikimori.PLANNING
"rewatching" -> Shikimori.REPEATING
else -> throw Exception("Unknown status")
}

View File

@ -1,24 +0,0 @@
package eu.kanade.tachiyomi.data.track.shikomori
import eu.kanade.tachiyomi.data.database.models.Track
fun Track.toShikomoriStatus() = when (status) {
Shikomori.READING -> "watching"
Shikomori.COMPLETED -> "completed"
Shikomori.ON_HOLD -> "on_hold"
Shikomori.DROPPED -> "dropped"
Shikomori.PLANNING -> "planned"
Shikomori.REPEATING -> "rewatching"
else -> throw NotImplementedError("Unknown status")
}
fun toTrackStatus(status: String) = when (status) {
"watching" -> Shikomori.READING
"completed" -> Shikomori.COMPLETED
"on_hold" -> Shikomori.ON_HOLD
"dropped" -> Shikomori.DROPPED
"planned" -> Shikomori.PLANNING
"rewatching" -> Shikomori.REPEATING
else -> throw Exception("Unknown status")
}

View File

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

View File

@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.data.updater
interface Release {
val info: String
/**
* Get download link of latest release.
* @return download link of latest release.
*/
val downloadLink: String
}

View File

@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.data.updater
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.updater.devrepo.DevRepoUpdateChecker
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
import rx.Observable
abstract class UpdateChecker {
companion object {
fun getUpdateChecker(): UpdateChecker {
return if (BuildConfig.DEBUG) {
DevRepoUpdateChecker()
} else {
GithubUpdateChecker()
}
}
}
/**
* Returns observable containing release information
*/
abstract fun checkForUpdate(): Observable<UpdateResult>
}

View File

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.data.updater
abstract class UpdateResult {
open class NewUpdate<T : Release>(val release: T): UpdateResult()
open class NoNewUpdate: UpdateResult()
}

View File

@ -13,10 +13,10 @@ import eu.kanade.tachiyomi.util.notificationManager
class UpdaterJob : Job() {
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()

View File

@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.data.updater.devrepo
import eu.kanade.tachiyomi.data.updater.Release
class DevRepoRelease(override val info: String) : Release {
override val downloadLink: String
get() = LATEST_URL
companion object {
const val LATEST_URL = "https://tachiyomi.kanade.eu/latest"
}
}

View File

@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.data.updater.devrepo
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.updater.UpdateChecker
import eu.kanade.tachiyomi.data.updater.UpdateResult
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservable
import okhttp3.OkHttpClient
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class DevRepoUpdateChecker : UpdateChecker() {
private val client: OkHttpClient by lazy {
Injekt.get<NetworkHelper>().client.newBuilder()
.followRedirects(false)
.build()
}
private val versionRegex: Regex by lazy {
Regex("tachiyomi-r(\\d+).apk")
}
override fun checkForUpdate(): Observable<UpdateResult> {
return client.newCall(GET(DevRepoRelease.LATEST_URL)).asObservable()
.map { response ->
// Get latest repo version number from header in format "Location: tachiyomi-r1512.apk"
val latestVersionNumber: String = versionRegex.find(response.header("Location")!!)!!.groupValues[1]
if (latestVersionNumber.toInt() > BuildConfig.COMMIT_COUNT.toInt()) {
DevRepoUpdateResult.NewUpdate(DevRepoRelease("v$latestVersionNumber"))
} else {
DevRepoUpdateResult.NoNewUpdate()
}
}
}
}

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.data.updater.devrepo
import eu.kanade.tachiyomi.data.updater.UpdateResult
sealed class DevRepoUpdateResult : UpdateResult() {
class NewUpdate(release: DevRepoRelease): UpdateResult.NewUpdate<DevRepoRelease>(release)
class NoNewUpdate: UpdateResult.NoNewUpdate()
}

View File

@ -1,24 +1,25 @@
package eu.kanade.tachiyomi.data.updater
package eu.kanade.tachiyomi.data.updater.github
import com.google.gson.annotations.SerializedName
import 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
/**

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.updater
package eu.kanade.tachiyomi.data.updater.github
import eu.kanade.tachiyomi.network.NetworkHelper
import retrofit2.Retrofit

View File

@ -1,16 +1,15 @@
package eu.kanade.tachiyomi.data.updater
package eu.kanade.tachiyomi.data.updater.github
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.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 {
}
}
}
}

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.data.updater.github
import eu.kanade.tachiyomi.data.updater.UpdateResult
sealed class GithubUpdateResult : UpdateResult() {
class NewUpdate(release: GithubRelease): UpdateResult.NewUpdate<GithubRelease>(release)
class NoNewUpdate : UpdateResult.NoNewUpdate()
}

View File

@ -47,11 +47,12 @@ class AndroidCookieJar(context: Context) : CookieJar {
}
fun remove(url: HttpUrl) {
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()

View File

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

View File

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

View File

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

View File

@ -21,7 +21,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.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

View File

@ -95,6 +95,8 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
// Set onclickListener to toggle favorite when FAB clicked.
fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
// Set onLongClickListener to manage categories when FAB is clicked.
fab_favorite.longClicks().subscribeUntilDestroy{ onFabLongClick() }
// Set SwipeRefresh to refresh manga data.
@ -439,26 +441,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory)
categories.size <= 1 -> // default or the one from the user
presenter.moveMangaToCategory(manga, categories.firstOrNull())
else -> askCategories(manga, categories)
}
activity?.toast(activity?.getString(R.string.manga_added_library))
} else {
activity?.toast(activity?.getString(R.string.manga_removed_library))
}
}
private fun onFabLongClick() {
if(preferences.eh_askCategoryOnLongPress().getOrDefault()) {
val manga = presenter.manga
if(!manga.favorite) toggleFavorite()
val categories = presenter.getCategories()
if(categories.size > 1) {
askCategories(manga, categories)
}
}
}
private fun askCategories(manga: Manga, categories: List<Category>) {
else -> {
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
@ -467,6 +450,36 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
.showDialog(router)
}
}
activity?.toast(activity?.getString(R.string.manga_added_library))
} else {
activity?.toast(activity?.getString(R.string.manga_removed_library))
}
}
/**
* Called when the fab is long clicked.
*/
private fun onFabLongClick() {
val manga = presenter.manga
if (!manga.favorite) {
toggleFavorite()
activity?.toast(activity?.getString(R.string.manga_added_library))
}
val categories = presenter.getCategories()
if (categories.size <= 1) {
// default or the one from the user then just add to favorite.
presenter.moveMangaToCategory(manga, categories.firstOrNull())
} else {
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
.showDialog(router)
}
}
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
val manga = mangas.firstOrNull() ?: return

View File

@ -52,6 +52,7 @@ class TrackSearchAdapter(context: Context)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(view.track_search_cover)
}
if (track.publishing_status.isNullOrBlank()) {
view.track_search_status.gone()
@ -76,4 +77,3 @@ class TrackSearchAdapter(context: Context)
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.ui.reader
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
class ReaderColorFilterView(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
private val colorFilterPaint: Paint = Paint()
fun setFilterColor(color: Int, filterMode: Int) {
colorFilterPaint.setColor(color)
colorFilterPaint.xfermode = PorterDuffXfermode(when (filterMode) {
1 -> PorterDuff.Mode.MULTIPLY
2 -> PorterDuff.Mode.SCREEN
3 -> PorterDuff.Mode.OVERLAY
4 -> PorterDuff.Mode.LIGHTEN
5 -> PorterDuff.Mode.DARKEN
else -> PorterDuff.Mode.SRC_OVER
})
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawPaint(colorFilterPaint)
}
}

View File

@ -151,10 +151,9 @@ class ReaderPresenter(
/**
* Called when the user pressed the back button and is going to leave the reader. Used to
* 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 ->

View File

@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.ui.setting
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.Gravity.CENTER
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import android.widget.ProgressBar
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.ui.main.MainActivity
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class BangumiLoginActivity : AppCompatActivity() {
private val trackManager: TrackManager by injectLazy()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
val view = ProgressBar(this)
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
val code = intent.data?.getQueryParameter("code")
if (code != null) {
trackManager.bangumi.login(code)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
returnToSettings()
}, {
returnToSettings()
})
} else {
trackManager.bangumi.logout()
returnToSettings()
}
}
private fun returnToSettings() {
finish()
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
}
}

View File

@ -9,8 +9,8 @@ import android.view.View
import com.afollestad.materialdialogs.MaterialDialog
import 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)
}
}

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="112dp"
android:height="112dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -395,7 +395,6 @@ También asegúrese de haber iniciado sesión en las fuentes que lo requieren an
<string name="local_source_badge">Local</string>
<string name="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>

View File

@ -396,7 +396,6 @@ Assurez-vous que vous êtes connecté à des sources qui le demande avant de com
<string name="action_login">Connexion</string>
<string name="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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -365,7 +365,6 @@ Zorg ook dat je ingelogd bent voor bronnen die dit vereisen alvorens je het teru
<string name="local_source_badge">Lokaal</string>
<string name="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>

View File

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

View File

@ -362,7 +362,6 @@ Além disso, verifique se as fontes que requerem uma conta foram configuradas co
<string name="action_login">Entrar</string>
<string name="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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="color_filter_modes">
<item>@string/filter_mode_default</item>
<item>@string/filter_mode_multiply</item>
<item>@string/filter_mode_screen</item>
<!-- Attributes specific for SDK 28 and up -->
<item>@string/filter_mode_overlay</item>
<item>@string/filter_mode_lighten</item>
<item>@string/filter_mode_darken</item>
</string-array>
</resources>

View File

@ -461,7 +461,6 @@
<string name="invalid_combination">Mặc định không thể chọn với các danh mục khác</string>
<string name="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>

View File

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

View File

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

View File

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