mirror of
https://github.com/mihonapp/mihon.git
synced 2025-07-27 01:45:53 +02:00
Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
9d5cf9163a | |||
9abce0cca3 | |||
c6245f4fa3 | |||
75fc160204 | |||
263198dd89 | |||
345f96055d | |||
51144aa45e | |||
86a599d13f | |||
0cf81e6f7a | |||
8874fe973c | |||
f8a03226ee | |||
32db1e3045 | |||
303e6c0102 | |||
18883f1ba3 | |||
5c31271e91 | |||
00981cf4e8 | |||
968f4a69e8 | |||
e7e1a9bf50 | |||
fe1becb001 | |||
7789171c71 | |||
3fd2222c99 | |||
6de36a88c0 | |||
b37685542d | |||
a2b1b9e746 | |||
8017324033 | |||
7464497c88 | |||
499def3daa | |||
6931b75cc5 | |||
f853610578 | |||
69f51b88bf | |||
e0d680201a | |||
1566b8f8b8 | |||
4bbf78e840 | |||
7ab16a69df | |||
95e60ed775 | |||
d38cd2547a | |||
2159b72e69 | |||
81c23bbf9d |
.github
.travis.yml.travis
README.mdapp
.gitignorebuild.gradle
build.gradlesrc
main
java
eu
kanade
tachiyomi
data
backup
database
download
preference
track
anilist
kitsu
model
myanimelist
updater
network
source
online
ui
base
catalogue
library
manga
reader
setting
util
res
gradle/wrapper
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@ -1,5 +1,5 @@
|
||||
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||
2. If you are unsure, ask here: [](https://discord.gg/WrBkRk4)
|
||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
||||
3. What is your type of issue?
|
||||
* [Catalogue request](#catalogue-requests)
|
||||
* [Bugs](#bugs)
|
||||
|
@ -1,7 +1,7 @@
|
||||
language: android
|
||||
android:
|
||||
components:
|
||||
- build-tools-27.0.2
|
||||
- build-tools-27.0.3
|
||||
- android-27
|
||||
- extra-android-m2repository
|
||||
- extra-google-m2repository
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
git fetch --unshallow #required for commit count
|
||||
|
||||
cp .travis/google-services.json app/
|
||||
|
||||
if [ -z "$TRAVIS_TAG" ]; then
|
||||
./gradlew clean assembleStandardDebug
|
||||
|
||||
|
73
.travis/google-services.json
Normal file
73
.travis/google-services.json
Normal 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"
|
||||
}
|
14
README.md
14
README.md
@ -1,6 +1,6 @@
|
||||
| Build | Download | F-Droid | Contribute | Contact |
|
||||
| Build | Stable | Dev | Contribute | Contact |
|
||||
|-------|----------|---------|------------|---------|
|
||||
| [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | [](https://github.com/inorichi/tachiyomi/wiki/Translation) | [](https://discord.gg/2dDQBv2) |
|
||||
| [](https://travis-ci.org/inorichi/tachiyomi) | [)](https://github.com/inorichi/tachiyomi/releases) | [](http://tachiyomi.kanade.eu/latest) [](//github.com/inorichi/tachiyomi/wiki/F-Droid-for-dev-versions) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
||||
|
||||
|
||||
# Tachiyomi
|
||||
@ -14,16 +14,16 @@ Features include:
|
||||
* Online reading from sources such as KissManga, MangaFox, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
||||
* Local reading of downloaded manga
|
||||
* Configurable reader with multiple viewers, reading directions and other settings
|
||||
* MyAnimeList, AniList, and Kitsu support
|
||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), and [Kitsu](https://kitsu.io/explore/anime) support
|
||||
* Categories to organize your library
|
||||
* Light and dark themes
|
||||
* Schedule updating your library for new chapters
|
||||
* Create backups locally or to your cloud service of choice
|
||||
* Create backups locally to read offline or to your desired cloud service
|
||||
|
||||
## Download
|
||||
Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases) 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
|
||||
|
||||
@ -32,7 +32,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
||||
<details><summary>Issues</summary>
|
||||
|
||||
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||
2. If you are unsure, ask here: [](https://discord.gg/WrBkRk4)
|
||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
||||
|
||||
</details>
|
||||
|
||||
|
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
*iml
|
||||
*.iml
|
||||
custom.gradle
|
||||
google-services.json
|
||||
|
@ -30,7 +30,7 @@ ext {
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion "27.0.2"
|
||||
buildToolsVersion '27.0.3'
|
||||
publishNonDefault true
|
||||
|
||||
defaultConfig {
|
||||
@ -38,8 +38,8 @@ android {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 27
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
versionCode 33
|
||||
versionName "0.7.0"
|
||||
versionCode 37
|
||||
versionName "0.7.4"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
@ -116,10 +116,12 @@ dependencies {
|
||||
implementation "com.android.support:support-annotations:$support_library_version"
|
||||
implementation "com.android.support:customtabs:$support_library_version"
|
||||
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.0-beta6'
|
||||
|
||||
implementation 'com.android.support:multidex:1.0.2'
|
||||
|
||||
standardImplementation 'com.google.firebase:firebase-core:11.8.0'
|
||||
|
||||
// ReactiveX
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
implementation 'io.reactivex:rxjava:1.3.6'
|
||||
@ -128,7 +130,7 @@ dependencies {
|
||||
implementation 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||
|
||||
// Network client
|
||||
implementation "com.squareup.okhttp3:okhttp:3.9.1"
|
||||
implementation "com.squareup.okhttp3:okhttp:3.10.0"
|
||||
implementation 'com.squareup.okio:okio:1.14.0'
|
||||
|
||||
// REST
|
||||
@ -152,7 +154,7 @@ dependencies {
|
||||
implementation 'org.jsoup:jsoup:1.10.2'
|
||||
|
||||
// Job scheduling
|
||||
implementation 'com.evernote:android-job:1.2.4'
|
||||
implementation 'com.evernote:android-job:1.2.5'
|
||||
implementation 'com.google.android.gms:play-services-gcm:11.8.0'
|
||||
|
||||
// Changelog
|
||||
@ -201,7 +203,7 @@ dependencies {
|
||||
implementation 'me.gujun.android.taggroup:library:1.4@aar'
|
||||
|
||||
// Conductor
|
||||
implementation "com.github.inorichi.Conductor:conductor:05c4d4d"
|
||||
implementation "com.github.inorichi.Conductor:conductor:be8b3c5"
|
||||
implementation ("com.bluelinelabs:conductor-support:2.1.5-SNAPSHOT") {
|
||||
exclude group: "com.bluelinelabs", module: "conductor"
|
||||
}
|
||||
@ -254,3 +256,7 @@ kotlin {
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
|
@ -402,8 +402,11 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.sync_id == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
if (track.remote_id != dbTrack.remote_id) {
|
||||
dbTrack.remote_id = track.remote_id
|
||||
if (track.media_id != dbTrack.media_id) {
|
||||
dbTrack.media_id = track.media_id
|
||||
}
|
||||
if (track.library_id != dbTrack.library_id) {
|
||||
dbTrack.library_id = track.library_id
|
||||
}
|
||||
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||
isInDatabase = true
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
|
||||
import android.telecom.DisconnectCause.REMOTE
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonToken
|
||||
@ -11,7 +12,8 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
object TrackTypeAdapter {
|
||||
|
||||
private const val SYNC = "s"
|
||||
private const val REMOTE = "r"
|
||||
private const val MEDIA = "r"
|
||||
private const val LIBRARY = "ml"
|
||||
private const val TITLE = "t"
|
||||
private const val LAST_READ = "l"
|
||||
private const val TRACKING_URL = "u"
|
||||
@ -24,8 +26,10 @@ object TrackTypeAdapter {
|
||||
value(it.title)
|
||||
name(SYNC)
|
||||
value(it.sync_id)
|
||||
name(REMOTE)
|
||||
value(it.remote_id)
|
||||
name(MEDIA)
|
||||
value(it.media_id)
|
||||
name(LIBRARY)
|
||||
value(it.library_id)
|
||||
name(LAST_READ)
|
||||
value(it.last_chapter_read)
|
||||
name(TRACKING_URL)
|
||||
@ -43,7 +47,8 @@ object TrackTypeAdapter {
|
||||
when (name) {
|
||||
TITLE -> track.title = nextString()
|
||||
SYNC -> track.sync_id = nextInt()
|
||||
REMOTE -> track.remote_id = nextInt()
|
||||
MEDIA -> track.media_id = nextInt()
|
||||
LIBRARY -> track.library_id = nextLong()
|
||||
LAST_READ -> track.last_chapter_read = nextInt()
|
||||
TRACKING_URL -> track.tracking_url = nextString()
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
|
||||
/**
|
||||
* Version of the database.
|
||||
*/
|
||||
const val DATABASE_VERSION = 6
|
||||
const val DATABASE_VERSION = 7
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||
@ -57,6 +57,9 @@ class DbOpenHelper(context: Context)
|
||||
if (oldVersion < 6) {
|
||||
db.execSQL(TrackTable.addTrackingUrl)
|
||||
}
|
||||
if (oldVersion < 7) {
|
||||
db.execSQL(TrackTable.addLibraryId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigure(db: SQLiteDatabase) {
|
||||
|
@ -13,8 +13,9 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LIBRARY_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_REMOTE_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MEDIA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
|
||||
@ -45,7 +46,8 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_MANGA_ID, obj.manga_id)
|
||||
put(COL_SYNC_ID, obj.sync_id)
|
||||
put(COL_REMOTE_ID, obj.remote_id)
|
||||
put(COL_MEDIA_ID, obj.media_id)
|
||||
put(COL_LIBRARY_ID, obj.library_id)
|
||||
put(COL_TITLE, obj.title)
|
||||
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
|
||||
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
|
||||
@ -62,7 +64,8 @@ class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
||||
remote_id = cursor.getInt(cursor.getColumnIndex(COL_REMOTE_ID))
|
||||
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
|
||||
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
|
||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
||||
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
||||
|
@ -10,7 +10,9 @@ interface Track : Serializable {
|
||||
|
||||
var sync_id: Int
|
||||
|
||||
var remote_id: Int
|
||||
var media_id: Int
|
||||
|
||||
var library_id: Long?
|
||||
|
||||
var title: String
|
||||
|
||||
|
@ -8,7 +8,9 @@ class TrackImpl : Track {
|
||||
|
||||
override var sync_id: Int = 0
|
||||
|
||||
override var remote_id: Int = 0
|
||||
override var media_id: Int = 0
|
||||
|
||||
override var library_id: Long? = null
|
||||
|
||||
override lateinit var title: String
|
||||
|
||||
@ -30,13 +32,13 @@ class TrackImpl : Track {
|
||||
|
||||
if (manga_id != other.manga_id) return false
|
||||
if (sync_id != other.sync_id) return false
|
||||
return remote_id == other.remote_id
|
||||
return media_id == other.media_id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
||||
result = 31 * result + sync_id
|
||||
result = 31 * result + remote_id
|
||||
result = 31 * result + media_id
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,9 @@ object TrackTable {
|
||||
|
||||
const val COL_SYNC_ID = "sync_id"
|
||||
|
||||
const val COL_REMOTE_ID = "remote_id"
|
||||
const val COL_MEDIA_ID = "remote_id"
|
||||
|
||||
const val COL_LIBRARY_ID = "library_id"
|
||||
|
||||
const val COL_TITLE = "title"
|
||||
|
||||
@ -29,7 +31,8 @@ object TrackTable {
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_SYNC_ID INTEGER NOT NULL,
|
||||
$COL_REMOTE_ID INTEGER NOT NULL,
|
||||
$COL_MEDIA_ID INTEGER NOT NULL,
|
||||
$COL_LIBRARY_ID INTEGER,
|
||||
$COL_TITLE TEXT NOT NULL,
|
||||
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
|
||||
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
||||
@ -43,4 +46,7 @@ object TrackTable {
|
||||
|
||||
val addTrackingUrl: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_TRACKING_URL TEXT DEFAULT ''"
|
||||
|
||||
val addLibraryId: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL"
|
||||
}
|
||||
|
@ -1,16 +1,20 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.NetworkInfo.State.CONNECTED
|
||||
import android.net.NetworkInfo.State.DISCONNECTED
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import com.github.pwittchen.reactivenetwork.library.Connectivity
|
||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.connectivityManager
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
@ -41,7 +45,12 @@ class DownloadService : Service() {
|
||||
* @param context the application context.
|
||||
*/
|
||||
fun start(context: Context) {
|
||||
context.startService(Intent(context, DownloadService::class.java))
|
||||
val intent = Intent(context, DownloadService::class.java)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
context.startService(intent)
|
||||
} else {
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,6 +90,7 @@ class DownloadService : Service() {
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
|
||||
runningRelay.call(true)
|
||||
subscriptions = CompositeSubscription()
|
||||
listenDownloaderState()
|
||||
@ -176,4 +186,10 @@ class DownloadService : Service() {
|
||||
if (!isHeld) acquire()
|
||||
}
|
||||
|
||||
private fun getPlaceholderNotification(): Notification {
|
||||
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER)
|
||||
.setContentTitle(getString(R.string.download_notifier_downloader_title))
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -39,6 +39,8 @@ object PreferenceKeys {
|
||||
|
||||
const val cropBorders = "crop_borders"
|
||||
|
||||
const val cropBordersWebtoon = "crop_borders_webtoon"
|
||||
|
||||
const val readWithTapping = "reader_tap"
|
||||
|
||||
const val readWithVolumeKeys = "reader_volume_keys"
|
||||
|
@ -67,6 +67,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun cropBorders() = rxPrefs.getBoolean(Keys.cropBorders, false)
|
||||
|
||||
fun cropBordersWebtoon() = rxPrefs.getBoolean(Keys.cropBordersWebtoon, false)
|
||||
|
||||
fun readWithTapping() = rxPrefs.getBoolean(Keys.readWithTapping, true)
|
||||
|
||||
fun readWithVolumeKeys() = rxPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
||||
@ -117,7 +119,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "")
|
||||
|
||||
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
|
||||
fun anilistScoreType() = rxPrefs.getString("anilist_score_type", "POINT_10")
|
||||
|
||||
fun backupsDirectory() = rxPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
||||
|
||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
@ -9,6 +10,7 @@ import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
@ -17,24 +19,45 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
const val COMPLETED = 2
|
||||
const val ON_HOLD = 3
|
||||
const val DROPPED = 4
|
||||
const val PLAN_TO_READ = 5
|
||||
const val PLANNING = 5
|
||||
const val REPEATING = 6
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
|
||||
const val POINT_100 = "POINT_100"
|
||||
const val POINT_10 = "POINT_10"
|
||||
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
|
||||
const val POINT_5 = "POINT_5"
|
||||
const val POINT_3 = "POINT_3"
|
||||
}
|
||||
|
||||
override val name = "AniList"
|
||||
|
||||
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
|
||||
private val gson: Gson by injectLazy()
|
||||
|
||||
private val interceptor by lazy { AnilistInterceptor(this, getPassword()) }
|
||||
|
||||
private val api by lazy { AnilistApi(client, interceptor) }
|
||||
|
||||
private val scorePreference = preferences.anilistScoreType()
|
||||
|
||||
init {
|
||||
// If the preference is an int from APIv1, logout user to force using APIv2
|
||||
try {
|
||||
scorePreference.get()
|
||||
} catch (e: ClassCastException) {
|
||||
logout()
|
||||
scorePreference.delete()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLogo() = R.drawable.al
|
||||
|
||||
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
||||
|
||||
override fun getStatusList(): List<Int> {
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING)
|
||||
}
|
||||
|
||||
override fun getStatus(status: Int): String = with(context) {
|
||||
@ -43,48 +66,50 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
COMPLETED -> getString(R.string.completed)
|
||||
ON_HOLD -> getString(R.string.on_hold)
|
||||
DROPPED -> getString(R.string.dropped)
|
||||
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||
PLANNING -> getString(R.string.plan_to_read)
|
||||
REPEATING -> getString(R.string.repeating)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun getScoreList(): List<String> {
|
||||
return when (preferences.anilistScoreType().getOrDefault()) {
|
||||
return when (scorePreference.getOrDefault()) {
|
||||
// 10 point
|
||||
0 -> IntRange(0, 10).map(Int::toString)
|
||||
POINT_10 -> IntRange(0, 10).map(Int::toString)
|
||||
// 100 point
|
||||
1 -> IntRange(0, 100).map(Int::toString)
|
||||
POINT_100 -> IntRange(0, 100).map(Int::toString)
|
||||
// 5 stars
|
||||
2 -> IntRange(0, 5).map { "$it ★" }
|
||||
POINT_5 -> IntRange(0, 5).map { "$it ★" }
|
||||
// Smiley
|
||||
3 -> listOf("-", "😦", "😐", "😊")
|
||||
POINT_3 -> listOf("-", "😦", "😐", "😊")
|
||||
// 10 point decimal
|
||||
4 -> IntRange(0, 100).map { (it / 10f).toString() }
|
||||
POINT_10_DECIMAL -> IntRange(0, 100).map { (it / 10f).toString() }
|
||||
else -> throw Exception("Unknown score type")
|
||||
}
|
||||
}
|
||||
|
||||
override fun indexToScore(index: Int): Float {
|
||||
return when (preferences.anilistScoreType().getOrDefault()) {
|
||||
return when (scorePreference.getOrDefault()) {
|
||||
// 10 point
|
||||
0 -> index * 10f
|
||||
POINT_10 -> index * 10f
|
||||
// 100 point
|
||||
1 -> index.toFloat()
|
||||
POINT_100 -> index.toFloat()
|
||||
// 5 stars
|
||||
2 -> index * 20f
|
||||
POINT_5 -> index * 20f
|
||||
// Smiley
|
||||
3 -> index * 30f
|
||||
POINT_3 -> index * 30f
|
||||
// 10 point decimal
|
||||
4 -> index.toFloat()
|
||||
POINT_10_DECIMAL -> index.toFloat()
|
||||
else -> throw Exception("Unknown score type")
|
||||
}
|
||||
}
|
||||
|
||||
override fun displayScore(track: Track): String {
|
||||
val score = track.score
|
||||
return when (preferences.anilistScoreType().getOrDefault()) {
|
||||
2 -> "${(score / 20).toInt()} ★"
|
||||
3 -> when {
|
||||
|
||||
return when (scorePreference.getOrDefault()) {
|
||||
POINT_5 -> "${(score / 20).toInt()} ★"
|
||||
POINT_3 -> when {
|
||||
score == 0f -> "0"
|
||||
score <= 30 -> "😦"
|
||||
score <= 60 -> "😐"
|
||||
@ -102,15 +127,26 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = COMPLETED
|
||||
}
|
||||
// If user was using API v1 fetch library_id
|
||||
if (track.library_id == null || track.library_id!! == 0L){
|
||||
return api.findLibManga(track, getUsername().toInt()).flatMap {
|
||||
if (it == null) {
|
||||
throw Exception("$track not found on user library")
|
||||
}
|
||||
track.library_id = it.library_id
|
||||
api.updateLibManga(track)
|
||||
}
|
||||
}
|
||||
|
||||
return api.updateLibManga(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUsername())
|
||||
return api.findLibManga(track, getUsername().toInt())
|
||||
.flatMap { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
update(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
@ -126,7 +162,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.getLibManga(track, getUsername())
|
||||
return api.getLibManga(track, getUsername().toInt())
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
@ -136,26 +172,34 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
override fun login(username: String, password: String) = login(password)
|
||||
|
||||
fun login(authCode: String): Completable {
|
||||
return api.login(authCode)
|
||||
// Save the token in the interceptor.
|
||||
.doOnNext { interceptor.setAuth(it) }
|
||||
// Obtain the authenticated user from the API.
|
||||
.zipWith(api.getCurrentUser().map { pair ->
|
||||
preferences.anilistScoreType().set(pair.second)
|
||||
pair.first
|
||||
}, { oauth, user -> Pair(user, oauth.refresh_token!!) })
|
||||
// Save service credentials (username and refresh token).
|
||||
.doOnNext { saveCredentials(it.first, it.second) }
|
||||
// Logout on any error.
|
||||
.doOnError { logout() }
|
||||
.toCompletable()
|
||||
fun login(token: String): Completable {
|
||||
val oauth = api.createOAuth(token)
|
||||
interceptor.setAuth(oauth)
|
||||
return api.getCurrentUser().map { (username, scoreType) ->
|
||||
scorePreference.set(scoreType)
|
||||
saveCredentials(username.toString(), oauth.access_token)
|
||||
}.doOnError{
|
||||
logout()
|
||||
}.toCompletable()
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
preferences.trackToken(this).set(null)
|
||||
interceptor.setAuth(null)
|
||||
}
|
||||
|
||||
fun saveOAuth(oAuth: OAuth?) {
|
||||
preferences.trackToken(this).set(gson.toJson(oAuth))
|
||||
}
|
||||
|
||||
fun loadOAuth(): OAuth? {
|
||||
return try {
|
||||
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,167 +1,275 @@
|
||||
package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.int
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.github.salomonbrys.kotson.*
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import okhttp3.FormBody
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.*
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import rx.Observable
|
||||
|
||||
|
||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
private val rest = restBuilder()
|
||||
.client(client.newBuilder().addInterceptor(interceptor).build())
|
||||
.build()
|
||||
.create(Rest::class.java)
|
||||
private val parser = JsonParser()
|
||||
private val jsonMime = MediaType.parse("application/json; charset=utf-8")
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
|
||||
.map { response ->
|
||||
response.body()?.close()
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("Could not add manga")
|
||||
val query = """
|
||||
mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||
SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status)
|
||||
{ id status } }
|
||||
"""
|
||||
val variables = jsonObject(
|
||||
"mangaId" to track.media_id,
|
||||
"progress" to track.last_chapter_read,
|
||||
"status" to track.toAnilistStatus()
|
||||
)
|
||||
val payload = jsonObject(
|
||||
"query" to query,
|
||||
"variables" to variables
|
||||
)
|
||||
val body = RequestBody.create(jsonMime, payload.toString())
|
||||
val request = Request.Builder()
|
||||
.url(apiUrl)
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
netResponse.close()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).obj
|
||||
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
|
||||
track.toAnilistScore())
|
||||
.map { response ->
|
||||
response.body()?.close()
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("Could not update manga")
|
||||
val query = """
|
||||
mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
||||
SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
||||
id
|
||||
status
|
||||
progress
|
||||
}
|
||||
}
|
||||
"""
|
||||
val variables = jsonObject(
|
||||
"listId" to track.library_id,
|
||||
"progress" to track.last_chapter_read,
|
||||
"status" to track.toAnilistStatus(),
|
||||
"score" to track.score.toInt()
|
||||
)
|
||||
val payload = jsonObject(
|
||||
"query" to query,
|
||||
"variables" to variables
|
||||
)
|
||||
val body = RequestBody.create(jsonMime, payload.toString())
|
||||
val request = Request.Builder()
|
||||
.url(apiUrl)
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String): Observable<List<TrackSearch>> {
|
||||
return rest.search(query, 1)
|
||||
.map { list ->
|
||||
list.filter { it.type != "Novel" }.map { it.toTrack() }
|
||||
fun search(search: String): Observable<List<TrackSearch>> {
|
||||
val query = """
|
||||
query Search(${'$'}query: String) {
|
||||
Page (perPage: 25) {
|
||||
media(search: ${'$'}query, type: MANGA, format: MANGA) {
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
}
|
||||
.onErrorReturn { emptyList() }
|
||||
coverImage {
|
||||
large
|
||||
}
|
||||
|
||||
fun getList(username: String): Observable<List<Track>> {
|
||||
return rest.getLib(username)
|
||||
.map { lib ->
|
||||
lib.flatten().map { it.toTrack() }
|
||||
type
|
||||
status
|
||||
chapters
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
val variables = jsonObject(
|
||||
"query" to search
|
||||
)
|
||||
val payload = jsonObject(
|
||||
"query" to query,
|
||||
"variables" to variables
|
||||
)
|
||||
val body = RequestBody.create(jsonMime, payload.toString())
|
||||
val request = Request.Builder()
|
||||
.url(apiUrl)
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).obj
|
||||
val data = response["data"]!!.obj
|
||||
val page = data["Page"].obj
|
||||
val media = page["media"].array
|
||||
val entries = media.map { jsonToALManga(it.obj) }
|
||||
entries.map { it.toTrack() }
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, username: String) : Observable<Track?> {
|
||||
// TODO avoid getting the entire list
|
||||
return getList(username)
|
||||
.map { list -> list.find { it.remote_id == track.remote_id } }
|
||||
|
||||
fun findLibManga(track: Track, userid: Int) : Observable<Track?> {
|
||||
val query = """
|
||||
query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||
Page {
|
||||
mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||
id
|
||||
status
|
||||
scoreRaw: score(format: POINT_100)
|
||||
progress
|
||||
media{
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
}
|
||||
coverImage {
|
||||
large
|
||||
}
|
||||
type
|
||||
status
|
||||
chapters
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
val variables = jsonObject(
|
||||
"id" to userid,
|
||||
"manga_id" to track.media_id
|
||||
)
|
||||
val payload = jsonObject(
|
||||
"query" to query,
|
||||
"variables" to variables
|
||||
)
|
||||
val body = RequestBody.create(jsonMime, payload.toString())
|
||||
val request = Request.Builder()
|
||||
.url(apiUrl)
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).obj
|
||||
val data = response["data"]!!.obj
|
||||
val page = data["Page"].obj
|
||||
val media = page["mediaList"].array
|
||||
val entries = media.map { jsonToALUserManga(it.obj) }
|
||||
entries.firstOrNull()?.toTrack()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track, username: String): Observable<Track> {
|
||||
return findLibManga(track, username)
|
||||
fun getLibManga(track: Track, userid: Int): Observable<Track> {
|
||||
return findLibManga(track, userid)
|
||||
.map { it ?: throw Exception("Could not find manga") }
|
||||
}
|
||||
|
||||
fun login(authCode: String): Observable<OAuth> {
|
||||
return restBuilder()
|
||||
.client(client)
|
||||
fun createOAuth(token: String): OAuth {
|
||||
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
||||
}
|
||||
|
||||
fun getCurrentUser(): Observable<Pair<Int, String>> {
|
||||
val query = """
|
||||
query User
|
||||
{
|
||||
Viewer {
|
||||
id
|
||||
mediaListOptions {
|
||||
scoreFormat
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
val payload = jsonObject(
|
||||
"query" to query
|
||||
)
|
||||
val body = RequestBody.create(jsonMime, payload.toString())
|
||||
val request = Request.Builder()
|
||||
.url(apiUrl)
|
||||
.post(body)
|
||||
.build()
|
||||
.create(Rest::class.java)
|
||||
.requestAccessToken(authCode)
|
||||
return authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).obj
|
||||
val data = response["data"]!!.obj
|
||||
val viewer = data["Viewer"].obj
|
||||
Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentUser(): Observable<Pair<String, Int>> {
|
||||
return rest.getCurrentUser()
|
||||
.map { it["id"].string to it["score_type"].int }
|
||||
fun jsonToALManga(struct: JsonObject): ALManga{
|
||||
return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
|
||||
null, struct["type"].asString, struct["status"].asString,
|
||||
struct["startDate"]["year"].nullString.orEmpty() + struct["startDate"]["month"].nullString.orEmpty()
|
||||
+ struct["startDate"]["day"].nullString.orEmpty(), struct["chapters"].nullInt ?: 0)
|
||||
}
|
||||
|
||||
private fun restBuilder() = Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
|
||||
private interface Rest {
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("auth/access_token")
|
||||
fun requestAccessToken(
|
||||
@Field("code") code: String,
|
||||
@Field("grant_type") grant_type: String = "authorization_code",
|
||||
@Field("client_id") client_id: String = clientId,
|
||||
@Field("client_secret") client_secret: String = clientSecret,
|
||||
@Field("redirect_uri") redirect_uri: String = clientUrl
|
||||
) : Observable<OAuth>
|
||||
|
||||
@GET("user")
|
||||
fun getCurrentUser(): Observable<JsonObject>
|
||||
|
||||
@GET("manga/search/{query}")
|
||||
fun search(
|
||||
@Path("query") query: String,
|
||||
@Query("page") page: Int
|
||||
): Observable<List<ALManga>>
|
||||
|
||||
@GET("user/{username}/mangalist")
|
||||
fun getLib(
|
||||
@Path("username") username: String
|
||||
): Observable<ALUserLists>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("mangalist")
|
||||
fun addLibManga(
|
||||
@Field("id") id: Int,
|
||||
@Field("chapters_read") chapters_read: Int,
|
||||
@Field("list_status") list_status: String
|
||||
) : Observable<Response<ResponseBody>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("mangalist")
|
||||
fun updateLibManga(
|
||||
@Field("id") id: Int,
|
||||
@Field("chapters_read") chapters_read: Int,
|
||||
@Field("list_status") list_status: String,
|
||||
@Field("score") score_raw: String
|
||||
) : Observable<Response<ResponseBody>>
|
||||
|
||||
fun jsonToALUserManga(struct: JsonObject): ALUserManga{
|
||||
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj) )
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val clientId = "tachiyomi-hrtje"
|
||||
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
||||
private const val clientId = "385"
|
||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
||||
private const val baseUrl = "https://anilist.co/api/"
|
||||
private const val apiUrl = "https://graphql.anilist.co/"
|
||||
private const val baseUrl = "https://anilist.co/api/v2/"
|
||||
private const val baseMangaUrl = "https://anilist.co/manga/"
|
||||
|
||||
fun mangaUrl(remoteId: Int): String {
|
||||
return baseMangaUrl + remoteId
|
||||
fun mangaUrl(mediaId: Int): String {
|
||||
return baseMangaUrl + mediaId
|
||||
}
|
||||
|
||||
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
||||
.appendQueryParameter("grant_type", "authorization_code")
|
||||
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("redirect_uri", clientUrl)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("response_type", "token")
|
||||
.build()
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("refresh_token", token)
|
||||
.build())
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
||||
|
||||
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
|
||||
|
||||
/**
|
||||
* OAuth object used for authenticated requests.
|
||||
@ -20,24 +20,21 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
if (refreshToken.isNullOrEmpty()) {
|
||||
if (token.isNullOrEmpty()) {
|
||||
throw Exception("Not authenticated with Anilist")
|
||||
}
|
||||
|
||||
// Refresh access token if null or expired.
|
||||
if (oauth == null || oauth!!.isExpired()) {
|
||||
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
||||
oauth = if (response.isSuccessful) {
|
||||
Gson().fromJson(response.body()!!.string(), OAuth::class.java)
|
||||
} else {
|
||||
response.close()
|
||||
null
|
||||
if (oauth == null){
|
||||
oauth = anilist.loadOAuth()
|
||||
}
|
||||
// Refresh access token if null or expired.
|
||||
if (oauth!!.isExpired()) {
|
||||
anilist.logout()
|
||||
throw Exception("Token expired")
|
||||
}
|
||||
|
||||
// Throw on null auth.
|
||||
if (oauth == null) {
|
||||
throw Exception("Access token wasn't refreshed")
|
||||
throw Exception("No authentication token")
|
||||
}
|
||||
|
||||
// Add the authorization header to the original request.
|
||||
@ -53,8 +50,9 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
||||
* and the oauth object.
|
||||
*/
|
||||
fun setAuth(oauth: OAuth?) {
|
||||
refreshToken = oauth?.refresh_token
|
||||
token = oauth?.access_token
|
||||
this.oauth = oauth
|
||||
anilist.saveOAuth(oauth)
|
||||
}
|
||||
|
||||
}
|
@ -11,7 +11,7 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
data class ALManga(
|
||||
val id: Int,
|
||||
val media_id: Int,
|
||||
val title_romaji: String,
|
||||
val image_url_lge: String,
|
||||
val description: String?,
|
||||
@ -21,12 +21,12 @@ data class ALManga(
|
||||
val total_chapters: Int) {
|
||||
|
||||
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
|
||||
remote_id = this@ALManga.id
|
||||
media_id = this@ALManga.media_id
|
||||
title = title_romaji
|
||||
total_chapters = this@ALManga.total_chapters
|
||||
cover_url = image_url_lge
|
||||
summary = description ?: ""
|
||||
tracking_url = AnilistApi.mangaUrl(remote_id)
|
||||
tracking_url = AnilistApi.mangaUrl(media_id)
|
||||
publishing_status = this@ALManga.publishing_status
|
||||
publishing_type = type
|
||||
if (!start_date_fuzzy.isNullOrBlank()) {
|
||||
@ -43,40 +43,37 @@ data class ALManga(
|
||||
}
|
||||
|
||||
data class ALUserManga(
|
||||
val id: Int,
|
||||
val library_id: Long,
|
||||
val list_status: String,
|
||||
val score_raw: Int,
|
||||
val chapters_read: Int,
|
||||
val manga: ALManga) {
|
||||
|
||||
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||
remote_id = manga.id
|
||||
media_id = manga.media_id
|
||||
status = toTrackStatus()
|
||||
score = score_raw.toFloat()
|
||||
last_chapter_read = chapters_read
|
||||
library_id = this@ALUserManga.library_id
|
||||
}
|
||||
|
||||
fun toTrackStatus() = when (list_status) {
|
||||
"reading" -> Anilist.READING
|
||||
"completed" -> Anilist.COMPLETED
|
||||
"on-hold" -> Anilist.ON_HOLD
|
||||
"dropped" -> Anilist.DROPPED
|
||||
"plan to read" -> Anilist.PLAN_TO_READ
|
||||
"CURRENT" -> Anilist.READING
|
||||
"COMPLETED" -> Anilist.COMPLETED
|
||||
"PAUSED" -> Anilist.ON_HOLD
|
||||
"DROPPED" -> Anilist.DROPPED
|
||||
"PLANNING" -> Anilist.PLANNING
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
}
|
||||
|
||||
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
||||
|
||||
fun flatten() = lists.values.flatten()
|
||||
}
|
||||
|
||||
fun Track.toAnilistStatus() = when (status) {
|
||||
Anilist.READING -> "reading"
|
||||
Anilist.COMPLETED -> "completed"
|
||||
Anilist.ON_HOLD -> "on-hold"
|
||||
Anilist.DROPPED -> "dropped"
|
||||
Anilist.PLAN_TO_READ -> "plan to read"
|
||||
Anilist.READING -> "CURRENT"
|
||||
Anilist.COMPLETED -> "COMPLETED"
|
||||
Anilist.ON_HOLD -> "PAUSED"
|
||||
Anilist.DROPPED -> "DROPPED"
|
||||
Anilist.PLANNING -> "PLANNING"
|
||||
Anilist.REPEATING -> "REPEATING"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
|
||||
@ -84,11 +81,11 @@ private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) {
|
||||
// 10 point
|
||||
0 -> (score.toInt() / 10).toString()
|
||||
"POINT_10" -> (score.toInt() / 10).toString()
|
||||
// 100 point
|
||||
1 -> score.toInt().toString()
|
||||
"POINT_100" -> score.toInt().toString()
|
||||
// 5 stars
|
||||
2 -> when {
|
||||
"POINT_5" -> when {
|
||||
score == 0f -> "0"
|
||||
score < 30 -> "1"
|
||||
score < 50 -> "2"
|
||||
@ -97,13 +94,13 @@ fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrD
|
||||
else -> "5"
|
||||
}
|
||||
// Smiley
|
||||
3 -> when {
|
||||
"POINT_3" -> when {
|
||||
score == 0f -> "0"
|
||||
score <= 30 -> ":("
|
||||
score <= 60 -> ":|"
|
||||
else -> ":)"
|
||||
}
|
||||
// 10 point decimal
|
||||
4 -> (score / 10).toString()
|
||||
"POINT_10_DECIMAL" -> (score / 10).toString()
|
||||
else -> throw Exception("Unknown score type")
|
||||
}
|
@ -4,8 +4,7 @@ data class OAuth(
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val expires: Long,
|
||||
val expires_in: Long,
|
||||
val refresh_token: String?) {
|
||||
val expires_in: Long) {
|
||||
|
||||
fun isExpired() = System.currentTimeMillis() > expires
|
||||
}
|
@ -87,7 +87,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
.flatMap { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.remote_id = remoteTrack.remote_id
|
||||
track.media_id = remoteTrack.media_id
|
||||
update(track)
|
||||
} else {
|
||||
track.score = DEFAULT_SCORE
|
||||
|
@ -42,7 +42,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
),
|
||||
"media" to jsonObject(
|
||||
"data" to jsonObject(
|
||||
"id" to track.remote_id,
|
||||
"id" to track.media_id,
|
||||
"type" to "manga"
|
||||
)
|
||||
)
|
||||
@ -52,7 +52,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
|
||||
rest.addLibManga(jsonObject("data" to data))
|
||||
.map { json ->
|
||||
track.remote_id = json["data"]["id"].int
|
||||
track.media_id = json["data"]["id"].int
|
||||
track
|
||||
}
|
||||
}
|
||||
@ -63,7 +63,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
// @formatter:off
|
||||
val data = jsonObject(
|
||||
"type" to "libraryEntries",
|
||||
"id" to track.remote_id,
|
||||
"id" to track.media_id,
|
||||
"attributes" to jsonObject(
|
||||
"status" to track.toKitsuStatus(),
|
||||
"progress" to track.last_chapter_read,
|
||||
@ -72,7 +72,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
)
|
||||
// @formatter:on
|
||||
|
||||
rest.updateLibManga(track.remote_id, jsonObject("data" to data))
|
||||
rest.updateLibManga(track.media_id, jsonObject("data" to data))
|
||||
.map { track }
|
||||
}
|
||||
}
|
||||
@ -88,7 +88,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, userId: String): Observable<Track?> {
|
||||
return rest.findLibManga(track.remote_id, userId)
|
||||
return rest.findLibManga(track.media_id, userId)
|
||||
.map { json ->
|
||||
val data = json["data"].array
|
||||
if (data.size() > 0) {
|
||||
@ -101,7 +101,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track): Observable<Track> {
|
||||
return rest.getLibManga(track.remote_id)
|
||||
return rest.getLibManga(track.media_id)
|
||||
.map { json ->
|
||||
val data = json["data"].array
|
||||
if (data.size() > 0) {
|
||||
|
@ -19,12 +19,12 @@ open class KitsuManga(obj: JsonObject) {
|
||||
|
||||
@CallSuper
|
||||
open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
||||
remote_id = this@KitsuManga.id
|
||||
media_id = this@KitsuManga.id
|
||||
title = canonicalTitle
|
||||
total_chapters = chapterCount ?: 0
|
||||
cover_url = original
|
||||
summary = synopsis
|
||||
tracking_url = KitsuApi.mangaUrl(remote_id)
|
||||
tracking_url = KitsuApi.mangaUrl(media_id)
|
||||
publishing_status = this@KitsuManga.status
|
||||
publishing_type = type
|
||||
start_date = startDate.orEmpty()
|
||||
@ -32,13 +32,13 @@ open class KitsuManga(obj: JsonObject) {
|
||||
}
|
||||
|
||||
class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) {
|
||||
val remoteId by obj.byInt("id")
|
||||
val libraryId by obj.byInt("id")
|
||||
override val status by obj["attributes"].byString
|
||||
val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
|
||||
val progress by obj["attributes"].byInt
|
||||
|
||||
override fun toTrack() = super.toTrack().apply {
|
||||
remote_id = remoteId
|
||||
media_id = libraryId // TODO migrate media ids to library ids
|
||||
status = toTrackStatus()
|
||||
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f
|
||||
last_chapter_read = progress
|
||||
|
@ -10,7 +10,9 @@ class TrackSearch : Track {
|
||||
|
||||
override var sync_id: Int = 0
|
||||
|
||||
override var remote_id: Int = 0
|
||||
override var media_id: Int = 0
|
||||
|
||||
override var library_id: Long? = null
|
||||
|
||||
override lateinit var title: String
|
||||
|
||||
@ -42,13 +44,13 @@ class TrackSearch : Track {
|
||||
|
||||
if (manga_id != other.manga_id) return false
|
||||
if (sync_id != other.sync_id) return false
|
||||
return remote_id == other.remote_id
|
||||
return media_id == other.media_id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
||||
result = 31 * result + sync_id
|
||||
result = 31 * result + remote_id
|
||||
result = 31 * result + media_id
|
||||
return result
|
||||
}
|
||||
companion object {
|
||||
|
@ -54,11 +54,11 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
||||
.map {
|
||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||
title = it.selectText("title")!!
|
||||
remote_id = it.selectInt("id")
|
||||
media_id = it.selectInt("id")
|
||||
total_chapters = it.selectInt("chapters")
|
||||
summary = it.selectText("synopsis")!!
|
||||
cover_url = it.selectText("image")!!
|
||||
tracking_url = MyanimelistApi.mangaUrl(remote_id)
|
||||
tracking_url = MyanimelistApi.mangaUrl(media_id)
|
||||
publishing_status = it.selectText("status")!!
|
||||
publishing_type = it.selectText("type")!!
|
||||
start_date = it.selectText("start_date")!!
|
||||
@ -77,13 +77,13 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
||||
.map {
|
||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||
title = it.selectText("series_title")!!
|
||||
remote_id = it.selectInt("series_mangadb_id")
|
||||
media_id = it.selectInt("series_mangadb_id")
|
||||
last_chapter_read = it.selectInt("my_read_chapters")
|
||||
status = it.selectInt("my_status")
|
||||
score = it.selectInt("my_score").toFloat()
|
||||
total_chapters = it.selectInt("series_chapters")
|
||||
cover_url = it.selectText("series_image")!!
|
||||
tracking_url = MyanimelistApi.mangaUrl(remote_id)
|
||||
tracking_url = MyanimelistApi.mangaUrl(media_id)
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
@ -91,7 +91,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
||||
|
||||
fun findLibManga(track: Track, username: String): Observable<Track?> {
|
||||
return getList(username)
|
||||
.map { list -> list.find { it.remote_id == track.remote_id } }
|
||||
.map { list -> list.find { it.media_id == track.media_id } }
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track, username: String): Observable<Track> {
|
||||
@ -169,12 +169,12 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
||||
|
||||
fun getUpdateUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
|
||||
.appendEncodedPath("api/mangalist/update")
|
||||
.appendPath("${track.remote_id}.xml")
|
||||
.appendPath("${track.media_id}.xml")
|
||||
.toString()
|
||||
|
||||
fun getAddUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
|
||||
.appendEncodedPath("api/mangalist/add")
|
||||
.appendPath("${track.remote_id}.xml")
|
||||
.appendPath("${track.media_id}.xml")
|
||||
.toString()
|
||||
|
||||
fun createHeaders(username: String, password: String): Headers {
|
||||
|
@ -1,10 +1,13 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Used to connect with the Github API.
|
||||
@ -17,6 +20,7 @@ interface GithubService {
|
||||
.baseUrl("https://api.github.com")
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.client(Injekt.get<NetworkHelper>().client)
|
||||
.build()
|
||||
|
||||
return restAdapter.create(GithubService::class.java)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import com.squareup.duktape.Duktape
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
@ -21,7 +22,7 @@ class CloudflareInterceptor : Interceptor {
|
||||
val response = chain.proceed(chain.request())
|
||||
|
||||
// Check if Cloudflare anti-bot is on
|
||||
if (response.code() == 503 && serverCheck.contains(response.header("Server"))) {
|
||||
if (response.code() == 503 && response.header("Server") in serverCheck) {
|
||||
return chain.proceed(resolveChallenge(response))
|
||||
}
|
||||
|
||||
@ -43,31 +44,32 @@ class CloudflareInterceptor : Interceptor {
|
||||
val pass = passPattern.find(content)?.groups?.get(1)?.value
|
||||
|
||||
if (operation == null || challenge == null || pass == null) {
|
||||
throw RuntimeException("Failed resolving Cloudflare challenge")
|
||||
throw Exception("Failed resolving Cloudflare challenge")
|
||||
}
|
||||
|
||||
val js = operation
|
||||
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
|
||||
.replace(Regex("""a\.value = (.+ \+ t\.length).+"""), "$1")
|
||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
||||
.replace("t.length", "${domain.length}")
|
||||
.replace("\n", "")
|
||||
|
||||
val result = (duktape.evaluate(js) as Double).toInt()
|
||||
|
||||
val answer = "${result + domain.length}"
|
||||
val result = duktape.evaluate(js) as Double
|
||||
|
||||
val cloudflareUrl = HttpUrl.parse("${url.scheme()}://$domain/cdn-cgi/l/chk_jschl")!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("jschl_vc", challenge)
|
||||
.addQueryParameter("pass", pass)
|
||||
.addQueryParameter("jschl_answer", answer)
|
||||
.addQueryParameter("jschl_answer", "$result")
|
||||
.toString()
|
||||
|
||||
val cloudflareHeaders = originalRequest.headers()
|
||||
.newBuilder()
|
||||
.add("Referer", url.toString())
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml")
|
||||
.add("Accept-Language", "en")
|
||||
.build()
|
||||
|
||||
return GET(cloudflareUrl, cloudflareHeaders)
|
||||
return GET(cloudflareUrl, cloudflareHeaders, cache = CacheControl.Builder().build())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,25 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import okhttp3.Cache
|
||||
import okhttp3.CipherSuite
|
||||
import okhttp3.ConnectionSpec
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.TlsVersion
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.Socket
|
||||
import java.net.UnknownHostException
|
||||
import java.security.KeyManagementException
|
||||
import java.security.KeyStore
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
class NetworkHelper(context: Context) {
|
||||
|
||||
@ -16,6 +32,7 @@ class NetworkHelper(context: Context) {
|
||||
val client = OkHttpClient.Builder()
|
||||
.cookieJar(cookieManager)
|
||||
.cache(Cache(cacheDir, cacheSize))
|
||||
.enableTLS12()
|
||||
.build()
|
||||
|
||||
val cloudflareClient = client.newBuilder()
|
||||
@ -25,4 +42,87 @@ class NetworkHelper(context: Context) {
|
||||
val cookies: PersistentCookieStore
|
||||
get() = cookieManager.store
|
||||
|
||||
private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
|
||||
return this
|
||||
}
|
||||
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustManagerFactory.init(null as KeyStore?)
|
||||
val trustManagers = trustManagerFactory.trustManagers
|
||||
if (trustManagers.size == 1 && trustManagers[0] is X509TrustManager) {
|
||||
class TLSSocketFactory @Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
|
||||
constructor() : SSLSocketFactory() {
|
||||
|
||||
private val internalSSLSocketFactory: SSLSocketFactory
|
||||
|
||||
init {
|
||||
val context = SSLContext.getInstance("TLS")
|
||||
context.init(null, null, null)
|
||||
internalSSLSocketFactory = context.socketFactory
|
||||
}
|
||||
|
||||
override fun getDefaultCipherSuites(): Array<String> {
|
||||
return internalSSLSocketFactory.defaultCipherSuites
|
||||
}
|
||||
|
||||
override fun getSupportedCipherSuites(): Array<String> {
|
||||
return internalSSLSocketFactory.supportedCipherSuites
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun createSocket(): Socket? {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket? {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose))
|
||||
}
|
||||
|
||||
@Throws(IOException::class, UnknownHostException::class)
|
||||
override fun createSocket(host: String, port: Int): Socket? {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port))
|
||||
}
|
||||
|
||||
@Throws(IOException::class, UnknownHostException::class)
|
||||
override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket? {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun createSocket(host: InetAddress, port: Int): Socket? {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket? {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort))
|
||||
}
|
||||
|
||||
private fun enableTLSOnSocket(socket: Socket?): Socket? {
|
||||
if (socket != null && socket is SSLSocket) {
|
||||
socket.enabledProtocols = socket.supportedProtocols
|
||||
}
|
||||
return socket
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -24,4 +24,8 @@ class Batoto : Source {
|
||||
return Observable.error(Exception("RIP Batoto"))
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$name (EN)"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -49,7 +49,11 @@ class Mangachan : ParsedHttpSource() {
|
||||
}
|
||||
}
|
||||
}
|
||||
is OrderBy -> { if (filter.state!!.ascending && filter.state!!.index == 0) { statusParam = false } }
|
||||
is OrderBy -> {
|
||||
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 {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = element.select("div.manga_images img").first().attr("src")
|
||||
element.select("h2 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
@ -220,32 +225,22 @@ class Mangachan : ParsedHttpSource() {
|
||||
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)")].map((el,i) =>
|
||||
* { const link=el.getAttribute('href');const id=link.substr(6,link.length);
|
||||
* return `Genre("${id.replace("_", " ")}")` }).join(',\n')
|
||||
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")]
|
||||
* .map(el => `Genre("${el.getAttribute('href').substr(6)}")`).join(',\n')
|
||||
* on http://mangachan.me/
|
||||
*/
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("18 плюс"),
|
||||
Genre("18_плюс"),
|
||||
Genre("bdsm"),
|
||||
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("сказка"),
|
||||
@ -279,7 +274,6 @@ class Mangachan : ParsedHttpSource() {
|
||||
Genre("сёдзё-ай"),
|
||||
Genre("сёнэн"),
|
||||
Genre("сёнэн-ай"),
|
||||
Genre("темное фэнтези"),
|
||||
Genre("тентакли"),
|
||||
Genre("трагедия"),
|
||||
Genre("триллер"),
|
||||
|
@ -24,23 +24,19 @@ class Mintmanga : ParsedHttpSource() {
|
||||
|
||||
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 =
|
||||
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
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 {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = element.select("img.lazy").first().attr("data-original")
|
||||
element.select("h3 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
@ -90,10 +86,15 @@ class Mintmanga : ParsedHttpSource() {
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
val urlText = urlElement.text()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
|
||||
chapter.name = urlElement.text().replace(" новое", "")
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mtr=1")
|
||||
if (urlText.endsWith(" новое")) {
|
||||
chapter.name = urlText.dropLast(6)
|
||||
} else {
|
||||
chapter.name = urlText
|
||||
}
|
||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
||||
} ?: 0
|
||||
@ -143,11 +144,19 @@ class Mintmanga : ParsedHttpSource() {
|
||||
|
||||
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)
|
||||
|
||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
||||
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")]
|
||||
* .map(el => `Genre("${el.textContent.trim()}", $"{el.getAttribute('onclick')
|
||||
* .substr(31,el.getAttribute('onclick').length-33)"})`).join(',\n')
|
||||
* on http://mintmanga.com/search/advanced
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
|
@ -24,14 +24,9 @@ class Readmanga : ParsedHttpSource() {
|
||||
|
||||
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 popularMangaSelector() = "div.tile"
|
||||
|
||||
override fun popularMangaSelector() = "div.desc"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.desc"
|
||||
override fun latestUpdatesSelector() = "div.tile"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
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 {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = element.select("img.lazy").first().attr("data-original")
|
||||
element.select("h3 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
@ -90,10 +86,15 @@ class Readmanga : ParsedHttpSource() {
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
val urlText = urlElement.text()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
|
||||
chapter.name = urlElement.text().replace(" новое", "")
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mtr=1")
|
||||
if (urlText.endsWith(" новое")) {
|
||||
chapter.name = urlText.dropLast(6)
|
||||
} else {
|
||||
chapter.name = urlText
|
||||
}
|
||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
||||
} ?: 0
|
||||
@ -143,11 +144,19 @@ class Readmanga : ParsedHttpSource() {
|
||||
|
||||
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)
|
||||
|
||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
||||
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")]
|
||||
* .map(el => `Genre("${el.textContent.trim()}", $"{el.getAttribute('onclick')
|
||||
* .substr(31,el.getAttribute('onclick').length-33)"})`).join(',\n')
|
||||
* on http://readmanga.me/search/advanced
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
|
@ -1,11 +1,21 @@
|
||||
package eu.kanade.tachiyomi.ui.base.presenter
|
||||
|
||||
import android.os.Bundle
|
||||
import nucleus.presenter.RxPresenter
|
||||
import nucleus.presenter.delivery.Delivery
|
||||
import rx.Observable
|
||||
|
||||
open class BasePresenter<V> : RxPresenter<V>() {
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
try {
|
||||
super.onCreate(savedState)
|
||||
} catch (e: NullPointerException) {
|
||||
// Swallow this error. This should be fixed in the library but since it's not critical
|
||||
// (only used by restartables) it should be enough. It saves me a fork.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
|
||||
* subscription list.
|
||||
|
@ -28,7 +28,7 @@ public class NucleusConductorDelegate<P extends Presenter> {
|
||||
|
||||
Bundle onSaveInstanceState() {
|
||||
Bundle bundle = new Bundle();
|
||||
getPresenter();
|
||||
// getPresenter(); // Workaround a crash related to saving instance state with child routers
|
||||
if (presenter != null) {
|
||||
presenter.save(bundle);
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
|
||||
|
||||
class LangHolder(view: View, adapter: FlexibleAdapter<*>) :
|
||||
BaseFlexibleViewHolder(view, adapter, true) {
|
||||
BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
fun bind(item: LangItem) {
|
||||
title.text = LocaleHelper.getDisplayName(item.code, itemView.context)
|
||||
|
@ -34,7 +34,7 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
||||
* @param manga the manga to find.
|
||||
*/
|
||||
fun indexOf(manga: Manga): Int {
|
||||
return mangas.indexOfFirst { it.manga.id == manga.id }
|
||||
return currentItems.indexOfFirst { it.manga.id == manga.id }
|
||||
}
|
||||
|
||||
fun performFilter() {
|
||||
|
@ -73,7 +73,7 @@ class LibraryController(
|
||||
/**
|
||||
* Currently selected mangas.
|
||||
*/
|
||||
val selectedMangas = mutableListOf<Manga>()
|
||||
val selectedMangas = mutableSetOf<Manga>()
|
||||
|
||||
private var selectedCoverManga: Manga? = null
|
||||
|
||||
@ -429,13 +429,15 @@ class LibraryController(
|
||||
*/
|
||||
fun setSelection(manga: Manga, selected: Boolean) {
|
||||
if (selected) {
|
||||
selectedMangas.add(manga)
|
||||
if (selectedMangas.add(manga)) {
|
||||
selectionRelay.call(LibrarySelectionEvent.Selected(manga))
|
||||
}
|
||||
} else {
|
||||
selectedMangas.remove(manga)
|
||||
if (selectedMangas.remove(manga)) {
|
||||
selectionRelay.call(LibrarySelectionEvent.Unselected(manga))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the selected manga to a list of categories.
|
||||
|
@ -261,7 +261,11 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
}
|
||||
|
||||
fun setLastUpdateDate(date: Date) {
|
||||
if (date.time != 0L) {
|
||||
manga_last_update?.text = DateFormat.getDateInstance(DateFormat.SHORT).format(date)
|
||||
} else {
|
||||
manga_last_update?.text = resources?.getString(R.string.unknown)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -8,9 +8,9 @@ import com.jakewharton.rxbinding.widget.itemClicks
|
||||
import com.jakewharton.rxbinding.widget.textChanges
|
||||
import eu.kanade.tachiyomi.R
|
||||
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.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import kotlinx.android.synthetic.main.track_search_dialog.view.*
|
||||
@ -114,14 +114,14 @@ class TrackSearchDialog : DialogController {
|
||||
private fun search(query: String) {
|
||||
val view = dialogView ?: return
|
||||
view.progress.visibility = View.VISIBLE
|
||||
view.track_search_list.visibility = View.GONE
|
||||
view.track_search_list.visibility = View.INVISIBLE
|
||||
trackController.presenter.search(query, service)
|
||||
}
|
||||
|
||||
fun onSearchResults(results: List<TrackSearch>) {
|
||||
selectedItem = null
|
||||
val view = dialogView ?: return
|
||||
view.progress.visibility = View.GONE
|
||||
view.progress.visibility = View.INVISIBLE
|
||||
view.track_search_list.visibility = View.VISIBLE
|
||||
adapter?.setItems(results)
|
||||
}
|
||||
@ -129,7 +129,7 @@ class TrackSearchDialog : DialogController {
|
||||
fun onSearchResultsError() {
|
||||
val view = dialogView ?: return
|
||||
view.progress.visibility = View.VISIBLE
|
||||
view.track_search_list.visibility = View.GONE
|
||||
view.track_search_list.visibility = View.INVISIBLE
|
||||
adapter?.setItems(emptyList())
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import eu.kanade.tachiyomi.util.visibleIf
|
||||
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
|
||||
import kotlinx.android.synthetic.main.reader_settings_dialog.view.*
|
||||
import rx.Observable
|
||||
@ -91,6 +92,23 @@ class ReaderSettingsDialog : DialogFragment() {
|
||||
crop_borders.setOnCheckedChangeListener { _, isChecked ->
|
||||
preferences.cropBorders().set(isChecked)
|
||||
}
|
||||
|
||||
crop_borders_webtoon.isChecked = preferences.cropBordersWebtoon().getOrDefault()
|
||||
crop_borders_webtoon.setOnCheckedChangeListener { _, isChecked ->
|
||||
preferences.cropBordersWebtoon().set(isChecked)
|
||||
}
|
||||
|
||||
val readerActivity = activity as? ReaderActivity
|
||||
val isWebtoonViewer = if (readerActivity != null) {
|
||||
val mangaViewer = readerActivity.presenter.manga.viewer
|
||||
val viewer = if (mangaViewer == 0) preferences.defaultViewer() else mangaViewer
|
||||
viewer == ReaderActivity.WEBTOON
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
crop_borders.visibleIf { !isWebtoonViewer }
|
||||
crop_borders_webtoon.visibleIf { isWebtoonViewer }
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
@ -4,7 +4,12 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.*
|
||||
import android.view.Display
|
||||
import android.view.GestureDetector
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
@ -123,7 +128,7 @@ class WebtoonReader : BaseReader() {
|
||||
.distinctUntilChanged()
|
||||
.subscribe { refreshAdapter() })
|
||||
|
||||
subscriptions.add(readerActivity.preferences.cropBorders()
|
||||
subscriptions.add(readerActivity.preferences.cropBordersWebtoon()
|
||||
.asObservable()
|
||||
.doOnNext { cropBorders = it }
|
||||
.skip(1)
|
||||
|
@ -23,9 +23,10 @@ class AnilistLoginActivity : AppCompatActivity() {
|
||||
val view = ProgressBar(this)
|
||||
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
|
||||
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
trackManager.aniList.login(code)
|
||||
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
||||
val matchResult = regex.find(intent.data?.fragment.toString())
|
||||
if (matchResult?.groups?.get(1) != null) {
|
||||
trackManager.aniList.login(matchResult.groups[1]!!.value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
|
@ -22,7 +22,8 @@ import timber.log.Timber
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
|
||||
|
||||
@ -71,7 +72,16 @@ class SettingsAboutController : SettingsController() {
|
||||
}
|
||||
preference {
|
||||
title = "Discord"
|
||||
val url = "https://discord.gg/2dDQBv2"
|
||||
val url = "https://discord.gg/tachiyomi"
|
||||
summary = url
|
||||
onClick {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
preference {
|
||||
title = "Github"
|
||||
val url = "https://github.com/inorichi/tachiyomi"
|
||||
summary = url
|
||||
onClick {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
@ -126,6 +136,7 @@ class SettingsAboutController : SettingsController() {
|
||||
}
|
||||
}
|
||||
}, { error ->
|
||||
activity?.toast(error.message)
|
||||
Timber.e(error)
|
||||
})
|
||||
}
|
||||
|
@ -29,8 +29,9 @@ class SettingsGeneralController : SettingsController() {
|
||||
listPreference {
|
||||
key = Keys.lang
|
||||
titleRes = R.string.pref_language
|
||||
entryValues = arrayOf("", "ar", "bg", "bn", "de", "en", "es", "fr", "hi", "hu", "id",
|
||||
"it", "ja", "ko", "lv", "ms", "nl", "pl", "pt", "pt-BR", "ro", "ru", "vi")
|
||||
entryValues = arrayOf("", "ar", "bg", "bn", "de", "en-US", "en-GB", "es", "fr", "hi",
|
||||
"hu", "in", "it", "ja", "ko", "lv", "ms", "nl", "pl", "pt", "pt-BR", "ro",
|
||||
"ru", "vi")
|
||||
entries = entryValues.map { value ->
|
||||
val locale = LocaleHelper.getLocaleFromString(value.toString())
|
||||
locale?.getDisplayName(locale)?.capitalize() ?:
|
||||
|
@ -76,8 +76,8 @@ class SettingsReaderController : SettingsController() {
|
||||
defaultValue = true
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.enableTransitions
|
||||
titleRes = R.string.pref_page_transitions
|
||||
key = Keys.keepScreenOn
|
||||
titleRes = R.string.pref_keep_screen_on
|
||||
defaultValue = true
|
||||
}
|
||||
switchPreference {
|
||||
@ -85,15 +85,28 @@ class SettingsReaderController : SettingsController() {
|
||||
titleRes = R.string.pref_show_page_number
|
||||
defaultValue = true
|
||||
}
|
||||
preferenceCategory {
|
||||
titleRes = R.string.pager_viewer
|
||||
|
||||
switchPreference {
|
||||
key = Keys.enableTransitions
|
||||
titleRes = R.string.pref_page_transitions
|
||||
defaultValue = true
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.cropBorders
|
||||
titleRes = R.string.pref_crop_borders
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
preferenceCategory {
|
||||
titleRes = R.string.webtoon_viewer
|
||||
|
||||
switchPreference {
|
||||
key = Keys.keepScreenOn
|
||||
titleRes = R.string.pref_keep_screen_on
|
||||
defaultValue = true
|
||||
key = Keys.cropBordersWebtoon
|
||||
titleRes = R.string.pref_crop_borders
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
preferenceCategory {
|
||||
titleRes = R.string.pref_reader_navigation
|
||||
|
@ -36,7 +36,7 @@ fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT)
|
||||
* @param duration the duration of the toast. Defaults to short.
|
||||
*/
|
||||
fun Context.toast(text: String?, duration: Int = Toast.LENGTH_SHORT) {
|
||||
Toast.makeText(this, text, duration).show()
|
||||
Toast.makeText(this, text.orEmpty(), duration).show()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -47,6 +47,10 @@ inline fun View.gone() {
|
||||
visibility = View.GONE
|
||||
}
|
||||
|
||||
inline fun View.visibleIf(block: () -> Boolean) {
|
||||
visibility = if (block()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a TextDrawable determined by input
|
||||
*
|
||||
|
@ -105,7 +105,7 @@
|
||||
tools:text="122"
|
||||
tools:visibility="visible"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintRight_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -63,7 +63,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_more_vert_black_24dp"
|
||||
app:layout_constraintRight_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="16dp"
|
||||
@ -82,7 +82,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="DOWNLOADED"
|
||||
android:textAllCaps="true"
|
||||
app:layout_constraintRight_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginRight="16dp"/>
|
||||
|
||||
|
@ -27,13 +27,16 @@
|
||||
<TextView
|
||||
android:id="@+id/ext_title"
|
||||
style="@style/TextAppearance.Regular"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Regular.SubHeading"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintStart_toEndOf="@id/image"
|
||||
app:layout_constraintEnd_toStartOf="@id/ext_button"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/lang"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
|
@ -259,19 +259,22 @@
|
||||
<android.support.v4.widget.NestedScrollView
|
||||
android:id="@+id/description_scrollview"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/manga_genres_tags"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/manga_summary_label"
|
||||
app:layout_constraintBottom_toTopOf="@id/manga_genres_tags">
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintVertical_chainStyle="packed">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_summary"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:textIsSelectable="false" />
|
||||
|
||||
</android.support.v4.widget.NestedScrollView>
|
||||
@ -279,12 +282,15 @@
|
||||
<me.gujun.android.taggroup.TagGroup
|
||||
android:id="@+id/manga_genres_tags"
|
||||
style="@style/TagGroup"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/description_scrollview"
|
||||
|
@ -171,6 +171,12 @@
|
||||
android:layout_height="wrap_content"
|
||||
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:id="@+id/fullscreen"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -1,10 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.constraint.ConstraintLayout
|
||||
<LinearLayout
|
||||
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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
@ -14,8 +13,13 @@
|
||||
android:hint="@string/title"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:maxLines="1"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
android:inputType="text"
|
||||
android:maxLines="1"/>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
@ -24,11 +28,8 @@
|
||||
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"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="invisible"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<ListView
|
||||
@ -36,9 +37,8 @@
|
||||
style="@style/Theme.Widget.CardView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="40dp"
|
||||
android:choiceMode="singleChoice"
|
||||
android:clipToPadding="false"
|
||||
android:choiceMode="singleChoice"
|
||||
android:divider="@null"
|
||||
android:dividerHeight="10dp"
|
||||
android:footerDividersEnabled="true"
|
||||
@ -47,19 +47,15 @@
|
||||
android:paddingBottom="4dp"
|
||||
android:paddingTop="4dp"
|
||||
android:scrollbars="none"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/track_search"
|
||||
android:visibility="invisible"
|
||||
tools:listitem="@layout/track_search_item"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/divider"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/track_search_list"/>
|
||||
android:background="?android:attr/divider"/>
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
@ -12,7 +12,6 @@
|
||||
android:layout_height="216dp"
|
||||
android:background="?attr/selectable_list_drawable"
|
||||
android:orientation="horizontal">
|
||||
>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/track_search_cover"
|
||||
|
@ -1,5 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<changelog bulletedList="true">
|
||||
<changelogversion versionName="v0.7.4" changeDate="">
|
||||
<changelogtext>Updated Anilist's API to v2.</changelogtext>
|
||||
|
||||
<changelogtext>Added Github link to about.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed indonesian language not working.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed an issue on KitKat that crashed the app when scheduling updates.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed a few more issues introduced on the previous release.</changelogtext>
|
||||
</changelogversion>
|
||||
|
||||
<changelogversion versionName="v0.7.3" changeDate="">
|
||||
<changelogtext>Fixed the tracking search layout when there are many results.</changelogtext>
|
||||
|
||||
<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="">
|
||||
<changelogtext>Fixed missing downloaded label in chapters screen.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed updater in KitKat and lower due to TLS.</changelogtext>
|
||||
</changelogversion>
|
||||
|
||||
<changelogversion versionName="v0.7.1" changeDate="">
|
||||
<changelogtext>Updated Cloudflare bypass.</changelogtext>
|
||||
|
||||
<changelogtext>Enabled TLS 1.1 and TLS 1.2 on Android KitKat and lower.</changelogtext>
|
||||
|
||||
<changelogtext>Minor UI changes.</changelogtext>
|
||||
</changelogversion>
|
||||
|
||||
<changelogversion versionName="v0.7.0" changeDate="">
|
||||
<changelogtext>Added extensions support. You can now install and update extensions within the app.
|
||||
If you installed any extension previously through F-Droid, you'll have to uninstall them first.</changelogtext>
|
||||
@ -169,54 +209,4 @@
|
||||
<changelogtext>Fixed lost covers on some devices.</changelogtext>
|
||||
</changelogversion>
|
||||
|
||||
<changelogversion versionName="v0.4.2" changeDate="">
|
||||
<changelogtext>Added support for Anilist and Kitsu.</changelogtext>
|
||||
|
||||
<changelogtext>Added library refresh option to library updates tab.</changelogtext>
|
||||
|
||||
<changelogtext>Back button closes drawers before exiting the app.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed issues when using custom app language.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed updater in Android N.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed Mangafox search.</changelogtext>
|
||||
</changelogversion>
|
||||
|
||||
<changelogversion versionName="v0.4.1" changeDate="">
|
||||
<changelogtext>Added an app's language selector.</changelogtext>
|
||||
|
||||
<changelogtext>Added options to sort the library and merged them with the filters.</changelogtext>
|
||||
|
||||
<changelogtext>Added an option to automatically download chapters.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed performance issues when using a custom downloads directory, especially in the library updates tab.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed gesture conflicts with the contextual menu and the webtoon reader.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed wrong page direction when using volume keys for the right to left reader.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed many crashes.</changelogtext>
|
||||
</changelogversion>
|
||||
|
||||
<changelogversion versionName="v0.4.0" changeDate="">
|
||||
<changelogtext>The download manager has been rewritten and it's possible some of your downloads
|
||||
aren't recognized anymore. It's recommended to manually delete everything and start over.
|
||||
</changelogtext>
|
||||
|
||||
<changelogtext>Now it's possible to download to any folder in a SD card.</changelogtext>
|
||||
|
||||
<changelogtext>The download directory setting has been reset.</changelogtext>
|
||||
|
||||
<changelogtext>Active downloads now persist after restarts.</changelogtext>
|
||||
|
||||
<changelogtext>Allow to bookmark chapters.</changelogtext>
|
||||
|
||||
<changelogtext>Allow to share or save a single page while reading with a long tap.</changelogtext>
|
||||
|
||||
<changelogtext>Added italian translation.</changelogtext>
|
||||
|
||||
<changelogtext>Image is now the default decoder.</changelogtext>
|
||||
</changelogversion>
|
||||
|
||||
</changelog>
|
||||
|
@ -57,7 +57,7 @@
|
||||
<string name="action_retry">Réessayer</string>
|
||||
<string name="action_remove">Supprimer</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_display_mode">Changer le mode d\'affichage</string>
|
||||
<string name="action_display">Affichage</string>
|
||||
@ -152,9 +152,9 @@
|
||||
<string name="rotation_force_portrait">Forcer portrait</string>
|
||||
<string name="rotation_force_landscape">Forcer paysage</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_a_value">A</string>
|
||||
<string name="color_filter_a_value">O</string>
|
||||
|
||||
|
||||
<!-- Downloads section -->
|
||||
@ -164,7 +164,7 @@
|
||||
<string name="pref_remove_after_read">Supprimer après avoir lu</string>
|
||||
<string name="disabled">Désactivé</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="fourth_to_last">Du quatrième au dernier chapitre</string>
|
||||
<string name="pref_download_new">Télécharger les nouveaux chapitres</string>
|
||||
@ -286,7 +286,7 @@
|
||||
|
||||
<!-- Reader activity -->
|
||||
<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="page_downloaded">Page copiée vers %1$s</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_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_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_no_connection_title">Synchronisation annulée</string>
|
||||
<string name="notification_no_connection_body">Connexion non disponible</string>
|
||||
@ -327,12 +327,12 @@
|
||||
<string name="update_check_title">Nouvelle mise à jour disponible !</string>
|
||||
<string name="update_check_confirm">Télécharger</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_look_for_updates">Vérification des mises à jour</string>
|
||||
|
||||
<!--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_complete">Téléchargement terminé</string>
|
||||
<string name="update_check_notification_download_error">Erreur lors du téléchargement</string>
|
||||
@ -400,7 +400,7 @@
|
||||
<string name="pref_backup_directory">Dossier de sauvegarde</string>
|
||||
<string name="pref_backup_service_category">Service</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="file_saved">Fichier enregistré dans %1$s</string>
|
||||
<string name="backup_choice">Que voulez-vous sauvegarder ?</string>
|
||||
@ -439,7 +439,7 @@
|
||||
|
||||
<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="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>
|
||||
<string name="action_global_search">Recherche globale</string>
|
||||
<string name="action_open">Ouvrir</string>
|
||||
|
@ -27,7 +27,7 @@
|
||||
<!-- Attributes specific for SDK 21 and up -->
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@color/colorAmoled</item>
|
||||
<item name="android:navigationBarColor">@color/colorAmoledPrimary</item>
|
||||
</style>
|
||||
|
||||
<!--==============-->
|
||||
|
@ -1,12 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Application Colors -->
|
||||
<color name="colorPrimary">#54759e</color>
|
||||
<color name="colorPrimaryDark">#435e7e</color>
|
||||
<color name="colorPrimary">#54759E</color>
|
||||
<color name="colorPrimaryDark">#435E7E</color>
|
||||
<!-- Dark Application Colors -->
|
||||
<color name="colorDarkPrimary">#212121</color>
|
||||
<color name="colorDarkPrimaryDark">#1c1c1d</color>
|
||||
<color name="colorAmoled">@color/md_black_1000</color>
|
||||
<color name="colorDarkPrimaryDark">#1C1C1D</color>
|
||||
<color name="colorAmoledPrimary">@color/md_black_1000</color>
|
||||
|
||||
<!-- Light Theme -->
|
||||
<color name="colorAccentLight">@color/md_blue_A400</color>
|
||||
@ -27,7 +27,7 @@
|
||||
<color name="iconColorLight">@color/md_black_1000</color>
|
||||
|
||||
<!-- Dark Theme -->
|
||||
<color name="colorAccentDark">#3399ff</color>
|
||||
<color name="colorAccentDark">#3399FF</color>
|
||||
<color name="textColorPrimaryDark">@color/md_white_1000</color>
|
||||
<color name="textColorSecondaryDark">@color/md_white_1000_70</color>
|
||||
<color name="textColorHintDark">@color/md_white_1000_50</color>
|
||||
@ -36,7 +36,7 @@
|
||||
|
||||
<color name="statusBarDark">@color/md_black_1000</color>
|
||||
<color name="appBarDark">@color/md_grey_900</color>
|
||||
<color name="backgroundDark">#1c1c1d</color>
|
||||
<color name="backgroundDark">@color/colorDarkPrimaryDark</color>
|
||||
<color name="dialogDark">@color/colorDarkPrimary</color>
|
||||
<color name="dialog_amoled">@color/colorDarkPrimaryDark</color>
|
||||
|
||||
|
@ -190,6 +190,7 @@
|
||||
<string name="right_to_left_viewer">Right to left</string>
|
||||
<string name="vertical_viewer">Vertical</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_scale_type">Scale type</string>
|
||||
<string name="scale_type_fit_screen">Fit screen</string>
|
||||
@ -384,6 +385,7 @@
|
||||
<string name="dropped">Dropped</string>
|
||||
<string name="on_hold">On hold</string>
|
||||
<string name="plan_to_read">Plan to read</string>
|
||||
<string name="repeating">Re-reading</string>
|
||||
<string name="score">Score</string>
|
||||
<string name="title">Title</string>
|
||||
<string name="status">Status</string>
|
||||
|
@ -66,6 +66,7 @@
|
||||
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
|
||||
<item name="actionBarPopupTheme">@style/ThemeOverlay.AppCompat</item>
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Material</item>
|
||||
<item name="md_background_color">@color/dialogDark</item>
|
||||
<item name="alertDialogTheme">@style/Theme.AlertDialog.Dark</item>
|
||||
|
||||
|
||||
@ -86,8 +87,8 @@
|
||||
<!-- Amoled Theme -->
|
||||
<!--==============-->
|
||||
<style name="Theme.Base.Amoled" parent="Theme.Base.Dark">
|
||||
<item name="colorPrimary">@color/colorAmoled</item>
|
||||
<item name="colorPrimaryDark">@color/colorAmoled</item>
|
||||
<item name="colorPrimary">@color/colorAmoledPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorAmoledPrimary</item>
|
||||
<item name="android:colorBackground">@color/md_black_1000</item>
|
||||
|
||||
<!-- Custom Attributes-->
|
||||
|
@ -7,9 +7,10 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.0.1'
|
||||
classpath 'com.android.tools.build:gradle:3.1.2'
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
|
||||
classpath 'com.github.zellius:android-shortcut-gradle-plugin:0.1.2'
|
||||
classpath 'com.google.gms:google-services:3.2.0'
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Wed Oct 25 23:17:30 CEST 2017
|
||||
#Thu Apr 05 09:21:32 CEST 2018
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
||||
|
Reference in New Issue
Block a user