Compare commits

..

10 Commits

Author SHA1 Message Date
9d5cf9163a Release v0.7.4 2018-05-13 11:56:24 +02:00
9abce0cca3 Vanity url (#1408)
* vanity url

* vanity url

* vanity url
2018-05-13 11:36:08 +02:00
c6245f4fa3 Reenable cipher suites after upgrading to okhttp 3.10. Fixes #1411 2018-05-11 15:08:12 +02:00
75fc160204 Update okhttp version 2018-05-05 15:44:17 +02:00
263198dd89 Minor fix 2018-05-05 15:29:08 +02:00
345f96055d Fix indonesian language. Closes #1387 2018-05-05 14:23:34 +02:00
51144aa45e Implement Anilist API v2 (closes #1159) (#1383)
* Implement Anilist API v2 (closes #1159)

Switches to using the Anilist v2 API.
Login is now done by implicit grant and tokens are good for one year.
Users will need to login again after token expiration.
"clientId" on line 289 of AnilistApi.kt should be changed to Tachiyomi's
own client ID number.

* Code style formatting

Revert to kotlin 1.2.30
Use correct client ID
Rename AnilistApi.login to AnilistApi.createOAuth to reflect changed implementation
Rename json mimetype variable from json to jsonMime for clarity
Don't read response if it's ignored
Remove unused parameters from api requests

* Close netResponse after read

* Refactor remote_id into media_id and library_id

* DB: Refactor RemoteId

Refactor RemoteId into library_id and media_id
Implement function to fetch library_id if user is migrating rom APIv1

* Remove logging interceptor

* Compatability and sql simplification

* Fix score and minor improvements

* Revert changes to Kitsu API
2018-05-05 14:05:02 +02:00
86a599d13f Added Github link to about. (#1389)
* Added Github link to about.

* Added github link to About page (Fixed)

Fixed based on jogerj's comment in #1389

* Changed Github link to correct URL.

* Balanced brackets
2018-05-04 16:36:06 +02:00
0cf81e6f7a Update README.md (#1341)
* Update README.md

thought it would be cool to have hyperlinks to the sites

* Update README.md
2018-05-04 16:35:34 +02:00
8874fe973c Bugfixes 2018-04-30 18:31:31 +02:00
30 changed files with 474 additions and 303 deletions

View File

@ -1,5 +1,5 @@
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/WrBkRk4)
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
3. What is your type of issue?
* [Catalogue request](#catalogue-requests)
* [Bugs](#bugs)

View File

@ -1,6 +1,6 @@
| Build | Stable | Dev | Contribute | Contact |
|-------|----------|---------|------------|---------|
| [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=download%20(autoupdate%20included))](https://github.com/inorichi/tachiyomi/releases) | [![latest dev build](https://img.shields.io/badge/download-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest) [![fdroid dev](https://img.shields.io/badge/autoupdate-wiki-blue.svg)](//github.com/inorichi/tachiyomi/wiki/F-Droid-for-dev-versions) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/2dDQBv2) |
| [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=download%20(autoupdate%20included))](https://github.com/inorichi/tachiyomi/releases) | [![latest dev build](https://img.shields.io/badge/download-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest) [![fdroid dev](https://img.shields.io/badge/autoupdate-wiki-blue.svg)](//github.com/inorichi/tachiyomi/wiki/F-Droid-for-dev-versions) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) |
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
@ -14,11 +14,11 @@ Features include:
* Online reading from sources such as KissManga, MangaFox, [and more](https://github.com/inorichi/tachiyomi-extensions)
* Local reading of downloaded manga
* Configurable reader with multiple viewers, reading directions and other settings
* MyAnimeList, AniList, and Kitsu support
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), and [Kitsu](https://kitsu.io/explore/anime) support
* Categories to organize your library
* Light and dark themes
* Schedule updating your library for new chapters
* Create backups locally or to your cloud service of choice
* Create backups locally to read offline or to your desired cloud service
## Download
Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases).
@ -32,7 +32,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
<details><summary>Issues</summary>
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/WrBkRk4)
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
</details>

View File

@ -38,8 +38,8 @@ android {
minSdkVersion 16
targetSdkVersion 27
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 36
versionName "0.7.3"
versionCode 37
versionName "0.7.4"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -120,7 +120,7 @@ dependencies {
implementation 'com.android.support:multidex:1.0.2'
standardImplementation 'com.google.firebase:firebase-core:12.0.1'
standardImplementation 'com.google.firebase:firebase-core:11.8.0'
// ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1'
@ -130,7 +130,7 @@ dependencies {
implementation 'com.github.pwittchen:reactivenetwork:0.7.0'
// Network client
implementation "com.squareup.okhttp3:okhttp:3.9.1"
implementation "com.squareup.okhttp3:okhttp:3.10.0"
implementation 'com.squareup.okio:okio:1.14.0'
// REST
@ -154,8 +154,8 @@ dependencies {
implementation 'org.jsoup:jsoup:1.10.2'
// Job scheduling
implementation 'com.evernote:android-job:1.2.4'
implementation 'com.google.android.gms:play-services-gcm:12.0.1'
implementation 'com.evernote:android-job:1.2.5'
implementation 'com.google.android.gms:play-services-gcm:11.8.0'
// Changelog
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'

View File

@ -402,8 +402,11 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
for (dbTrack in dbTracks) {
if (track.sync_id == dbTrack.sync_id) {
// The sync is already in the db, only update its fields
if (track.remote_id != dbTrack.remote_id) {
dbTrack.remote_id = track.remote_id
if (track.media_id != dbTrack.media_id) {
dbTrack.media_id = track.media_id
}
if (track.library_id != dbTrack.library_id) {
dbTrack.library_id = track.library_id
}
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.backup.serializer
import android.telecom.DisconnectCause.REMOTE
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonToken
@ -11,7 +12,8 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl
object TrackTypeAdapter {
private const val SYNC = "s"
private const val REMOTE = "r"
private const val MEDIA = "r"
private const val LIBRARY = "ml"
private const val TITLE = "t"
private const val LAST_READ = "l"
private const val TRACKING_URL = "u"
@ -24,8 +26,10 @@ object TrackTypeAdapter {
value(it.title)
name(SYNC)
value(it.sync_id)
name(REMOTE)
value(it.remote_id)
name(MEDIA)
value(it.media_id)
name(LIBRARY)
value(it.library_id)
name(LAST_READ)
value(it.last_chapter_read)
name(TRACKING_URL)
@ -43,7 +47,8 @@ object TrackTypeAdapter {
when (name) {
TITLE -> track.title = nextString()
SYNC -> track.sync_id = nextInt()
REMOTE -> track.remote_id = nextInt()
MEDIA -> track.media_id = nextInt()
LIBRARY -> track.library_id = nextLong()
LAST_READ -> track.last_chapter_read = nextInt()
TRACKING_URL -> track.tracking_url = nextString()
}

View File

@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
/**
* Version of the database.
*/
const val DATABASE_VERSION = 6
const val DATABASE_VERSION = 7
}
override fun onCreate(db: SQLiteDatabase) = with(db) {
@ -57,6 +57,9 @@ class DbOpenHelper(context: Context)
if (oldVersion < 6) {
db.execSQL(TrackTable.addTrackingUrl)
}
if (oldVersion < 7) {
db.execSQL(TrackTable.addLibraryId)
}
}
override fun onConfigure(db: SQLiteDatabase) {

View File

@ -13,8 +13,9 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LIBRARY_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_REMOTE_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MEDIA_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
@ -45,7 +46,8 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_SYNC_ID, obj.sync_id)
put(COL_REMOTE_ID, obj.remote_id)
put(COL_MEDIA_ID, obj.media_id)
put(COL_LIBRARY_ID, obj.library_id)
put(COL_TITLE, obj.title)
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
@ -62,7 +64,8 @@ class TrackGetResolver : DefaultGetResolver<Track>() {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
remote_id = cursor.getInt(cursor.getColumnIndex(COL_REMOTE_ID))
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))

View File

@ -10,7 +10,9 @@ interface Track : Serializable {
var sync_id: Int
var remote_id: Int
var media_id: Int
var library_id: Long?
var title: String

View File

@ -8,7 +8,9 @@ class TrackImpl : Track {
override var sync_id: Int = 0
override var remote_id: Int = 0
override var media_id: Int = 0
override var library_id: Long? = null
override lateinit var title: String
@ -30,13 +32,13 @@ class TrackImpl : Track {
if (manga_id != other.manga_id) return false
if (sync_id != other.sync_id) return false
return remote_id == other.remote_id
return media_id == other.media_id
}
override fun hashCode(): Int {
var result = (manga_id xor manga_id.ushr(32)).toInt()
result = 31 * result + sync_id
result = 31 * result + remote_id
result = 31 * result + media_id
return result
}

View File

@ -10,7 +10,9 @@ object TrackTable {
const val COL_SYNC_ID = "sync_id"
const val COL_REMOTE_ID = "remote_id"
const val COL_MEDIA_ID = "remote_id"
const val COL_LIBRARY_ID = "library_id"
const val COL_TITLE = "title"
@ -29,7 +31,8 @@ object TrackTable {
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_SYNC_ID INTEGER NOT NULL,
$COL_REMOTE_ID INTEGER NOT NULL,
$COL_MEDIA_ID INTEGER NOT NULL,
$COL_LIBRARY_ID INTEGER,
$COL_TITLE TEXT NOT NULL,
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
@ -43,4 +46,7 @@ object TrackTable {
val addTrackingUrl: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_TRACKING_URL TEXT DEFAULT ''"
val addLibraryId: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL"
}

View File

@ -119,7 +119,7 @@ class PreferencesHelper(val context: Context) {
fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "")
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
fun anilistScoreType() = rxPrefs.getString("anilist_score_type", "POINT_10")
fun backupsDirectory() = rxPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.anilist
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.preference.getOrDefault
@ -9,6 +10,7 @@ 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 Anilist(private val context: Context, id: Int) : TrackService(id) {
@ -17,24 +19,45 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
const val COMPLETED = 2
const val ON_HOLD = 3
const val DROPPED = 4
const val PLAN_TO_READ = 5
const val PLANNING = 5
const val REPEATING = 6
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
const val POINT_100 = "POINT_100"
const val POINT_10 = "POINT_10"
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
const val POINT_5 = "POINT_5"
const val POINT_3 = "POINT_3"
}
override val name = "AniList"
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
private val gson: Gson by injectLazy()
private val interceptor by lazy { AnilistInterceptor(this, getPassword()) }
private val api by lazy { AnilistApi(client, interceptor) }
private val scorePreference = preferences.anilistScoreType()
init {
// If the preference is an int from APIv1, logout user to force using APIv2
try {
scorePreference.get()
} catch (e: ClassCastException) {
logout()
scorePreference.delete()
}
}
override fun getLogo() = R.drawable.al
override fun getLogoColor() = Color.rgb(18, 25, 35)
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING)
}
override fun getStatus(status: Int): String = with(context) {
@ -43,48 +66,50 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLAN_TO_READ -> getString(R.string.plan_to_read)
PLANNING -> getString(R.string.plan_to_read)
REPEATING -> getString(R.string.repeating)
else -> ""
}
}
override fun getScoreList(): List<String> {
return when (preferences.anilistScoreType().getOrDefault()) {
return when (scorePreference.getOrDefault()) {
// 10 point
0 -> IntRange(0, 10).map(Int::toString)
POINT_10 -> IntRange(0, 10).map(Int::toString)
// 100 point
1 -> IntRange(0, 100).map(Int::toString)
POINT_100 -> IntRange(0, 100).map(Int::toString)
// 5 stars
2 -> IntRange(0, 5).map { "$it" }
POINT_5 -> IntRange(0, 5).map { "$it" }
// Smiley
3 -> listOf("-", "😦", "😐", "😊")
POINT_3 -> listOf("-", "😦", "😐", "😊")
// 10 point decimal
4 -> IntRange(0, 100).map { (it / 10f).toString() }
POINT_10_DECIMAL -> IntRange(0, 100).map { (it / 10f).toString() }
else -> throw Exception("Unknown score type")
}
}
override fun indexToScore(index: Int): Float {
return when (preferences.anilistScoreType().getOrDefault()) {
return when (scorePreference.getOrDefault()) {
// 10 point
0 -> index * 10f
POINT_10 -> index * 10f
// 100 point
1 -> index.toFloat()
POINT_100 -> index.toFloat()
// 5 stars
2 -> index * 20f
POINT_5 -> index * 20f
// Smiley
3 -> index * 30f
POINT_3 -> index * 30f
// 10 point decimal
4 -> index.toFloat()
POINT_10_DECIMAL -> index.toFloat()
else -> throw Exception("Unknown score type")
}
}
override fun displayScore(track: Track): String {
val score = track.score
return when (preferences.anilistScoreType().getOrDefault()) {
2 -> "${(score / 20).toInt()}"
3 -> when {
return when (scorePreference.getOrDefault()) {
POINT_5 -> "${(score / 20).toInt()}"
POINT_3 -> when {
score == 0f -> "0"
score <= 30 -> "😦"
score <= 60 -> "😐"
@ -102,15 +127,26 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED
}
// If user was using API v1 fetch library_id
if (track.library_id == null || track.library_id!! == 0L){
return api.findLibManga(track, getUsername().toInt()).flatMap {
if (it == null) {
throw Exception("$track not found on user library")
}
track.library_id = it.library_id
api.updateLibManga(track)
}
}
return api.updateLibManga(track)
}
override fun bind(track: Track): Observable<Track> {
return api.findLibManga(track, getUsername())
return api.findLibManga(track, getUsername().toInt())
.flatMap { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
update(track)
} else {
// Set default fields if it's not found in the list
@ -126,7 +162,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
}
override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track, getUsername())
return api.getLibManga(track, getUsername().toInt())
.map { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
@ -136,26 +172,34 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
override fun login(username: String, password: String) = login(password)
fun login(authCode: String): Completable {
return api.login(authCode)
// Save the token in the interceptor.
.doOnNext { interceptor.setAuth(it) }
// Obtain the authenticated user from the API.
.zipWith(api.getCurrentUser().map { pair ->
preferences.anilistScoreType().set(pair.second)
pair.first
}, { oauth, user -> Pair(user, oauth.refresh_token!!) })
// Save service credentials (username and refresh token).
.doOnNext { saveCredentials(it.first, it.second) }
// Logout on any error.
.doOnError { logout() }
.toCompletable()
fun login(token: String): Completable {
val oauth = api.createOAuth(token)
interceptor.setAuth(oauth)
return api.getCurrentUser().map { (username, scoreType) ->
scorePreference.set(scoreType)
saveCredentials(username.toString(), oauth.access_token)
}.doOnError{
logout()
}.toCompletable()
}
override fun logout() {
super.logout()
preferences.trackToken(this).set(null)
interceptor.setAuth(null)
}
fun saveOAuth(oAuth: OAuth?) {
preferences.trackToken(this).set(gson.toJson(oAuth))
}
fun loadOAuth(): OAuth? {
return try {
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
} catch (e: Exception) {
null
}
}
}

View File

@ -1,167 +1,275 @@
package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.string
import com.github.salomonbrys.kotson.*
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST
import okhttp3.FormBody
import eu.kanade.tachiyomi.network.asObservableSuccess
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.*
import okhttp3.Request
import okhttp3.RequestBody
import rx.Observable
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val rest = restBuilder()
.client(client.newBuilder().addInterceptor(interceptor).build())
.build()
.create(Rest::class.java)
private val parser = JsonParser()
private val jsonMime = MediaType.parse("application/json; charset=utf-8")
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track): Observable<Track> {
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
.map { response ->
response.body()?.close()
if (!response.isSuccessful) {
throw Exception("Could not add manga")
val query = """
mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status)
{ id status } }
"""
val variables = jsonObject(
"mangaId" to track.media_id,
"progress" to track.last_chapter_read,
"status" to track.toAnilistStatus()
)
val payload = jsonObject(
"query" to query,
"variables" to variables
)
val body = RequestBody.create(jsonMime, payload.toString())
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body()?.string().orEmpty()
netResponse.close()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = parser.parse(responseBody).obj
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
track
}
}
fun updateLibManga(track: Track): Observable<Track> {
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
track.toAnilistScore())
.map { response ->
response.body()?.close()
if (!response.isSuccessful) {
throw Exception("Could not update manga")
val query = """
mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
id
status
progress
}
}
"""
val variables = jsonObject(
"listId" to track.library_id,
"progress" to track.last_chapter_read,
"status" to track.toAnilistStatus(),
"score" to track.score.toInt()
)
val payload = jsonObject(
"query" to query,
"variables" to variables
)
val body = RequestBody.create(jsonMime, payload.toString())
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map {
track
}
}
fun search(query: String): Observable<List<TrackSearch>> {
return rest.search(query, 1)
.map { list ->
list.filter { it.type != "Novel" }.map { it.toTrack() }
fun search(search: String): Observable<List<TrackSearch>> {
val query = """
query Search(${'$'}query: String) {
Page (perPage: 25) {
media(search: ${'$'}query, type: MANGA, format: MANGA) {
id
title {
romaji
}
coverImage {
large
}
type
status
chapters
startDate {
year
month
day
}
}
}
}
.onErrorReturn { emptyList() }
}
fun getList(username: String): Observable<List<Track>> {
return rest.getLib(username)
.map { lib ->
lib.flatten().map { it.toTrack() }
"""
val variables = jsonObject(
"query" to search
)
val payload = jsonObject(
"query" to query,
"variables" to variables
)
val body = RequestBody.create(jsonMime, payload.toString())
val request = Request.Builder()
.url(apiUrl)
.post(body)
.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
val data = response["data"]!!.obj
val page = data["Page"].obj
val media = page["media"].array
val entries = media.map { jsonToALManga(it.obj) }
entries.map { it.toTrack() }
}
}
fun findLibManga(track: Track, username: String) : Observable<Track?> {
// TODO avoid getting the entire list
return getList(username)
.map { list -> list.find { it.remote_id == track.remote_id } }
fun findLibManga(track: Track, userid: Int) : Observable<Track?> {
val query = """
query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
Page {
mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
id
status
scoreRaw: score(format: POINT_100)
progress
media{
id
title {
romaji
}
coverImage {
large
}
type
status
chapters
startDate {
year
month
day
}
}
}
}
}
"""
val variables = jsonObject(
"id" to userid,
"manga_id" to track.media_id
)
val payload = jsonObject(
"query" to query,
"variables" to variables
)
val body = RequestBody.create(jsonMime, payload.toString())
val request = Request.Builder()
.url(apiUrl)
.post(body)
.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
val data = response["data"]!!.obj
val page = data["Page"].obj
val media = page["mediaList"].array
val entries = media.map { jsonToALUserManga(it.obj) }
entries.firstOrNull()?.toTrack()
}
}
fun getLibManga(track: Track, username: String): Observable<Track> {
return findLibManga(track, username)
fun getLibManga(track: Track, userid: Int): Observable<Track> {
return findLibManga(track, userid)
.map { it ?: throw Exception("Could not find manga") }
}
fun login(authCode: String): Observable<OAuth> {
return restBuilder()
.client(client)
fun createOAuth(token: String): OAuth {
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
}
fun getCurrentUser(): Observable<Pair<Int, String>> {
val query = """
query User
{
Viewer {
id
mediaListOptions {
scoreFormat
}
}
}
"""
val payload = jsonObject(
"query" to query
)
val body = RequestBody.create(jsonMime, payload.toString())
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
.create(Rest::class.java)
.requestAccessToken(authCode)
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
val data = response["data"]!!.obj
val viewer = data["Viewer"].obj
Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
}
}
fun getCurrentUser(): Observable<Pair<String, Int>> {
return rest.getCurrentUser()
.map { it["id"].string to it["score_type"].int }
fun jsonToALManga(struct: JsonObject): ALManga{
return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
null, struct["type"].asString, struct["status"].asString,
struct["startDate"]["year"].nullString.orEmpty() + struct["startDate"]["month"].nullString.orEmpty()
+ struct["startDate"]["day"].nullString.orEmpty(), struct["chapters"].nullInt ?: 0)
}
private fun restBuilder() = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
private interface Rest {
@FormUrlEncoded
@POST("auth/access_token")
fun requestAccessToken(
@Field("code") code: String,
@Field("grant_type") grant_type: String = "authorization_code",
@Field("client_id") client_id: String = clientId,
@Field("client_secret") client_secret: String = clientSecret,
@Field("redirect_uri") redirect_uri: String = clientUrl
) : Observable<OAuth>
@GET("user")
fun getCurrentUser(): Observable<JsonObject>
@GET("manga/search/{query}")
fun search(
@Path("query") query: String,
@Query("page") page: Int
): Observable<List<ALManga>>
@GET("user/{username}/mangalist")
fun getLib(
@Path("username") username: String
): Observable<ALUserLists>
@FormUrlEncoded
@PUT("mangalist")
fun addLibManga(
@Field("id") id: Int,
@Field("chapters_read") chapters_read: Int,
@Field("list_status") list_status: String
) : Observable<Response<ResponseBody>>
@FormUrlEncoded
@PUT("mangalist")
fun updateLibManga(
@Field("id") id: Int,
@Field("chapters_read") chapters_read: Int,
@Field("list_status") list_status: String,
@Field("score") score_raw: String
) : Observable<Response<ResponseBody>>
fun jsonToALUserManga(struct: JsonObject): ALUserManga{
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj) )
}
companion object {
private const val clientId = "tachiyomi-hrtje"
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
private const val clientId = "385"
private const val clientUrl = "tachiyomi://anilist-auth"
private const val baseUrl = "https://anilist.co/api/"
private const val apiUrl = "https://graphql.anilist.co/"
private const val baseUrl = "https://anilist.co/api/v2/"
private const val baseMangaUrl = "https://anilist.co/manga/"
fun mangaUrl(remoteId: Int): String {
return baseMangaUrl + remoteId
fun mangaUrl(mediaId: Int): String {
return baseMangaUrl + mediaId
}
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
.appendQueryParameter("grant_type", "authorization_code")
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", clientUrl)
.appendQueryParameter("response_type", "code")
.appendQueryParameter("response_type", "token")
.build()
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
body = FormBody.Builder()
.add("grant_type", "refresh_token")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("refresh_token", token)
.build())
}
}
}

View File

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.data.track.anilist
import com.google.gson.Gson
import okhttp3.Interceptor
import okhttp3.Response
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
/**
* OAuth object used for authenticated requests.
@ -20,24 +20,21 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
if (refreshToken.isNullOrEmpty()) {
if (token.isNullOrEmpty()) {
throw Exception("Not authenticated with Anilist")
}
if (oauth == null){
oauth = anilist.loadOAuth()
}
// Refresh access token if null or expired.
if (oauth == null || oauth!!.isExpired()) {
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
oauth = if (response.isSuccessful) {
Gson().fromJson(response.body()!!.string(), OAuth::class.java)
} else {
response.close()
null
}
if (oauth!!.isExpired()) {
anilist.logout()
throw Exception("Token expired")
}
// Throw on null auth.
if (oauth == null) {
throw Exception("Access token wasn't refreshed")
throw Exception("No authentication token")
}
// Add the authorization header to the original request.
@ -53,8 +50,9 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
* and the oauth object.
*/
fun setAuth(oauth: OAuth?) {
refreshToken = oauth?.refresh_token
token = oauth?.access_token
this.oauth = oauth
anilist.saveOAuth(oauth)
}
}

View File

@ -11,7 +11,7 @@ import java.text.SimpleDateFormat
import java.util.*
data class ALManga(
val id: Int,
val media_id: Int,
val title_romaji: String,
val image_url_lge: String,
val description: String?,
@ -21,12 +21,12 @@ data class ALManga(
val total_chapters: Int) {
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
remote_id = this@ALManga.id
media_id = this@ALManga.media_id
title = title_romaji
total_chapters = this@ALManga.total_chapters
cover_url = image_url_lge
summary = description ?: ""
tracking_url = AnilistApi.mangaUrl(remote_id)
tracking_url = AnilistApi.mangaUrl(media_id)
publishing_status = this@ALManga.publishing_status
publishing_type = type
if (!start_date_fuzzy.isNullOrBlank()) {
@ -43,40 +43,37 @@ data class ALManga(
}
data class ALUserManga(
val id: Int,
val library_id: Long,
val list_status: String,
val score_raw: Int,
val chapters_read: Int,
val manga: ALManga) {
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
remote_id = manga.id
media_id = manga.media_id
status = toTrackStatus()
score = score_raw.toFloat()
last_chapter_read = chapters_read
library_id = this@ALUserManga.library_id
}
fun toTrackStatus() = when (list_status) {
"reading" -> Anilist.READING
"completed" -> Anilist.COMPLETED
"on-hold" -> Anilist.ON_HOLD
"dropped" -> Anilist.DROPPED
"plan to read" -> Anilist.PLAN_TO_READ
"CURRENT" -> Anilist.READING
"COMPLETED" -> Anilist.COMPLETED
"PAUSED" -> Anilist.ON_HOLD
"DROPPED" -> Anilist.DROPPED
"PLANNING" -> Anilist.PLANNING
else -> throw NotImplementedError("Unknown status")
}
}
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
fun flatten() = lists.values.flatten()
}
fun Track.toAnilistStatus() = when (status) {
Anilist.READING -> "reading"
Anilist.COMPLETED -> "completed"
Anilist.ON_HOLD -> "on-hold"
Anilist.DROPPED -> "dropped"
Anilist.PLAN_TO_READ -> "plan to read"
Anilist.READING -> "CURRENT"
Anilist.COMPLETED -> "COMPLETED"
Anilist.ON_HOLD -> "PAUSED"
Anilist.DROPPED -> "DROPPED"
Anilist.PLANNING -> "PLANNING"
Anilist.REPEATING -> "REPEATING"
else -> throw NotImplementedError("Unknown status")
}
@ -84,11 +81,11 @@ private val preferences: PreferencesHelper by injectLazy()
fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) {
// 10 point
0 -> (score.toInt() / 10).toString()
"POINT_10" -> (score.toInt() / 10).toString()
// 100 point
1 -> score.toInt().toString()
"POINT_100" -> score.toInt().toString()
// 5 stars
2 -> when {
"POINT_5" -> when {
score == 0f -> "0"
score < 30 -> "1"
score < 50 -> "2"
@ -97,13 +94,13 @@ fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrD
else -> "5"
}
// Smiley
3 -> when {
"POINT_3" -> when {
score == 0f -> "0"
score <= 30 -> ":("
score <= 60 -> ":|"
else -> ":)"
}
// 10 point decimal
4 -> (score / 10).toString()
"POINT_10_DECIMAL" -> (score / 10).toString()
else -> throw Exception("Unknown score type")
}
}

View File

@ -4,8 +4,7 @@ data class OAuth(
val access_token: String,
val token_type: String,
val expires: Long,
val expires_in: Long,
val refresh_token: String?) {
val expires_in: Long) {
fun isExpired() = System.currentTimeMillis() > expires
}

View File

@ -87,7 +87,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
.flatMap { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.remote_id = remoteTrack.remote_id
track.media_id = remoteTrack.media_id
update(track)
} else {
track.score = DEFAULT_SCORE
@ -141,4 +141,4 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
}
}
}
}

View File

@ -42,7 +42,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
),
"media" to jsonObject(
"data" to jsonObject(
"id" to track.remote_id,
"id" to track.media_id,
"type" to "manga"
)
)
@ -52,7 +52,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
rest.addLibManga(jsonObject("data" to data))
.map { json ->
track.remote_id = json["data"]["id"].int
track.media_id = json["data"]["id"].int
track
}
}
@ -63,7 +63,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
// @formatter:off
val data = jsonObject(
"type" to "libraryEntries",
"id" to track.remote_id,
"id" to track.media_id,
"attributes" to jsonObject(
"status" to track.toKitsuStatus(),
"progress" to track.last_chapter_read,
@ -72,7 +72,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
)
// @formatter:on
rest.updateLibManga(track.remote_id, jsonObject("data" to data))
rest.updateLibManga(track.media_id, jsonObject("data" to data))
.map { track }
}
}
@ -88,7 +88,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
}
fun findLibManga(track: Track, userId: String): Observable<Track?> {
return rest.findLibManga(track.remote_id, userId)
return rest.findLibManga(track.media_id, userId)
.map { json ->
val data = json["data"].array
if (data.size() > 0) {
@ -101,7 +101,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
}
fun getLibManga(track: Track): Observable<Track> {
return rest.getLibManga(track.remote_id)
return rest.getLibManga(track.media_id)
.map { json ->
val data = json["data"].array
if (data.size() > 0) {
@ -204,4 +204,4 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
}
}
}

View File

@ -19,12 +19,12 @@ open class KitsuManga(obj: JsonObject) {
@CallSuper
open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
remote_id = this@KitsuManga.id
media_id = this@KitsuManga.id
title = canonicalTitle
total_chapters = chapterCount ?: 0
cover_url = original
summary = synopsis
tracking_url = KitsuApi.mangaUrl(remote_id)
tracking_url = KitsuApi.mangaUrl(media_id)
publishing_status = this@KitsuManga.status
publishing_type = type
start_date = startDate.orEmpty()
@ -32,13 +32,13 @@ open class KitsuManga(obj: JsonObject) {
}
class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) {
val remoteId by obj.byInt("id")
val libraryId by obj.byInt("id")
override val status by obj["attributes"].byString
val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
val progress by obj["attributes"].byInt
override fun toTrack() = super.toTrack().apply {
remote_id = remoteId
media_id = libraryId // TODO migrate media ids to library ids
status = toTrackStatus()
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
last_chapter_read = progress

View File

@ -10,7 +10,9 @@ class TrackSearch : Track {
override var sync_id: Int = 0
override var remote_id: Int = 0
override var media_id: Int = 0
override var library_id: Long? = null
override lateinit var title: String
@ -42,13 +44,13 @@ class TrackSearch : Track {
if (manga_id != other.manga_id) return false
if (sync_id != other.sync_id) return false
return remote_id == other.remote_id
return media_id == other.media_id
}
override fun hashCode(): Int {
var result = (manga_id xor manga_id.ushr(32)).toInt()
result = 31 * result + sync_id
result = 31 * result + remote_id
result = 31 * result + media_id
return result
}
companion object {

View File

@ -54,11 +54,11 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
.map {
TrackSearch.create(TrackManager.MYANIMELIST).apply {
title = it.selectText("title")!!
remote_id = it.selectInt("id")
media_id = it.selectInt("id")
total_chapters = it.selectInt("chapters")
summary = it.selectText("synopsis")!!
cover_url = it.selectText("image")!!
tracking_url = MyanimelistApi.mangaUrl(remote_id)
tracking_url = MyanimelistApi.mangaUrl(media_id)
publishing_status = it.selectText("status")!!
publishing_type = it.selectText("type")!!
start_date = it.selectText("start_date")!!
@ -77,13 +77,13 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
.map {
TrackSearch.create(TrackManager.MYANIMELIST).apply {
title = it.selectText("series_title")!!
remote_id = it.selectInt("series_mangadb_id")
media_id = it.selectInt("series_mangadb_id")
last_chapter_read = it.selectInt("my_read_chapters")
status = it.selectInt("my_status")
score = it.selectInt("my_score").toFloat()
total_chapters = it.selectInt("series_chapters")
cover_url = it.selectText("series_image")!!
tracking_url = MyanimelistApi.mangaUrl(remote_id)
tracking_url = MyanimelistApi.mangaUrl(media_id)
}
}
.toList()
@ -91,7 +91,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
fun findLibManga(track: Track, username: String): Observable<Track?> {
return getList(username)
.map { list -> list.find { it.remote_id == track.remote_id } }
.map { list -> list.find { it.media_id == track.media_id } }
}
fun getLibManga(track: Track, username: String): Observable<Track> {
@ -169,12 +169,12 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
fun getUpdateUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
.appendEncodedPath("api/mangalist/update")
.appendPath("${track.remote_id}.xml")
.appendPath("${track.media_id}.xml")
.toString()
fun getAddUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
.appendEncodedPath("api/mangalist/add")
.appendPath("${track.remote_id}.xml")
.appendPath("${track.media_id}.xml")
.toString()
fun createHeaders(username: String, password: String): Headers {

View File

@ -3,7 +3,10 @@ package eu.kanade.tachiyomi.network
import android.content.Context
import android.os.Build
import okhttp3.Cache
import okhttp3.CipherSuite
import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient
import okhttp3.TlsVersion
import java.io.File
import java.io.IOException
import java.net.InetAddress
@ -108,6 +111,18 @@ class NetworkHelper(context: Context) {
sslSocketFactory(TLSSocketFactory(), trustManagers[0] as X509TrustManager)
}
val specCompat = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
.cipherSuites(
*ConnectionSpec.MODERN_TLS.cipherSuites().orEmpty().toTypedArray(),
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
)
.build()
val specs = listOf(specCompat, ConnectionSpec.CLEARTEXT)
connectionSpecs(specs)
return this
}
}

View File

@ -1,11 +1,21 @@
package eu.kanade.tachiyomi.ui.base.presenter
import android.os.Bundle
import nucleus.presenter.RxPresenter
import nucleus.presenter.delivery.Delivery
import rx.Observable
open class BasePresenter<V> : RxPresenter<V>() {
override fun onCreate(savedState: Bundle?) {
try {
super.onCreate(savedState)
} catch (e: NullPointerException) {
// Swallow this error. This should be fixed in the library but since it's not critical
// (only used by restartables) it should be enough. It saves me a fork.
}
}
/**
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
* subscription list.

View File

@ -7,9 +7,9 @@ import eu.kanade.tachiyomi.util.LocaleHelper
import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
class LangHolder(view: View, adapter: FlexibleAdapter<*>) :
BaseFlexibleViewHolder(view, adapter, true) {
BaseFlexibleViewHolder(view, adapter) {
fun bind(item: LangItem) {
title.text = LocaleHelper.getDisplayName(item.code, itemView.context)
}
}
}

View File

@ -23,9 +23,10 @@ class AnilistLoginActivity : AppCompatActivity() {
val view = ProgressBar(this)
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
val code = intent.data?.getQueryParameter("code")
if (code != null) {
trackManager.aniList.login(code)
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
val matchResult = regex.find(intent.data?.fragment.toString())
if (matchResult?.groups?.get(1) != null) {
trackManager.aniList.login(matchResult.groups[1]!!.value)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({

View File

@ -22,7 +22,8 @@ import timber.log.Timber
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import java.util.Locale
import java.util.TimeZone
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
@ -71,7 +72,16 @@ class SettingsAboutController : SettingsController() {
}
preference {
title = "Discord"
val url = "https://discord.gg/2dDQBv2"
val url = "https://discord.gg/tachiyomi"
summary = url
onClick {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
}
}
preference {
title = "Github"
val url = "https://github.com/inorichi/tachiyomi"
summary = url
onClick {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))

View File

@ -30,7 +30,7 @@ class SettingsGeneralController : SettingsController() {
key = Keys.lang
titleRes = R.string.pref_language
entryValues = arrayOf("", "ar", "bg", "bn", "de", "en-US", "en-GB", "es", "fr", "hi",
"hu", "id", "it", "ja", "ko", "lv", "ms", "nl", "pl", "pt", "pt-BR", "ro",
"hu", "in", "it", "ja", "ko", "lv", "ms", "nl", "pl", "pt", "pt-BR", "ro",
"ru", "vi")
entries = entryValues.map { value ->
val locale = LocaleHelper.getLocaleFromString(value.toString())

View File

@ -1,5 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<changelog bulletedList="true">
<changelogversion versionName="v0.7.4" changeDate="">
<changelogtext>Updated Anilist's API to v2.</changelogtext>
<changelogtext>Added Github link to about.</changelogtext>
<changelogtext>Fixed indonesian language not working.</changelogtext>
<changelogtext>Fixed an issue on KitKat that crashed the app when scheduling updates.</changelogtext>
<changelogtext>Fixed a few more issues introduced on the previous release.</changelogtext>
</changelogversion>
<changelogversion versionName="v0.7.3" changeDate="">
<changelogtext>Fixed the tracking search layout when there are many results.</changelogtext>
@ -197,54 +209,4 @@
<changelogtext>Fixed lost covers on some devices.</changelogtext>
</changelogversion>
<changelogversion versionName="v0.4.2" changeDate="">
<changelogtext>Added support for Anilist and Kitsu.</changelogtext>
<changelogtext>Added library refresh option to library updates tab.</changelogtext>
<changelogtext>Back button closes drawers before exiting the app.</changelogtext>
<changelogtext>Fixed issues when using custom app language.</changelogtext>
<changelogtext>Fixed updater in Android N.</changelogtext>
<changelogtext>Fixed Mangafox search.</changelogtext>
</changelogversion>
<changelogversion versionName="v0.4.1" changeDate="">
<changelogtext>Added an app's language selector.</changelogtext>
<changelogtext>Added options to sort the library and merged them with the filters.</changelogtext>
<changelogtext>Added an option to automatically download chapters.</changelogtext>
<changelogtext>Fixed performance issues when using a custom downloads directory, especially in the library updates tab.</changelogtext>
<changelogtext>Fixed gesture conflicts with the contextual menu and the webtoon reader.</changelogtext>
<changelogtext>Fixed wrong page direction when using volume keys for the right to left reader.</changelogtext>
<changelogtext>Fixed many crashes.</changelogtext>
</changelogversion>
<changelogversion versionName="v0.4.0" changeDate="">
<changelogtext>The download manager has been rewritten and it's possible some of your downloads
aren't recognized anymore. It's recommended to manually delete everything and start over.
</changelogtext>
<changelogtext>Now it's possible to download to any folder in a SD card.</changelogtext>
<changelogtext>The download directory setting has been reset.</changelogtext>
<changelogtext>Active downloads now persist after restarts.</changelogtext>
<changelogtext>Allow to bookmark chapters.</changelogtext>
<changelogtext>Allow to share or save a single page while reading with a long tap.</changelogtext>
<changelogtext>Added italian translation.</changelogtext>
<changelogtext>Image is now the default decoder.</changelogtext>
</changelogversion>
</changelog>

View File

@ -385,6 +385,7 @@
<string name="dropped">Dropped</string>
<string name="on_hold">On hold</string>
<string name="plan_to_read">Plan to read</string>
<string name="repeating">Re-reading</string>
<string name="score">Score</string>
<string name="title">Title</string>
<string name="status">Status</string>