Compare commits

...

23 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
f8a03226ee Release v0.7.3 2018-04-28 11:10:29 +02:00
32db1e3045 Run downloader in foreground service 2018-04-28 10:54:27 +02:00
303e6c0102 Reorganize reader settings. Update Conductor version 2018-04-28 10:40:08 +02:00
18883f1ba3 Crop borders for webtoon now have a separate setting. Close #972 2018-04-27 16:55:12 +02:00
5c31271e91 Workaround a crash related to saving instance state and child controllers 2018-04-25 16:26:46 +02:00
00981cf4e8 Include firebase analytics 2018-04-25 13:46:57 +02:00
968f4a69e8 Separate 'en' locale into 'en-US' and 'en-GB' for displaying dates 2018-04-22 13:15:47 +02:00
e7e1a9bf50 Fix #1073 2018-04-15 13:26:33 +02:00
fe1becb001 Update strings.xml (#1324) 2018-04-15 12:59:52 +02:00
7789171c71 Remove F-Droid.org link (#1357)
* remove fdroid.org

* Update README.md

* fix broken links
2018-04-15 12:48:03 +02:00
3fd2222c99 Update russian sources (#1362)
1) moved headerBuilder to imageRequest
2) changed the method of gets thumbnails
3) updated js for genres
4) update genre list
2018-04-15 12:47:39 +02:00
6de36a88c0 Fix tracking search layout 2018-04-13 16:28:09 +02:00
b37685542d Add new translation link (#1355) 2018-04-09 16:25:30 +02:00
51 changed files with 789 additions and 444 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).** 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? 3. What is your type of issue?
* [Catalogue request](#catalogue-requests) * [Catalogue request](#catalogue-requests)
* [Bugs](#bugs) * [Bugs](#bugs)

View File

@ -2,6 +2,8 @@
git fetch --unshallow #required for commit count git fetch --unshallow #required for commit count
cp .travis/google-services.json app/
if [ -z "$TRAVIS_TAG" ]; then if [ -z "$TRAVIS_TAG" ]; then
./gradlew clean assembleStandardDebug ./gradlew clean assembleStandardDebug

View File

@ -0,0 +1,73 @@
{
"project_info": {
"project_number": "777921915939",
"firebase_url": "https://tachiyomi-47364.firebaseio.com",
"project_id": "tachiyomi-47364",
"storage_bucket": "tachiyomi-47364.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:777921915939:android:36544cd2d96c50c7",
"android_client_info": {
"package_name": "eu.kanade.tachiyomi"
}
},
"oauth_client": [
{
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAHr8RxyeiSPC_MxJTnivz-hmdo5oX0QQQ"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:777921915939:android:564fdc1d62efd1de",
"android_client_info": {
"package_name": "eu.kanade.tachiyomi.debug"
}
},
"oauth_client": [
{
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAHr8RxyeiSPC_MxJTnivz-hmdo5oX0QQQ"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}

View File

@ -1,6 +1,6 @@
| Build | Download | F-Droid | Contribute | Contact | | 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=stable)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest) | [![fdroid release](https://img.shields.io/badge/stable-f--droid.org-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid dev](https://img.shields.io/badge/dev-wiki-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | [![Translation status](http://weblate.j2ghz.com/widgets/tachiyomi/-/svg-badge.svg)](https://github.com/inorichi/tachiyomi/wiki/Translation) | [![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 # ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
@ -14,16 +14,16 @@ Features include:
* Online reading from sources such as KissManga, MangaFox, [and more](https://github.com/inorichi/tachiyomi-extensions) * Online reading from sources such as KissManga, MangaFox, [and more](https://github.com/inorichi/tachiyomi-extensions)
* Local reading of downloaded manga * Local reading of downloaded manga
* Configurable reader with multiple viewers, reading directions and other settings * Configurable reader with multiple viewers, reading directions and other settings
* MyAnimeList, 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 * Categories to organize your library
* Light and dark themes * Light and dark themes
* Schedule updating your library for new chapters * 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 ## Download
Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases) or from [f-droid](https://f-droid.org/packages/eu.kanade.tachiyomi/). Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases).
If you want to try new features before they get to the stable release, you can download the dev version [here](http://tachiyomi.kanade.eu/latest) (auto-updates not included), or add our [F-Droid repo](https://github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions). If you want to try new features before they get to the stable release, you can download the dev version [here](http://tachiyomi.kanade.eu/latest) (auto-updates not included), or add our [F-Droid repo](https://github.com/inorichi/tachiyomi/wiki/F-Droid-for-dev-versions).
## Issues, Feature Requests and Contributing ## Issues, Feature Requests and Contributing
@ -32,7 +32,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
<details><summary>Issues</summary> <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).** 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> </details>

3
app/.gitignore vendored
View File

@ -1,4 +1,5 @@
/build /build
*iml *iml
*.iml *.iml
custom.gradle custom.gradle
google-services.json

View File

@ -38,8 +38,8 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 27 targetSdkVersion 27
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 35 versionCode 37
versionName "0.7.2" versionName "0.7.4"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -120,6 +120,8 @@ dependencies {
implementation 'com.android.support:multidex:1.0.2' implementation 'com.android.support:multidex:1.0.2'
standardImplementation 'com.google.firebase:firebase-core:11.8.0'
// ReactiveX // ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'io.reactivex:rxjava:1.3.6' implementation 'io.reactivex:rxjava:1.3.6'
@ -128,7 +130,7 @@ dependencies {
implementation 'com.github.pwittchen:reactivenetwork:0.7.0' implementation 'com.github.pwittchen:reactivenetwork:0.7.0'
// Network client // 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' implementation 'com.squareup.okio:okio:1.14.0'
// REST // REST
@ -152,7 +154,7 @@ dependencies {
implementation 'org.jsoup:jsoup:1.10.2' implementation 'org.jsoup:jsoup:1.10.2'
// Job scheduling // 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' implementation 'com.google.android.gms:play-services-gcm:11.8.0'
// Changelog // Changelog
@ -201,7 +203,7 @@ dependencies {
implementation 'me.gujun.android.taggroup:library:1.4@aar' implementation 'me.gujun.android.taggroup:library:1.4@aar'
// Conductor // Conductor
implementation "com.github.inorichi.Conductor:conductor:05c4d4d" implementation "com.github.inorichi.Conductor:conductor:be8b3c5"
implementation ("com.bluelinelabs:conductor-support:2.1.5-SNAPSHOT") { implementation ("com.bluelinelabs:conductor-support:2.1.5-SNAPSHOT") {
exclude group: "com.bluelinelabs", module: "conductor" exclude group: "com.bluelinelabs", module: "conductor"
} }
@ -254,3 +256,7 @@ kotlin {
androidExtensions { androidExtensions {
experimental = true experimental = true
} }
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
apply plugin: 'com.google.gms.google-services'
}

View File

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

View File

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

View File

@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
/** /**
* Version of the database. * Version of the database.
*/ */
const val DATABASE_VERSION = 6 const val DATABASE_VERSION = 7
} }
override fun onCreate(db: SQLiteDatabase) = with(db) { override fun onCreate(db: SQLiteDatabase) = with(db) {
@ -57,6 +57,9 @@ class DbOpenHelper(context: Context)
if (oldVersion < 6) { if (oldVersion < 6) {
db.execSQL(TrackTable.addTrackingUrl) db.execSQL(TrackTable.addTrackingUrl)
} }
if (oldVersion < 7) {
db.execSQL(TrackTable.addLibraryId)
}
} }
override fun onConfigure(db: SQLiteDatabase) { 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.models.TrackImpl
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID 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_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_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_SCORE
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID 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_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id) put(COL_MANGA_ID, obj.manga_id)
put(COL_SYNC_ID, obj.sync_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_TITLE, obj.title)
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read) put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
put(COL_TOTAL_CHAPTERS, obj.total_chapters) put(COL_TOTAL_CHAPTERS, obj.total_chapters)
@ -62,7 +64,8 @@ class TrackGetResolver : DefaultGetResolver<Track>() {
id = cursor.getLong(cursor.getColumnIndex(COL_ID)) id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)) manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_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)) title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ)) last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS)) total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))

View File

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

View File

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

View File

@ -10,7 +10,9 @@ object TrackTable {
const val COL_SYNC_ID = "sync_id" 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" const val COL_TITLE = "title"
@ -29,7 +31,8 @@ object TrackTable {
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL, $COL_MANGA_ID INTEGER NOT NULL,
$COL_SYNC_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_TITLE TEXT NOT NULL,
$COL_LAST_CHAPTER_READ INTEGER NOT NULL, $COL_LAST_CHAPTER_READ INTEGER NOT NULL,
$COL_TOTAL_CHAPTERS INTEGER NOT NULL, $COL_TOTAL_CHAPTERS INTEGER NOT NULL,
@ -43,4 +46,7 @@ object TrackTable {
val addTrackingUrl: String val addTrackingUrl: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_TRACKING_URL TEXT DEFAULT ''" 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

@ -1,16 +1,20 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.app.Notification
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.NetworkInfo.State.CONNECTED import android.net.NetworkInfo.State.CONNECTED
import android.net.NetworkInfo.State.DISCONNECTED import android.net.NetworkInfo.State.DISCONNECTED
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import android.support.v4.app.NotificationCompat
import com.github.pwittchen.reactivenetwork.library.Connectivity import com.github.pwittchen.reactivenetwork.library.Connectivity
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.connectivityManager import eu.kanade.tachiyomi.util.connectivityManager
import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.util.plusAssign
@ -41,7 +45,12 @@ class DownloadService : Service() {
* @param context the application context. * @param context the application context.
*/ */
fun start(context: 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() { override fun onCreate() {
super.onCreate() super.onCreate()
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
runningRelay.call(true) runningRelay.call(true)
subscriptions = CompositeSubscription() subscriptions = CompositeSubscription()
listenDownloaderState() listenDownloaderState()
@ -176,4 +186,10 @@ class DownloadService : Service() {
if (!isHeld) acquire() if (!isHeld) acquire()
} }
private fun getPlaceholderNotification(): Notification {
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER)
.setContentTitle(getString(R.string.download_notifier_downloader_title))
.build()
}
} }

View File

@ -39,6 +39,8 @@ object PreferenceKeys {
const val cropBorders = "crop_borders" const val cropBorders = "crop_borders"
const val cropBordersWebtoon = "crop_borders_webtoon"
const val readWithTapping = "reader_tap" const val readWithTapping = "reader_tap"
const val readWithVolumeKeys = "reader_volume_keys" const val readWithVolumeKeys = "reader_volume_keys"

View File

@ -67,6 +67,8 @@ class PreferencesHelper(val context: Context) {
fun cropBorders() = rxPrefs.getBoolean(Keys.cropBorders, false) fun cropBorders() = rxPrefs.getBoolean(Keys.cropBorders, false)
fun cropBordersWebtoon() = rxPrefs.getBoolean(Keys.cropBordersWebtoon, false)
fun readWithTapping() = rxPrefs.getBoolean(Keys.readWithTapping, true) fun readWithTapping() = rxPrefs.getBoolean(Keys.readWithTapping, true)
fun readWithVolumeKeys() = rxPrefs.getBoolean(Keys.readWithVolumeKeys, false) fun readWithVolumeKeys() = rxPrefs.getBoolean(Keys.readWithVolumeKeys, false)
@ -117,7 +119,7 @@ class PreferencesHelper(val context: Context) {
fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "") 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()) 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.content.Context
import android.graphics.Color import android.graphics.Color
import com.google.gson.Gson
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.getOrDefault 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 eu.kanade.tachiyomi.data.track.model.TrackSearch
import rx.Completable import rx.Completable
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy
class Anilist(private val context: Context, id: Int) : TrackService(id) { 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 COMPLETED = 2
const val ON_HOLD = 3 const val ON_HOLD = 3
const val DROPPED = 4 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_STATUS = READING
const val DEFAULT_SCORE = 0 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" 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 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 getLogo() = R.drawable.al
override fun getLogoColor() = Color.rgb(18, 25, 35) override fun getLogoColor() = Color.rgb(18, 25, 35)
override fun getStatusList(): List<Int> { 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) { 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) COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold) ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped) 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 -> "" else -> ""
} }
} }
override fun getScoreList(): List<String> { override fun getScoreList(): List<String> {
return when (preferences.anilistScoreType().getOrDefault()) { return when (scorePreference.getOrDefault()) {
// 10 point // 10 point
0 -> IntRange(0, 10).map(Int::toString) POINT_10 -> IntRange(0, 10).map(Int::toString)
// 100 point // 100 point
1 -> IntRange(0, 100).map(Int::toString) POINT_100 -> IntRange(0, 100).map(Int::toString)
// 5 stars // 5 stars
2 -> IntRange(0, 5).map { "$it" } POINT_5 -> IntRange(0, 5).map { "$it" }
// Smiley // Smiley
3 -> listOf("-", "😦", "😐", "😊") POINT_3 -> listOf("-", "😦", "😐", "😊")
// 10 point decimal // 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") else -> throw Exception("Unknown score type")
} }
} }
override fun indexToScore(index: Int): Float { override fun indexToScore(index: Int): Float {
return when (preferences.anilistScoreType().getOrDefault()) { return when (scorePreference.getOrDefault()) {
// 10 point // 10 point
0 -> index * 10f POINT_10 -> index * 10f
// 100 point // 100 point
1 -> index.toFloat() POINT_100 -> index.toFloat()
// 5 stars // 5 stars
2 -> index * 20f POINT_5 -> index * 20f
// Smiley // Smiley
3 -> index * 30f POINT_3 -> index * 30f
// 10 point decimal // 10 point decimal
4 -> index.toFloat() POINT_10_DECIMAL -> index.toFloat()
else -> throw Exception("Unknown score type") else -> throw Exception("Unknown score type")
} }
} }
override fun displayScore(track: Track): String { override fun displayScore(track: Track): String {
val score = track.score val score = track.score
return when (preferences.anilistScoreType().getOrDefault()) {
2 -> "${(score / 20).toInt()}" return when (scorePreference.getOrDefault()) {
3 -> when { POINT_5 -> "${(score / 20).toInt()}"
POINT_3 -> when {
score == 0f -> "0" score == 0f -> "0"
score <= 30 -> "😦" score <= 30 -> "😦"
score <= 60 -> "😐" 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) { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED 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) return api.updateLibManga(track)
} }
override fun bind(track: Track): Observable<Track> { override fun bind(track: Track): Observable<Track> {
return api.findLibManga(track, getUsername()) return api.findLibManga(track, getUsername().toInt())
.flatMap { remoteTrack -> .flatMap { remoteTrack ->
if (remoteTrack != null) { if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
update(track) update(track)
} else { } else {
// Set default fields if it's not found in the list // 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> { override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track, getUsername()) return api.getLibManga(track, getUsername().toInt())
.map { remoteTrack -> .map { remoteTrack ->
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters 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) override fun login(username: String, password: String) = login(password)
fun login(authCode: String): Completable { fun login(token: String): Completable {
return api.login(authCode) val oauth = api.createOAuth(token)
// Save the token in the interceptor. interceptor.setAuth(oauth)
.doOnNext { interceptor.setAuth(it) } return api.getCurrentUser().map { (username, scoreType) ->
// Obtain the authenticated user from the API. scorePreference.set(scoreType)
.zipWith(api.getCurrentUser().map { pair -> saveCredentials(username.toString(), oauth.access_token)
preferences.anilistScoreType().set(pair.second) }.doOnError{
pair.first logout()
}, { oauth, user -> Pair(user, oauth.refresh_token!!) }) }.toCompletable()
// Save service credentials (username and refresh token).
.doOnNext { saveCredentials(it.first, it.second) }
// Logout on any error.
.doOnError { logout() }
.toCompletable()
} }
override fun logout() { override fun logout() {
super.logout() super.logout()
preferences.trackToken(this).set(null)
interceptor.setAuth(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 package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri import android.net.Uri
import com.github.salomonbrys.kotson.int import com.github.salomonbrys.kotson.*
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.asObservableSuccess
import okhttp3.FormBody import okhttp3.MediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.ResponseBody import okhttp3.Request
import retrofit2.Response import okhttp3.RequestBody
import retrofit2.Retrofit
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.*
import rx.Observable import rx.Observable
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val rest = restBuilder() private val parser = JsonParser()
.client(client.newBuilder().addInterceptor(interceptor).build()) private val jsonMime = MediaType.parse("application/json; charset=utf-8")
.build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
.create(Rest::class.java)
fun addLibManga(track: Track): Observable<Track> { fun addLibManga(track: Track): Observable<Track> {
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus()) val query = """
.map { response -> mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
response.body()?.close() SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status)
if (!response.isSuccessful) { { id status } }
throw Exception("Could not add manga") """
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 track
} }
} }
fun updateLibManga(track: Track): Observable<Track> { fun updateLibManga(track: Track): Observable<Track> {
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(), val query = """
track.toAnilistScore()) mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
.map { response -> SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
response.body()?.close() id
if (!response.isSuccessful) { status
throw Exception("Could not update manga") 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 track
} }
} }
fun search(query: String): Observable<List<TrackSearch>> { fun search(search: String): Observable<List<TrackSearch>> {
return rest.search(query, 1) val query = """
.map { list -> query Search(${'$'}query: String) {
list.filter { it.type != "Novel" }.map { it.toTrack() } Page (perPage: 25) {
media(search: ${'$'}query, type: MANGA, format: MANGA) {
id
title {
romaji
}
coverImage {
large
}
type
status
chapters
startDate {
year
month
day
}
}
}
} }
.onErrorReturn { emptyList() } """
} val variables = jsonObject(
"query" to search
fun getList(username: String): Observable<List<Track>> { )
return rest.getLib(username) val payload = jsonObject(
.map { lib -> "query" to query,
lib.flatten().map { it.toTrack() } "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 fun findLibManga(track: Track, userid: Int) : Observable<Track?> {
return getList(username) val query = """
.map { list -> list.find { it.remote_id == track.remote_id } } 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> { fun getLibManga(track: Track, userid: Int): Observable<Track> {
return findLibManga(track, username) return findLibManga(track, userid)
.map { it ?: throw Exception("Could not find manga") } .map { it ?: throw Exception("Could not find manga") }
} }
fun login(authCode: String): Observable<OAuth> { fun createOAuth(token: String): OAuth {
return restBuilder() return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
.client(client) }
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() .build()
.create(Rest::class.java) return authClient.newCall(request)
.requestAccessToken(authCode) .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>> { fun jsonToALManga(struct: JsonObject): ALManga{
return rest.getCurrentUser() return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
.map { it["id"].string to it["score_type"].int } 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() fun jsonToALUserManga(struct: JsonObject): ALUserManga{
.baseUrl(baseUrl) return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj) )
.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>>
} }
companion object { companion object {
private const val clientId = "tachiyomi-hrtje" private const val clientId = "385"
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
private const val clientUrl = "tachiyomi://anilist-auth" 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/" private const val baseMangaUrl = "https://anilist.co/manga/"
fun mangaUrl(remoteId: Int): String { fun mangaUrl(mediaId: Int): String {
return baseMangaUrl + remoteId return baseMangaUrl + mediaId
} }
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon() fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
.appendQueryParameter("grant_type", "authorization_code")
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", clientUrl) .appendQueryParameter("response_type", "token")
.appendQueryParameter("response_type", "code")
.build() .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 package eu.kanade.tachiyomi.data.track.anilist
import com.google.gson.Gson
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response 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. * OAuth object used for authenticated requests.
@ -20,24 +20,21 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
if (refreshToken.isNullOrEmpty()) { if (token.isNullOrEmpty()) {
throw Exception("Not authenticated with Anilist") throw Exception("Not authenticated with Anilist")
} }
if (oauth == null){
oauth = anilist.loadOAuth()
}
// Refresh access token if null or expired. // Refresh access token if null or expired.
if (oauth == null || oauth!!.isExpired()) { if (oauth!!.isExpired()) {
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!)) anilist.logout()
oauth = if (response.isSuccessful) { throw Exception("Token expired")
Gson().fromJson(response.body()!!.string(), OAuth::class.java)
} else {
response.close()
null
}
} }
// Throw on null auth. // Throw on null auth.
if (oauth == null) { if (oauth == null) {
throw Exception("Access token wasn't refreshed") throw Exception("No authentication token")
} }
// Add the authorization header to the original request. // Add the authorization header to the original request.
@ -53,8 +50,9 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
* and the oauth object. * and the oauth object.
*/ */
fun setAuth(oauth: OAuth?) { fun setAuth(oauth: OAuth?) {
refreshToken = oauth?.refresh_token token = oauth?.access_token
this.oauth = oauth this.oauth = oauth
anilist.saveOAuth(oauth)
} }
} }

View File

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

View File

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

View File

@ -87,7 +87,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
.flatMap { remoteTrack -> .flatMap { remoteTrack ->
if (remoteTrack != null) { if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.remote_id = remoteTrack.remote_id track.media_id = remoteTrack.media_id
update(track) update(track)
} else { } else {
track.score = DEFAULT_SCORE 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( "media" to jsonObject(
"data" to jsonObject( "data" to jsonObject(
"id" to track.remote_id, "id" to track.media_id,
"type" to "manga" "type" to "manga"
) )
) )
@ -52,7 +52,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
rest.addLibManga(jsonObject("data" to data)) rest.addLibManga(jsonObject("data" to data))
.map { json -> .map { json ->
track.remote_id = json["data"]["id"].int track.media_id = json["data"]["id"].int
track track
} }
} }
@ -63,7 +63,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
// @formatter:off // @formatter:off
val data = jsonObject( val data = jsonObject(
"type" to "libraryEntries", "type" to "libraryEntries",
"id" to track.remote_id, "id" to track.media_id,
"attributes" to jsonObject( "attributes" to jsonObject(
"status" to track.toKitsuStatus(), "status" to track.toKitsuStatus(),
"progress" to track.last_chapter_read, "progress" to track.last_chapter_read,
@ -72,7 +72,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
) )
// @formatter:on // @formatter:on
rest.updateLibManga(track.remote_id, jsonObject("data" to data)) rest.updateLibManga(track.media_id, jsonObject("data" to data))
.map { track } .map { track }
} }
} }
@ -88,7 +88,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
fun findLibManga(track: Track, userId: String): Observable<Track?> { fun findLibManga(track: Track, userId: String): Observable<Track?> {
return rest.findLibManga(track.remote_id, userId) return rest.findLibManga(track.media_id, userId)
.map { json -> .map { json ->
val data = json["data"].array val data = json["data"].array
if (data.size() > 0) { if (data.size() > 0) {
@ -101,7 +101,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
fun getLibManga(track: Track): Observable<Track> { fun getLibManga(track: Track): Observable<Track> {
return rest.getLibManga(track.remote_id) return rest.getLibManga(track.media_id)
.map { json -> .map { json ->
val data = json["data"].array val data = json["data"].array
if (data.size() > 0) { 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 @CallSuper
open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply { open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
remote_id = this@KitsuManga.id media_id = this@KitsuManga.id
title = canonicalTitle title = canonicalTitle
total_chapters = chapterCount ?: 0 total_chapters = chapterCount ?: 0
cover_url = original cover_url = original
summary = synopsis summary = synopsis
tracking_url = KitsuApi.mangaUrl(remote_id) tracking_url = KitsuApi.mangaUrl(media_id)
publishing_status = this@KitsuManga.status publishing_status = this@KitsuManga.status
publishing_type = type publishing_type = type
start_date = startDate.orEmpty() start_date = startDate.orEmpty()
@ -32,13 +32,13 @@ open class KitsuManga(obj: JsonObject) {
} }
class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) { 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 override val status by obj["attributes"].byString
val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
val progress by obj["attributes"].byInt val progress by obj["attributes"].byInt
override fun toTrack() = super.toTrack().apply { override fun toTrack() = super.toTrack().apply {
remote_id = remoteId media_id = libraryId // TODO migrate media ids to library ids
status = toTrackStatus() status = toTrackStatus()
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
last_chapter_read = progress last_chapter_read = progress

View File

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

View File

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

View File

@ -3,7 +3,10 @@ package eu.kanade.tachiyomi.network
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import okhttp3.Cache import okhttp3.Cache
import okhttp3.CipherSuite
import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.TlsVersion
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.InetAddress import java.net.InetAddress
@ -108,6 +111,18 @@ class NetworkHelper(context: Context) {
sslSocketFactory(TLSSocketFactory(), trustManagers[0] as X509TrustManager) 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 return this
} }
} }

View File

@ -49,8 +49,12 @@ class Mangachan : ParsedHttpSource() {
} }
} }
} }
is OrderBy -> { if (filter.state!!.ascending && filter.state!!.index == 0) { statusParam = false } } is OrderBy -> {
is Status -> status = arrayOf("", "all_done", "end", "ongoing", "new_ch")[filter.state] if (filter.state!!.ascending && filter.state!!.index == 0) {
statusParam = false
}
}
is Status -> status = arrayOf("", "all_done", "end", "ongoing", "new_ch")[filter.state]
} }
} }
@ -103,6 +107,7 @@ class Mangachan : ParsedHttpSource() {
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create() val manga = SManga.create()
manga.thumbnail_url = element.select("div.manga_images img").first().attr("src")
element.select("h2 > a").first().let { element.select("h2 > a").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
@ -220,32 +225,22 @@ class Mangachan : ParsedHttpSource() {
GenreList(getGenreList()) GenreList(getGenreList())
) )
// private class StatusList(status: List<Status>) : Filter.Group<Status>("Статус", status)
// private class Status(name: String, val id: String) : Filter.CheckBox(name, false)
// private fun getStatusList() = listOf(
// Status("Перевод завершен", "/all_done"),
// Status("Выпуск завершен", "/end"),
// Status("Онгоинг", "/ongoing"),
// Status("Новые главы", "/new_ch")
// )
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")]
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) => * .map(el => `Genre("${el.getAttribute('href').substr(6)}")`).join(',\n')
* { const link=el.getAttribute('href');const id=link.substr(6,link.length);
* return `Genre("${id.replace("_", " ")}")` }).join(',\n')
* on http://mangachan.me/ * on http://mangachan.me/
*/ */
private fun getGenreList() = listOf( private fun getGenreList() = listOf(
Genre("18 плюс"), Genre("18_плюс"),
Genre("bdsm"), Genre("bdsm"),
Genre("арт"), Genre("арт"),
Genre("боевик"), Genre("боевик"),
Genre("боевые искусства"), Genre("боевые_искусства"),
Genre("вампиры"), Genre("вампиры"),
Genre("веб"), Genre("веб"),
Genre("гарем"), Genre("гарем"),
Genre("гендерная интрига"), Genre("гендерная_интрига"),
Genre("героическое фэнтези"), Genre("героическое_фэнтези"),
Genre("детектив"), Genre("детектив"),
Genre("дзёсэй"), Genre("дзёсэй"),
Genre("додзинси"), Genre("додзинси"),
@ -262,13 +257,13 @@ class Mangachan : ParsedHttpSource() {
Genre("меха"), Genre("меха"),
Genre("мистика"), Genre("мистика"),
Genre("музыка"), Genre("музыка"),
Genre("научная фантастика"), Genre("научная_фантастика"),
Genre("повседневность"), Genre("повседневность"),
Genre("постапокалиптика"), Genre("постапокалиптика"),
Genre("приключения"), Genre("приключения"),
Genre("психология"), Genre("психология"),
Genre("романтика"), Genre("романтика"),
Genre("самурайский боевик"), Genre("самурайский_боевик"),
Genre("сборник"), Genre("сборник"),
Genre("сверхъестественное"), Genre("сверхъестественное"),
Genre("сказка"), Genre("сказка"),
@ -279,7 +274,6 @@ class Mangachan : ParsedHttpSource() {
Genre("сёдзё-ай"), Genre("сёдзё-ай"),
Genre("сёнэн"), Genre("сёнэн"),
Genre("сёнэн-ай"), Genre("сёнэн-ай"),
Genre("темное фэнтези"),
Genre("тентакли"), Genre("тентакли"),
Genre("трагедия"), Genre("трагедия"),
Genre("триллер"), Genre("триллер"),

View File

@ -24,23 +24,19 @@ class Mintmanga : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
add("Referer", baseUrl)
}
override fun popularMangaRequest(page: Int): Request = override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
override fun latestUpdatesRequest(page: Int): Request = override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
override fun popularMangaSelector() = "div.desc" override fun popularMangaSelector() = "div.tile"
override fun latestUpdatesSelector() = "div.desc" override fun latestUpdatesSelector() = "div.tile"
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create() val manga = SManga.create()
manga.thumbnail_url = element.select("img.lazy").first().attr("data-original")
element.select("h3 > a").first().let { element.select("h3 > a").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.attr("title") manga.title = it.attr("title")
@ -90,10 +86,15 @@ class Mintmanga : ParsedHttpSource() {
override fun chapterFromElement(element: Element): SChapter { override fun chapterFromElement(element: Element): SChapter {
val urlElement = element.select("a").first() val urlElement = element.select("a").first()
val urlText = urlElement.text()
val chapter = SChapter.create() val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1") chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mtr=1")
chapter.name = urlElement.text().replace(" новое", "") if (urlText.endsWith(" новое")) {
chapter.name = urlText.dropLast(6)
} else {
chapter.name = urlText
}
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
} ?: 0 } ?: 0
@ -143,11 +144,19 @@ class Mintmanga : ParsedHttpSource() {
override fun imageUrlParse(document: Document) = "" override fun imageUrlParse(document: Document) = ""
override fun imageRequest(page: Page): Request {
val imgHeader = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
add("Referer", baseUrl)
}.build()
return GET(page.imageUrl!!, imgHeader)
}
private class Genre(name: String, val id: String) : Filter.TriState(name) private class Genre(name: String, val id: String) : Filter.TriState(name)
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => { /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")]
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33); * .map(el => `Genre("${el.textContent.trim()}", $"{el.getAttribute('onclick')
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') * .substr(31,el.getAttribute('onclick').length-33)"})`).join(',\n')
* on http://mintmanga.com/search/advanced * on http://mintmanga.com/search/advanced
*/ */
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(

View File

@ -24,14 +24,9 @@ class Readmanga : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override fun headersBuilder() = Headers.Builder().apply { override fun popularMangaSelector() = "div.tile"
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
add("Referer", baseUrl)
}
override fun popularMangaSelector() = "div.desc" override fun latestUpdatesSelector() = "div.tile"
override fun latestUpdatesSelector() = "div.desc"
override fun popularMangaRequest(page: Int): Request = override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
@ -41,6 +36,7 @@ class Readmanga : ParsedHttpSource() {
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create() val manga = SManga.create()
manga.thumbnail_url = element.select("img.lazy").first().attr("data-original")
element.select("h3 > a").first().let { element.select("h3 > a").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.attr("title") manga.title = it.attr("title")
@ -90,10 +86,15 @@ class Readmanga : ParsedHttpSource() {
override fun chapterFromElement(element: Element): SChapter { override fun chapterFromElement(element: Element): SChapter {
val urlElement = element.select("a").first() val urlElement = element.select("a").first()
val urlText = urlElement.text()
val chapter = SChapter.create() val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1") chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mtr=1")
chapter.name = urlElement.text().replace(" новое", "") if (urlText.endsWith(" новое")) {
chapter.name = urlText.dropLast(6)
} else {
chapter.name = urlText
}
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
} ?: 0 } ?: 0
@ -143,11 +144,19 @@ class Readmanga : ParsedHttpSource() {
override fun imageUrlParse(document: Document) = "" override fun imageUrlParse(document: Document) = ""
override fun imageRequest(page: Page): Request {
val imgHeader = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
add("Referer", baseUrl)
}.build()
return GET(page.imageUrl!!, imgHeader)
}
private class Genre(name: String, val id: String) : Filter.TriState(name) private class Genre(name: String, val id: String) : Filter.TriState(name)
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => { /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")]
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33); * .map(el => `Genre("${el.textContent.trim()}", $"{el.getAttribute('onclick')
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') * .substr(31,el.getAttribute('onclick').length-33)"})`).join(',\n')
* on http://readmanga.me/search/advanced * on http://readmanga.me/search/advanced
*/ */
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(

View File

@ -1,11 +1,21 @@
package eu.kanade.tachiyomi.ui.base.presenter package eu.kanade.tachiyomi.ui.base.presenter
import android.os.Bundle
import nucleus.presenter.RxPresenter import nucleus.presenter.RxPresenter
import nucleus.presenter.delivery.Delivery import nucleus.presenter.delivery.Delivery
import rx.Observable import rx.Observable
open class BasePresenter<V> : RxPresenter<V>() { 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 * Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
* subscription list. * subscription list.

View File

@ -28,7 +28,7 @@ public class NucleusConductorDelegate<P extends Presenter> {
Bundle onSaveInstanceState() { Bundle onSaveInstanceState() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
getPresenter(); // getPresenter(); // Workaround a crash related to saving instance state with child routers
if (presenter != null) { if (presenter != null) {
presenter.save(bundle); presenter.save(bundle);
} }

View File

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

View File

@ -34,7 +34,7 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
* @param manga the manga to find. * @param manga the manga to find.
*/ */
fun indexOf(manga: Manga): Int { fun indexOf(manga: Manga): Int {
return mangas.indexOfFirst { it.manga.id == manga.id } return currentItems.indexOfFirst { it.manga.id == manga.id }
} }
fun performFilter() { fun performFilter() {

View File

@ -73,7 +73,7 @@ class LibraryController(
/** /**
* Currently selected mangas. * Currently selected mangas.
*/ */
val selectedMangas = mutableListOf<Manga>() val selectedMangas = mutableSetOf<Manga>()
private var selectedCoverManga: Manga? = null private var selectedCoverManga: Manga? = null
@ -429,11 +429,13 @@ class LibraryController(
*/ */
fun setSelection(manga: Manga, selected: Boolean) { fun setSelection(manga: Manga, selected: Boolean) {
if (selected) { if (selected) {
selectedMangas.add(manga) if (selectedMangas.add(manga)) {
selectionRelay.call(LibrarySelectionEvent.Selected(manga)) selectionRelay.call(LibrarySelectionEvent.Selected(manga))
}
} else { } else {
selectedMangas.remove(manga) if (selectedMangas.remove(manga)) {
selectionRelay.call(LibrarySelectionEvent.Unselected(manga)) selectionRelay.call(LibrarySelectionEvent.Unselected(manga))
}
} }
} }

View File

@ -8,9 +8,9 @@ import com.jakewharton.rxbinding.widget.itemClicks
import com.jakewharton.rxbinding.widget.textChanges import com.jakewharton.rxbinding.widget.textChanges
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.util.plusAssign
import kotlinx.android.synthetic.main.track_search_dialog.view.* import kotlinx.android.synthetic.main.track_search_dialog.view.*
@ -114,14 +114,14 @@ class TrackSearchDialog : DialogController {
private fun search(query: String) { private fun search(query: String) {
val view = dialogView ?: return val view = dialogView ?: return
view.progress.visibility = View.VISIBLE view.progress.visibility = View.VISIBLE
view.track_search_list.visibility = View.GONE view.track_search_list.visibility = View.INVISIBLE
trackController.presenter.search(query, service) trackController.presenter.search(query, service)
} }
fun onSearchResults(results: List<TrackSearch>) { fun onSearchResults(results: List<TrackSearch>) {
selectedItem = null selectedItem = null
val view = dialogView ?: return val view = dialogView ?: return
view.progress.visibility = View.GONE view.progress.visibility = View.INVISIBLE
view.track_search_list.visibility = View.VISIBLE view.track_search_list.visibility = View.VISIBLE
adapter?.setItems(results) adapter?.setItems(results)
} }
@ -129,7 +129,7 @@ class TrackSearchDialog : DialogController {
fun onSearchResultsError() { fun onSearchResultsError() {
val view = dialogView ?: return val view = dialogView ?: return
view.progress.visibility = View.VISIBLE view.progress.visibility = View.VISIBLE
view.track_search_list.visibility = View.GONE view.track_search_list.visibility = View.INVISIBLE
adapter?.setItems(emptyList()) adapter?.setItems(emptyList())
} }
@ -141,4 +141,4 @@ class TrackSearchDialog : DialogController {
const val KEY_SERVICE = "service_id" const val KEY_SERVICE = "service_id"
} }
} }

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.util.plusAssign
import eu.kanade.tachiyomi.util.visibleIf
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
import kotlinx.android.synthetic.main.reader_settings_dialog.view.* import kotlinx.android.synthetic.main.reader_settings_dialog.view.*
import rx.Observable import rx.Observable
@ -91,6 +92,23 @@ class ReaderSettingsDialog : DialogFragment() {
crop_borders.setOnCheckedChangeListener { _, isChecked -> crop_borders.setOnCheckedChangeListener { _, isChecked ->
preferences.cropBorders().set(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() { override fun onDestroyView() {
@ -98,4 +116,4 @@ class ReaderSettingsDialog : DialogFragment() {
super.onDestroyView() super.onDestroyView()
} }
} }

View File

@ -4,7 +4,12 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.util.DisplayMetrics 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.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -123,7 +128,7 @@ class WebtoonReader : BaseReader() {
.distinctUntilChanged() .distinctUntilChanged()
.subscribe { refreshAdapter() }) .subscribe { refreshAdapter() })
subscriptions.add(readerActivity.preferences.cropBorders() subscriptions.add(readerActivity.preferences.cropBordersWebtoon()
.asObservable() .asObservable()
.doOnNext { cropBorders = it } .doOnNext { cropBorders = it }
.skip(1) .skip(1)

View File

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

View File

@ -22,7 +22,8 @@ import timber.log.Timber
import java.text.DateFormat import java.text.DateFormat
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Locale
import java.util.TimeZone
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
@ -71,7 +72,16 @@ class SettingsAboutController : SettingsController() {
} }
preference { preference {
title = "Discord" 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 summary = url
onClick { onClick {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))

View File

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

View File

@ -76,8 +76,8 @@ class SettingsReaderController : SettingsController() {
defaultValue = true defaultValue = true
} }
switchPreference { switchPreference {
key = Keys.enableTransitions key = Keys.keepScreenOn
titleRes = R.string.pref_page_transitions titleRes = R.string.pref_keep_screen_on
defaultValue = true defaultValue = true
} }
switchPreference { switchPreference {
@ -85,15 +85,28 @@ class SettingsReaderController : SettingsController() {
titleRes = R.string.pref_show_page_number titleRes = R.string.pref_show_page_number
defaultValue = true defaultValue = true
} }
switchPreference { preferenceCategory {
key = Keys.cropBorders titleRes = R.string.pager_viewer
titleRes = R.string.pref_crop_borders
defaultValue = false 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 { preferenceCategory {
key = Keys.keepScreenOn titleRes = R.string.webtoon_viewer
titleRes = R.string.pref_keep_screen_on
defaultValue = true switchPreference {
key = Keys.cropBordersWebtoon
titleRes = R.string.pref_crop_borders
defaultValue = false
}
} }
preferenceCategory { preferenceCategory {
titleRes = R.string.pref_reader_navigation titleRes = R.string.pref_reader_navigation
@ -116,4 +129,4 @@ class SettingsReaderController : SettingsController() {
} }
} }
} }

View File

@ -47,6 +47,10 @@ inline fun View.gone() {
visibility = 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 * Returns a TextDrawable determined by input
* *
@ -63,4 +67,4 @@ fun View.getRound(text: String, random : Boolean = true): TextDrawable {
.useFont(Typeface.DEFAULT) .useFont(Typeface.DEFAULT)
.endConfig() .endConfig()
.buildRound(text, if (random) ColorGenerator.MATERIAL.randomColor else ColorGenerator.MATERIAL.getColor(text)) .buildRound(text, if (random) ColorGenerator.MATERIAL.randomColor else ColorGenerator.MATERIAL.getColor(text))
} }

View File

@ -171,10 +171,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/pref_crop_borders"/> android:text="@string/pref_crop_borders"/>
<android.support.v7.widget.SwitchCompat
android:id="@+id/crop_borders_webtoon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_crop_borders"/>
<android.support.v7.widget.SwitchCompat <android.support.v7.widget.SwitchCompat
android:id="@+id/fullscreen" android:id="@+id/fullscreen"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/pref_fullscreen"/> android:text="@string/pref_fullscreen"/>
</LinearLayout> </LinearLayout>

View File

@ -1,10 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<EditText <EditText
@ -14,52 +13,49 @@
android:hint="@string/title" android:hint="@string/title"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:maxLines="1" android:inputType="text"
app:layout_constraintTop_toTopOf="parent"/> android:maxLines="1"/>
<ProgressBar <FrameLayout
android:id="@+id/progress"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:layout_marginTop="32dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/divider1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/track_search"
tools:visibility="visible"/>
<ListView
android:id="@+id/track_search_list"
style="@style/Theme.Widget.CardView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_marginTop="40dp" android:layout_weight="1">
android:choiceMode="singleChoice"
android:clipToPadding="false" <ProgressBar
android:divider="@null" android:id="@+id/progress"
android:dividerHeight="10dp" style="?android:attr/progressBarStyle"
android:footerDividersEnabled="true" android:layout_width="wrap_content"
android:headerDividersEnabled="true" android:layout_height="wrap_content"
android:listSelector="?attr/selectable_list_drawable" android:layout_marginBottom="32dp"
android:paddingBottom="4dp" android:layout_marginTop="32dp"
android:paddingTop="4dp" android:layout_gravity="center"
android:scrollbars="none" android:visibility="invisible"
android:visibility="gone" tools:visibility="visible"/>
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" <ListView
app:layout_constraintTop_toTopOf="@+id/track_search" android:id="@+id/track_search_list"
tools:listitem="@layout/track_search_item" style="@style/Theme.Widget.CardView"
tools:visibility="visible"/> android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:choiceMode="singleChoice"
android:divider="@null"
android:dividerHeight="10dp"
android:footerDividersEnabled="true"
android:headerDividersEnabled="true"
android:listSelector="?attr/selectable_list_drawable"
android:paddingBottom="4dp"
android:paddingTop="4dp"
android:scrollbars="none"
android:visibility="invisible"
tools:listitem="@layout/track_search_item"
tools:visibility="visible"/>
</FrameLayout>
<View <View
android:id="@+id/divider1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:background="?android:attr/divider" android:background="?android:attr/divider"/>
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/track_search_list"/>
</android.support.constraint.ConstraintLayout> </LinearLayout>

View File

@ -12,7 +12,6 @@
android:layout_height="216dp" android:layout_height="216dp"
android:background="?attr/selectable_list_drawable" android:background="?attr/selectable_list_drawable"
android:orientation="horizontal"> android:orientation="horizontal">
>
<ImageView <ImageView
android:id="@+id/track_search_cover" android:id="@+id/track_search_cover"
@ -161,4 +160,4 @@
app:layout_constraintGuide_begin="150dp"/> app:layout_constraintGuide_begin="150dp"/>
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView> </android.support.v7.widget.CardView>

View File

@ -1,5 +1,31 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<changelog bulletedList="true"> <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>
<changelogtext>Separate english language into american and british so that dates are formatted according to that locale.</changelogtext>
<changelogtext>Added Firebase analytics, for Android API distribution.</changelogtext>
<changelogtext>Crop borders for webtoons now has a separate setting.</changelogtext>
<changelogtext>The downloader now runs in a foreground service to prevent it from being killed.</changelogtext>
<changelogtext>Fixed a few weird crashes.</changelogtext>
</changelogversion>
<changelogversion versionName="v0.7.2" changeDate=""> <changelogversion versionName="v0.7.2" changeDate="">
<changelogtext>Fixed missing downloaded label in chapters screen.</changelogtext> <changelogtext>Fixed missing downloaded label in chapters screen.</changelogtext>
@ -183,54 +209,4 @@
<changelogtext>Fixed lost covers on some devices.</changelogtext> <changelogtext>Fixed lost covers on some devices.</changelogtext>
</changelogversion> </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> </changelog>

View File

@ -11,7 +11,7 @@
<string name="label_recent_updates">Nouveautés bibliothèque</string> <string name="label_recent_updates">Nouveautés bibliothèque</string>
<string name="label_latest_updates">Dernières nouveautés</string> <string name="label_latest_updates">Dernières nouveautés</string>
<string name="label_categories">Catégories</string> <string name="label_categories">Catégories</string>
<string name="label_selected">Sélectionné: %1$d</string> <string name="label_selected">Sélectionné : %1$d</string>
<string name="label_backup">Sauvegarde</string> <string name="label_backup">Sauvegarde</string>
<!-- Actions --> <!-- Actions -->
@ -57,7 +57,7 @@
<string name="action_retry">Réessayer</string> <string name="action_retry">Réessayer</string>
<string name="action_remove">Supprimer</string> <string name="action_remove">Supprimer</string>
<string name="action_resume">Reprendre</string> <string name="action_resume">Reprendre</string>
<string name="action_open_in_browser">Ouvre dans le navigateur</string> <string name="action_open_in_browser">Ouvrir dans le navigateur</string>
<string name="action_add_to_home_screen">Ajouter à l\'écran d\'accueil</string> <string name="action_add_to_home_screen">Ajouter à l\'écran d\'accueil</string>
<string name="action_display_mode">Changer le mode d\'affichage</string> <string name="action_display_mode">Changer le mode d\'affichage</string>
<string name="action_display">Affichage</string> <string name="action_display">Affichage</string>
@ -152,9 +152,9 @@
<string name="rotation_force_portrait">Forcer portrait</string> <string name="rotation_force_portrait">Forcer portrait</string>
<string name="rotation_force_landscape">Forcer paysage</string> <string name="rotation_force_landscape">Forcer paysage</string>
<string name="color_filter_r_value">R</string> <string name="color_filter_r_value">R</string>
<string name="color_filter_g_value">G</string> <string name="color_filter_g_value">V</string>
<string name="color_filter_b_value">B</string> <string name="color_filter_b_value">B</string>
<string name="color_filter_a_value">A</string> <string name="color_filter_a_value">O</string>
<!-- Downloads section --> <!-- Downloads section -->
@ -164,7 +164,7 @@
<string name="pref_remove_after_read">Supprimer après avoir lu</string> <string name="pref_remove_after_read">Supprimer après avoir lu</string>
<string name="disabled">Désactivé</string> <string name="disabled">Désactivé</string>
<string name="last_read_chapter">Dernier chapitre lu</string> <string name="last_read_chapter">Dernier chapitre lu</string>
<string name="second_to_last">Avant-dernier chapiture lu</string> <string name="second_to_last">Avant-dernier chapitre lu</string>
<string name="third_to_last">Du troisième au dernier chapitre</string> <string name="third_to_last">Du troisième au dernier chapitre</string>
<string name="fourth_to_last">Du quatrième au dernier chapitre</string> <string name="fourth_to_last">Du quatrième au dernier chapitre</string>
<string name="pref_download_new">Télécharger les nouveaux chapitres</string> <string name="pref_download_new">Télécharger les nouveaux chapitres</string>
@ -286,7 +286,7 @@
<!-- Reader activity --> <!-- Reader activity -->
<string name="custom_filter">Filtre personnalisé</string> <string name="custom_filter">Filtre personnalisé</string>
<string name="set_as_cover">Mettre comme couverture</string> <string name="set_as_cover">Ajouter comme couverture</string>
<string name="cover_updated">Couverture mise à jour</string> <string name="cover_updated">Couverture mise à jour</string>
<string name="page_downloaded">Page copiée vers %1$s</string> <string name="page_downloaded">Page copiée vers %1$s</string>
<string name="downloading">En cours de téléchargement…</string> <string name="downloading">En cours de téléchargement…</string>
@ -313,7 +313,7 @@
<string name="notification_update_progress">Progression mise à jour: %1$d/%2$d</string> <string name="notification_update_progress">Progression mise à jour: %1$d/%2$d</string>
<string name="notification_new_chapters">Des nouveaux chapitres ont été trouvés</string> <string name="notification_new_chapters">Des nouveaux chapitres ont été trouvés</string>
<string name="notification_cover_update_failed">La mise à jour de la couverture a échoué</string> <string name="notification_cover_update_failed">La mise à jour de la couverture a échoué</string>
<string name="notification_first_add_to_library">Veuillez ajouter le manga dans votre bibliothèque avant de faire cela</string> <string name="notification_first_add_to_library">Veuillez ajouter le manga dans votre bibliothèque avant</string>
<string name="notification_not_connected_to_ac_body">Non branché</string> <string name="notification_not_connected_to_ac_body">Non branché</string>
<string name="notification_no_connection_title">Synchronisation annulée</string> <string name="notification_no_connection_title">Synchronisation annulée</string>
<string name="notification_no_connection_body">Connexion non disponible</string> <string name="notification_no_connection_body">Connexion non disponible</string>
@ -324,15 +324,15 @@
<string name="file_select_icon">Sélectionner une icône de raccourci</string> <string name="file_select_icon">Sélectionner une icône de raccourci</string>
<!--UpdateCheck--> <!--UpdateCheck-->
<string name="update_check_title">Nouvelle mise à jour disponible!</string> <string name="update_check_title">Nouvelle mise à jour disponible !</string>
<string name="update_check_confirm">Télécharger</string> <string name="update_check_confirm">Télécharger</string>
<string name="update_check_ignore">Ignorer</string> <string name="update_check_ignore">Ignorer</string>
<string name="update_check_no_new_updates">Aucun mise à jour disponible</string> <string name="update_check_no_new_updates">Aucune mise à jour disponible</string>
<string name="update_check_download_started">Téléchargement commencé</string> <string name="update_check_download_started">Téléchargement commencé</string>
<string name="update_check_look_for_updates">Vérification des mises à jour</string> <string name="update_check_look_for_updates">Vérification des mises à jour</string>
<!--UpdateCheck Notifications--> <!--UpdateCheck Notifications-->
<string name="update_check_notification_file_download">Télécharger mise à jour</string> <string name="update_check_notification_file_download">Télécharger la mise à jour</string>
<string name="update_check_notification_download_in_progress">Téléchargement en cours</string> <string name="update_check_notification_download_in_progress">Téléchargement en cours</string>
<string name="update_check_notification_download_complete">Téléchargement terminé</string> <string name="update_check_notification_download_complete">Téléchargement terminé</string>
<string name="update_check_notification_download_error">Erreur lors du téléchargement</string> <string name="update_check_notification_download_error">Erreur lors du téléchargement</string>
@ -400,12 +400,12 @@
<string name="pref_backup_directory">Dossier de sauvegarde</string> <string name="pref_backup_directory">Dossier de sauvegarde</string>
<string name="pref_backup_service_category">Service</string> <string name="pref_backup_service_category">Service</string>
<string name="source_not_found">Source introuvable</string> <string name="source_not_found">Source introuvable</string>
<string name="restore_completed">Restauration terminé</string> <string name="restore_completed">Restauration terminée</string>
<string name="error_opening_log">Impossible d\'ouvrir le fichier journal</string> <string name="error_opening_log">Impossible d\'ouvrir le fichier journal</string>
<string name="file_saved">Fichier enregistré dans %1$s</string> <string name="file_saved">Fichier enregistré dans %1$s</string>
<string name="backup_choice">Que voulez-vous sauvegarder?</string> <string name="backup_choice">Que voulez-vous sauvegarder ?</string>
<string name="invalid_combination">Le défaut ne peut pas être selectionné avec des autres catégories</string> <string name="invalid_combination">Le défaut ne peut pas être selectionné avec des autres catégories</string>
<string name="delete_downloads_for_manga">Supprimer les chapitres téléchargés?</string> <string name="delete_downloads_for_manga">Supprimer les chapitres téléchargés ?</string>
<string name="notification_new_chapters_text">Pour %1$s chapitres</string> <string name="notification_new_chapters_text">Pour %1$s chapitres</string>
<string name="track">Suivi</string> <string name="track">Suivi</string>
@ -439,7 +439,7 @@
<string name="pref_read_with_volume_keys_inverted">Inverse les boutons de volume</string> <string name="pref_read_with_volume_keys_inverted">Inverse les boutons de volume</string>
<string name="pref_crop_borders">Rogner les bordures</string> <string name="pref_crop_borders">Rogner les bordures</string>
<string name="backup_restore_content">La restauration utilise le source pour obtenir des données, des frais de l\'opérateur peuvent s\'appliquer. <string name="backup_restore_content">La restauration utilise la source pour obtenir des données, des frais de l\'opérateur peuvent s\'appliquer.
Assurez-vous que vous êtes connecté à des sources qui le demande avant de commencer la restauration.</string> Assurez-vous que vous êtes connecté à des sources qui le demande avant de commencer la restauration.</string>
<string name="action_global_search">Recherche globale</string> <string name="action_global_search">Recherche globale</string>
<string name="action_open">Ouvrir</string> <string name="action_open">Ouvrir</string>
@ -447,7 +447,7 @@ Assurez-vous que vous êtes connecté à des sources qui le demande avant de com
<string name="other_source">Autre</string> <string name="other_source">Autre</string>
<string name="action_global_search_hint">Recherche globale…</string> <string name="action_global_search_hint">Recherche globale…</string>
<string name="no_results">Aucun résultat!</string> <string name="no_results">Aucun résultat !</string>
<string name="latest">Récents</string> <string name="latest">Récents</string>
<string name="browse">Explorer</string> <string name="browse">Explorer</string>

View File

@ -190,6 +190,7 @@
<string name="right_to_left_viewer">Right to left</string> <string name="right_to_left_viewer">Right to left</string>
<string name="vertical_viewer">Vertical</string> <string name="vertical_viewer">Vertical</string>
<string name="webtoon_viewer">Webtoon</string> <string name="webtoon_viewer">Webtoon</string>
<string name="pager_viewer">Pager</string>
<string name="pref_image_decoder">Image decoder</string> <string name="pref_image_decoder">Image decoder</string>
<string name="pref_image_scale_type">Scale type</string> <string name="pref_image_scale_type">Scale type</string>
<string name="scale_type_fit_screen">Fit screen</string> <string name="scale_type_fit_screen">Fit screen</string>
@ -384,6 +385,7 @@
<string name="dropped">Dropped</string> <string name="dropped">Dropped</string>
<string name="on_hold">On hold</string> <string name="on_hold">On hold</string>
<string name="plan_to_read">Plan to read</string> <string name="plan_to_read">Plan to read</string>
<string name="repeating">Re-reading</string>
<string name="score">Score</string> <string name="score">Score</string>
<string name="title">Title</string> <string name="title">Title</string>
<string name="status">Status</string> <string name="status">Status</string>

View File

@ -7,9 +7,10 @@ buildscript {
google() google()
} }
dependencies { 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.ben-manes:gradle-versions-plugin:0.17.0'
classpath 'com.github.zellius:android-shortcut-gradle-plugin:0.1.2' 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 // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }