diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b77cf7f29..2d08379f9 100755 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -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) diff --git a/.travis/build.sh b/.travis/build.sh deleted file mode 100755 index 049fd015b..000000000 --- a/.travis/build.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -git fetch --unshallow #required for commit count - -if [ -z "$TRAVIS_TAG" ]; then - ./gradlew clean assembleStandardDebug - - COMMIT_COUNT=$(git rev-list --count HEAD) - export ARTIFACT="tachiyomi-r${COMMIT_COUNT}.apk" - - mv app/build/outputs/apk/standard/debug/app-standard-debug.apk $ARTIFACT -else - ./gradlew clean assembleStandardRelease - - TOOLS="$(ls -d ${ANDROID_HOME}/build-tools/* | tail -1)" - export ARTIFACT="tachiyomi-${TRAVIS_TAG}.apk" - - ${TOOLS}/zipalign -v -p 4 app/build/outputs/apk/standard/release/app-standard-release-unsigned.apk app-aligned.apk - ${TOOLS}/apksigner sign --ks $STORE_PATH --ks-key-alias $STORE_ALIAS --ks-pass env:STORE_PASS --key-pass env:KEY_PASS --out $ARTIFACT app-aligned.apk -fi diff --git a/.travis/deploy.sh b/.travis/deploy.sh deleted file mode 100755 index c95222614..000000000 --- a/.travis/deploy.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -pattern="tachiyomi-r*" -files=( $pattern ) -export ARTIFACT="${files[0]}" - -if [ -z "$ARTIFACT" ]; then - echo "Artifact not found" - exit 1 -fi - -export SSHOPTIONS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${DEPLOY_KEY}" - -scp $SSHOPTIONS $ARTIFACT $DEPLOY_USER@$DEPLOY_HOST:builds/ -ssh $SSHOPTIONS $DEPLOY_USER@$DEPLOY_HOST ln -sf $ARTIFACT builds/latest diff --git a/.travis/secrets.tar.enc b/.travis/secrets.tar.enc deleted file mode 100644 index a3fa82b35..000000000 Binary files a/.travis/secrets.tar.enc and /dev/null differ diff --git a/app/.gitignore b/app/.gitignore index 012bccc6a..4f8f315a1 100755 --- a/app/.gitignore +++ b/app/.gitignore @@ -2,4 +2,4 @@ *iml *.iml custom.gradle -google-services.json \ No newline at end of file +google-services.json diff --git a/app/build.gradle b/app/build.gradle index 9e3c73491..eb895785a 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,8 +40,8 @@ android { minSdkVersion 16 targetSdkVersion 27 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - versionCode 7203 - versionName "v7.2.3-EH" + versionCode 7400 + versionName "v7.4.3-EH" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" @@ -125,6 +125,8 @@ dependencies { implementation 'com.android.support:multidex:1.0.2' + standardImplementation 'com.google.firebase:firebase-core:11.8.0' + // ReactiveX implementation 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxjava:1.3.6' @@ -133,7 +135,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 @@ -157,7 +159,7 @@ dependencies { implementation 'org.jsoup:jsoup:1.10.2' // Job scheduling - implementation 'com.evernote:android-job:1.2.4' + implementation 'com.evernote:android-job:1.2.5' implementation 'com.google.android.gms:play-services-gcm:11.8.0' // Changelog @@ -206,9 +208,10 @@ dependencies { implementation 'me.gujun.android.taggroup:library:1.4@aar' // Conductor - implementation "com.github.inorichi.Conductor:conductor:05c4d4d" + implementation "com.github.inorichi.Conductor:conductor:be8b3c5" implementation ("com.bluelinelabs:conductor-support:2.1.5-SNAPSHOT") { exclude group: "com.bluelinelabs", module: "conductor" + exclude group: "com.android.support" } implementation 'com.github.inorichi:conductor-support-preference:27.0.2' @@ -275,3 +278,7 @@ kotlin { androidExtensions { experimental = true } + +if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) { + apply plugin: 'com.google.gms.google-services' +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt index 4fcbafc2f..c58b3e603 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/TrackTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/TrackTypeAdapter.kt index 6f01fa023..d9f3b8cac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/TrackTypeAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/TrackTypeAdapter.kt @@ -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() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt index 5330e1ab5..998a5a1a7 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt index aaf64f23a..6759316de 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt @@ -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() { 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() { 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)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt index 3b883a874..19133e037 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt index b7c445168..65f6ec7ab 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt @@ -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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt index 79aba5523..82c863fb9 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt @@ -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" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt index ae2cf2111..8fdb4f626 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt @@ -1,16 +1,20 @@ package eu.kanade.tachiyomi.data.download +import android.app.Notification import android.app.Service import android.content.Context import android.content.Intent import android.net.NetworkInfo.State.CONNECTED import android.net.NetworkInfo.State.DISCONNECTED +import android.os.Build import android.os.IBinder import android.os.PowerManager +import android.support.v4.app.NotificationCompat import com.github.pwittchen.reactivenetwork.library.Connectivity import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork import com.jakewharton.rxrelay.BehaviorRelay import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.util.connectivityManager import eu.kanade.tachiyomi.util.plusAssign @@ -41,7 +45,12 @@ class DownloadService : Service() { * @param context the application context. */ fun start(context: Context) { - context.startService(Intent(context, DownloadService::class.java)) + val intent = Intent(context, DownloadService::class.java) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + context.startService(intent) + } else { + context.startForegroundService(intent) + } } /** @@ -81,6 +90,7 @@ class DownloadService : Service() { */ override fun onCreate() { super.onCreate() + startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification()) runningRelay.call(true) subscriptions = CompositeSubscription() listenDownloaderState() @@ -176,4 +186,10 @@ class DownloadService : Service() { if (!isHeld) acquire() } + private fun getPlaceholderNotification(): Notification { + return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER) + .setContentTitle(getString(R.string.download_notifier_downloader_title)) + .build() + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 9b5e7cace..fb880d756 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -39,6 +39,8 @@ object PreferenceKeys { const val cropBorders = "crop_borders" + const val cropBordersWebtoon = "crop_borders_webtoon" + const val readWithTapping = "reader_tap" const val readWithVolumeKeys = "reader_volume_keys" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index c913aaff5..15e1527ba 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -68,6 +68,8 @@ class PreferencesHelper(val context: Context) { fun cropBorders() = rxPrefs.getBoolean(Keys.cropBorders, false) + fun cropBordersWebtoon() = rxPrefs.getBoolean(Keys.cropBordersWebtoon, false) + fun readWithTapping() = rxPrefs.getBoolean(Keys.readWithTapping, true) fun readWithVolumeKeys() = rxPrefs.getBoolean(Keys.readWithVolumeKeys, false) @@ -118,7 +120,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()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index 0b71e3e85..eea78051f 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -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 { - 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 { - 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 { - 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 { - 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 + } + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index ee2864352..edb63931c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -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 { - 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 { - 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> { - return rest.search(query, 1) - .map { list -> - list.filter { it.type != "Novel" }.map { it.toTrack() } + fun search(search: String): Observable> { + val query = """ + query Search(${'$'}query: String) { + Page (perPage: 25) { + media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { + id + title { + romaji + } + coverImage { + large + } + type + status + chapters + startDate { + year + month + day + } + } + } } - .onErrorReturn { emptyList() } - } - - fun getList(username: String): Observable> { - 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 { - // 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 { + 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 { - return findLibManga(track, username) + fun getLibManga(track: Track, userid: Int): Observable { + return findLibManga(track, userid) .map { it ?: throw Exception("Could not find manga") } } - fun login(authCode: String): Observable { - return restBuilder() - .client(client) + fun createOAuth(token: String): OAuth { + return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) + } + + fun getCurrentUser(): Observable> { + 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> { - 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 - - @GET("user") - fun getCurrentUser(): Observable - - @GET("manga/search/{query}") - fun search( - @Path("query") query: String, - @Query("page") page: Int - ): Observable> - - @GET("user/{username}/mangalist") - fun getLib( - @Path("username") username: String - ): Observable - - @FormUrlEncoded - @PUT("mangalist") - fun addLibManga( - @Field("id") id: Int, - @Field("chapters_read") chapters_read: Int, - @Field("list_status") list_status: String - ) : Observable> - - @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> - + 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()) - } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt index 2bb8525d3..427b0acfe 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt index 8398477dc..3083f3b69 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt @@ -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>) { - - 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") -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt index 6f5238b37..1d7a31ac5 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index c7f4f94dd..14be0ddb7 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -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) { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index 03226896e..9d16ddb5b 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -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 { - 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 { - 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) } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt index be4ca5034..70fdef6d9 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt index 0e701730f..a7fb8b80d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt @@ -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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt index 392ff220b..16c9269aa 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt @@ -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 { 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 { @@ -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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index 97d4b4d7d..5e9389483 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -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 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Kissmanga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Kissmanga.kt index b311a942e..0287c1fb5 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Kissmanga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Kissmanga.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.online.ParsedHttpSource import okhttp3.FormBody +import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response @@ -29,6 +30,11 @@ class Kissmanga : ParsedHttpSource() { override val client: OkHttpClient = network.cloudflareClient + override fun headersBuilder(): Headers.Builder { + return Headers.Builder() + .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) Gecko/20100101 Firefox/60") + } + override fun popularMangaSelector() = "table.listing tr:gt(1)" override fun latestUpdatesSelector() = "table.listing tr:gt(1)" @@ -156,7 +162,7 @@ class Kissmanga : ParsedHttpSource() { // There are two functions in an inline script needed to decrypt the urls. We find and // execute them. - var p = Pattern.compile("(.*CryptoJS.*)") + var p = Pattern.compile("(var.*CryptoJS.*)") var m = p.matcher(body) while (m.find()) { it.evaluate(m.group(1)) @@ -244,4 +250,4 @@ class Kissmanga : ParsedHttpSource() { Genre("Yaoi"), Genre("Yuri") ) -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt index cfe4191d9..130362f51 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt @@ -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 : RxPresenter() { + 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. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java index ddc4aba5a..5210a3a2a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java @@ -28,7 +28,7 @@ public class NucleusConductorDelegate

{ Bundle onSaveInstanceState() { Bundle bundle = new Bundle(); - getPresenter(); +// getPresenter(); // Workaround a crash related to saving instance state with child routers if (presenter != null) { presenter.save(bundle); } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangHolder.kt index 9604e7340..66c94967d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangHolder.kt @@ -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) } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt index 7edc3bd69..8bf0a6197 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt @@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import kotlinx.android.synthetic.main.extension_card_header.* class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<*>) : - BaseFlexibleViewHolder(view, adapter, true) { + BaseFlexibleViewHolder(view, adapter) { @SuppressLint("SetTextI18n") fun bind(item: ExtensionGroupItem) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsDialog.kt index 8927ad4fe..f40f47e2c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsDialog.kt @@ -9,6 +9,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.util.visibleIf import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener import kotlinx.android.synthetic.main.reader_settings_dialog.view.* import rx.Observable @@ -91,6 +92,23 @@ class ReaderSettingsDialog : DialogFragment() { crop_borders.setOnCheckedChangeListener { _, isChecked -> preferences.cropBorders().set(isChecked) } + + crop_borders_webtoon.isChecked = preferences.cropBordersWebtoon().getOrDefault() + crop_borders_webtoon.setOnCheckedChangeListener { _, isChecked -> + preferences.cropBordersWebtoon().set(isChecked) + } + + val readerActivity = activity as? ReaderActivity + val isWebtoonViewer = if (readerActivity != null) { + val mangaViewer = readerActivity.presenter.manga.viewer + val viewer = if (mangaViewer == 0) preferences.defaultViewer() else mangaViewer + viewer == ReaderActivity.WEBTOON + } else { + false + } + + crop_borders.visibleIf { !isWebtoonViewer } + crop_borders_webtoon.visibleIf { isWebtoonViewer } } override fun onDestroyView() { @@ -98,4 +116,4 @@ class ReaderSettingsDialog : DialogFragment() { super.onDestroyView() } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt index d2164f9e7..3ac0c5085 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt @@ -4,7 +4,12 @@ import android.os.Build import android.os.Bundle import android.support.v7.widget.RecyclerView import android.util.DisplayMetrics -import android.view.* +import android.view.Display +import android.view.GestureDetector +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import eu.kanade.tachiyomi.source.model.Page @@ -123,7 +128,7 @@ class WebtoonReader : BaseReader() { .distinctUntilChanged() .subscribe { refreshAdapter() }) - subscriptions.add(readerActivity.preferences.cropBorders() + subscriptions.add(readerActivity.preferences.cropBordersWebtoon() .asObservable() .doOnNext { cropBorders = it } .skip(1) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt index 982e74fe1..6b5da186e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt @@ -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({ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt index db043f18a..ae1e74694 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt @@ -20,7 +20,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 @@ -61,6 +62,15 @@ class SettingsAboutController : SettingsController() { isVisible = false } } + preference { + title = "Github" + val url = "https://github.com/NerdNumber9/TachiyomiEH" + summary = url + onClick { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + startActivity(intent) + } + } preference { titleRes = R.string.version summary = if (BuildConfig.DEBUG) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt index 6bd854ef7..527b16ce5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt @@ -31,8 +31,9 @@ class SettingsGeneralController : SettingsController() { listPreference { key = Keys.lang titleRes = R.string.pref_language - entryValues = arrayOf("", "ar", "bg", "bn", "de", "en", "es", "fr", "hi", "hu", "id", - "it", "ja", "ko", "lv", "ms", "nl", "pl", "pt", "pt-BR", "ro", "ru", "vi") + entryValues = arrayOf("", "ar", "bg", "bn", "de", "en-US", "en-GB", "es", "fr", "hi", + "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()) locale?.getDisplayName(locale)?.capitalize() ?: diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index aa3188f03..ae9f739f6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -76,8 +76,8 @@ class SettingsReaderController : SettingsController() { defaultValue = true } switchPreference { - key = Keys.enableTransitions - titleRes = R.string.pref_page_transitions + key = Keys.keepScreenOn + titleRes = R.string.pref_keep_screen_on defaultValue = true } switchPreference { @@ -85,15 +85,28 @@ class SettingsReaderController : SettingsController() { titleRes = R.string.pref_show_page_number defaultValue = true } - switchPreference { - key = Keys.cropBorders - titleRes = R.string.pref_crop_borders - defaultValue = false + preferenceCategory { + titleRes = R.string.pager_viewer + + switchPreference { + key = Keys.enableTransitions + titleRes = R.string.pref_page_transitions + defaultValue = true + } + switchPreference { + key = Keys.cropBorders + titleRes = R.string.pref_crop_borders + defaultValue = false + } } - switchPreference { - key = Keys.keepScreenOn - titleRes = R.string.pref_keep_screen_on - defaultValue = true + preferenceCategory { + titleRes = R.string.webtoon_viewer + + switchPreference { + key = Keys.cropBordersWebtoon + titleRes = R.string.pref_crop_borders + defaultValue = false + } } preferenceCategory { titleRes = R.string.pref_reader_navigation @@ -116,4 +129,4 @@ class SettingsReaderController : SettingsController() { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt index e3f6bbb01..af9b041e2 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt @@ -47,6 +47,10 @@ inline fun View.gone() { visibility = View.GONE } +inline fun View.visibleIf(block: () -> Boolean) { + visibility = if (block()) View.VISIBLE else View.GONE +} + /** * Returns a TextDrawable determined by input * @@ -63,4 +67,4 @@ fun View.getRound(text: String, random : Boolean = true): TextDrawable { .useFont(Typeface.DEFAULT) .endConfig() .buildRound(text, if (random) ColorGenerator.MATERIAL.randomColor else ColorGenerator.MATERIAL.getColor(text)) -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/reader_settings_dialog.xml b/app/src/main/res/layout/reader_settings_dialog.xml index 2ac02900b..ea9793236 100755 --- a/app/src/main/res/layout/reader_settings_dialog.xml +++ b/app/src/main/res/layout/reader_settings_dialog.xml @@ -171,10 +171,16 @@ android:layout_height="wrap_content" android:text="@string/pref_crop_borders"/> + + - \ No newline at end of file + diff --git a/app/src/main/res/raw/changelog_release.xml b/app/src/main/res/raw/changelog_release.xml index a2b1d952c..eb96cacdc 100755 --- a/app/src/main/res/raw/changelog_release.xml +++ b/app/src/main/res/raw/changelog_release.xml @@ -1,5 +1,31 @@ + + Updated Anilist's API to v2. + + Added Github link to about. + + Fixed indonesian language not working. + + Fixed an issue on KitKat that crashed the app when scheduling updates. + + Fixed a few more issues introduced on the previous release. + + + + Fixed the tracking search layout when there are many results. + + Separate english language into american and british so that dates are formatted according to that locale. + + Added Firebase analytics, for Android API distribution. + + Crop borders for webtoons now has a separate setting. + + The downloader now runs in a foreground service to prevent it from being killed. + + Fixed a few weird crashes. + + Fix app crashing on some older devices (again) Fix app crashing sometimes when long-pressing the back/menu button in the top left @@ -239,54 +265,4 @@ Fixed lost covers on some devices. - - Added support for Anilist and Kitsu. - - Added library refresh option to library updates tab. - - Back button closes drawers before exiting the app. - - Fixed issues when using custom app language. - - Fixed updater in Android N. - - Fixed Mangafox search. - - - - Added an app's language selector. - - Added options to sort the library and merged them with the filters. - - Added an option to automatically download chapters. - - Fixed performance issues when using a custom downloads directory, especially in the library updates tab. - - Fixed gesture conflicts with the contextual menu and the webtoon reader. - - Fixed wrong page direction when using volume keys for the right to left reader. - - Fixed many crashes. - - - - 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. - - - Now it's possible to download to any folder in a SD card. - - The download directory setting has been reset. - - Active downloads now persist after restarts. - - Allow to bookmark chapters. - - Allow to share or save a single page while reading with a long tap. - - Added italian translation. - - Image is now the default decoder. - - diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-in/strings.xml similarity index 100% rename from app/src/main/res/values-id/strings.xml rename to app/src/main/res/values-in/strings.xml diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 22865285e..c1c27dc43 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,7 +58,6 @@ Edit the cover picture Sort up Sort down - Unread Downloaded Next unread Start @@ -190,6 +189,7 @@ Right to left Vertical Webtoon + Pager Image decoder Scale type Fit screen @@ -384,6 +384,7 @@ Dropped On hold Plan to read + Re-reading Score Title Status diff --git a/build.gradle b/build.gradle index 0166a6a29..7ea03abe0 100755 --- a/build.gradle +++ b/build.gradle @@ -7,9 +7,10 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.0' + classpath 'com.android.tools.build:gradle:3.1.2' classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0' classpath 'com.github.zellius:android-shortcut-gradle-plugin:0.1.2' + classpath 'com.google.gms:google-services:3.2.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files