mirror of
https://github.com/mihonapp/mihon.git
synced 2025-07-02 05:57:50 +02:00
Compare commits
96 Commits
Author | SHA1 | Date | |
---|---|---|---|
82141cec6e | |||
218313428f | |||
44b47b49bc | |||
2f69317f5d | |||
e1eff7b744 | |||
3aa12281c3 | |||
72920130c0 | |||
0fd00331e1 | |||
7a4763ee68 | |||
647a78b791 | |||
82faa91ce3 | |||
ad664dfb9f | |||
005ac9e732 | |||
d8e3fe542d | |||
c40e4f6c5a | |||
9bb3195bca | |||
eee0bc4985 | |||
a24d670f54 | |||
51e049ab78 | |||
01e37dfab8 | |||
74087edebb | |||
db58c9b77f | |||
c4dad1c20b | |||
cae04656b9 | |||
aa57b1bc77 | |||
491d476cac | |||
f0053a2f78 | |||
10e7a3b35b | |||
4147fd6b19 | |||
2bb903088e | |||
c90f985fcc | |||
2ebaacfc89 | |||
c339bd49d0 | |||
c349fb0e37 | |||
bc825bdefa | |||
ed49ce8e1d | |||
ad2ecd538d | |||
ff8e3f0af4 | |||
698e17178a | |||
ebeee70931 | |||
b8b118bdeb | |||
5ddd7d1b14 | |||
450b23436f | |||
89793ac338 | |||
c456812a46 | |||
6f8f6e9233 | |||
5770d00f81 | |||
a0fb1eff4a | |||
89dc240a22 | |||
ee4f069341 | |||
011bb9f5b1 | |||
08b06e1b4e | |||
0416a2ff15 | |||
3a7cdfcaa4 | |||
c36a47576d | |||
6c9135c093 | |||
80ea9001b3 | |||
24bb94ceac | |||
0f16351f5f | |||
b60b26bbd0 | |||
86e53e08de | |||
d3cb10a74e | |||
7d6cfff719 | |||
cc0fe0a1a9 | |||
25327342fb | |||
e02cf67f85 | |||
bf4bef6d62 | |||
76645bce6e | |||
9276c491bc | |||
fa59b4f8a7 | |||
934a37c36b | |||
5362f62078 | |||
ccd360687e | |||
5a2e8a838c | |||
3abae1cc75 | |||
b68ef8c983 | |||
d5f5ba95bb | |||
e8638cb0b3 | |||
62f9071adc | |||
1d079dd9a4 | |||
cccb56bda1 | |||
5d8dc241d8 | |||
9ba7312caf | |||
8ebda219c4 | |||
47f14e8555 | |||
974a24d03b | |||
15f225537e | |||
a32572fc96 | |||
be3ed9b6af | |||
a0939e1c48 | |||
003dca9d45 | |||
5c1770247c | |||
021dde66eb | |||
5840a3e1e2 | |||
7c6478fe6b | |||
68aca55e6f |
24
.gitattributes
vendored
Normal file
24
.gitattributes
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
* text=auto
|
||||||
|
* text eol=lf
|
||||||
|
|
||||||
|
# Windows forced line-endings
|
||||||
|
/.idea/* text eol=crlf
|
||||||
|
|
||||||
|
# Gradle wrapper
|
||||||
|
*.jar binary
|
||||||
|
|
||||||
|
# Images
|
||||||
|
*.webp binary
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.gz binary
|
||||||
|
*.zip binary
|
||||||
|
*.7z binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.woff binary
|
||||||
|
*.pyc binary
|
||||||
|
*.swp binary
|
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
github: inorichi
|
||||||
|
ko_fi: inorichi
|
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: "🐞 Bug report"
|
||||||
|
about: Report a bug
|
||||||
|
title: "[Bug] Write short description here"
|
||||||
|
labels: "bug"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Device information
|
||||||
|
* Tachiyomi version: ?
|
||||||
|
* Android version: ?
|
||||||
|
|
||||||
|
## Steps to reproduce
|
||||||
|
1. First step
|
||||||
|
2. Second step
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
This should happen.
|
||||||
|
|
||||||
|
### Actual behavior
|
||||||
|
This happened instead.
|
||||||
|
|
||||||
|
### Other details
|
||||||
|
Additional details and attachments.
|
12
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: "🌟 Feature request"
|
||||||
|
about: Suggest a feature to improve Tachiyomi
|
||||||
|
title: "[Feature Request] Write short description here"
|
||||||
|
labels: "feature"
|
||||||
|
|
||||||
|
---
|
||||||
|
### Why/User Benefit/User Problem
|
||||||
|
(explain why this feature should be added)
|
||||||
|
|
||||||
|
### What/Requirements
|
||||||
|
(explain how this feature would behave)
|
BIN
.github/readme-images/screens.png
vendored
BIN
.github/readme-images/screens.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 1.0 MiB |
@ -1,8 +1,9 @@
|
|||||||
|
dist: trusty
|
||||||
language: android
|
language: android
|
||||||
android:
|
android:
|
||||||
components:
|
components:
|
||||||
- build-tools-28.0.3
|
- build-tools-29.0.2
|
||||||
- android-27
|
- android-28
|
||||||
- extra-android-m2repository
|
- extra-android-m2repository
|
||||||
- extra-google-m2repository
|
- extra-google-m2repository
|
||||||
- extra-android-support
|
- extra-android-support
|
||||||
@ -10,7 +11,7 @@ android:
|
|||||||
licenses:
|
licenses:
|
||||||
- android-sdk-license-.+
|
- android-sdk-license-.+
|
||||||
before_install:
|
before_install:
|
||||||
- yes | sdkmanager "platforms;android-27" # workaround for accepting the license
|
- yes | sdkmanager "platforms;android-28" # workaround for accepting the license
|
||||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
||||||
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
|
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
|
||||||
tar xf secrets.tar;
|
tar xf secrets.tar;
|
||||||
|
14
README.md
14
README.md
@ -1,6 +1,6 @@
|
|||||||
| Build | Stable | Dev | Contribute | Contact |
|
| Build | Stable | Dev | Contribute | Support Server |
|
||||||
|-------|----------|---------|------------|---------|
|
|-------|----------|---------|------------|---------|
|
||||||
| [](https://travis-ci.org/inorichi/tachiyomi) | [)](https://github.com/inorichi/tachiyomi/releases) | [](http://tachiyomi.kanade.eu/latest) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
| [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) | [](http://tachiyomi.kanade.eu/latest) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
|
|
||||||
# Tachiyomi
|
# Tachiyomi
|
||||||
@ -11,10 +11,10 @@ Tachiyomi is a free and open source manga reader for Android.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
Features include:
|
Features include:
|
||||||
* Online reading from sources such as KissManga, MangaFox, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
* Online reading from sources such as KissManga, MangaDex, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
||||||
* Local reading of downloaded manga
|
* Local reading of downloaded manga
|
||||||
* Configurable reader with multiple viewers, reading directions and other settings
|
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), and [Kitsu](https://kitsu.io/explore/anime) support
|
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/explore/anime), and [Shikimori](https://shikimori.one) support
|
||||||
* Categories to organize your library
|
* Categories to organize your library
|
||||||
* Light and dark themes
|
* Light and dark themes
|
||||||
* Schedule updating your library for new chapters
|
* Schedule updating your library for new chapters
|
||||||
@ -23,7 +23,7 @@ Features include:
|
|||||||
## Download
|
## Download
|
||||||
Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases).
|
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)
|
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).
|
||||||
|
|
||||||
## Issues, Feature Requests and Contributing
|
## Issues, Feature Requests and Contributing
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ Catalogue requests should be created at https://github.com/inorichi/tachiyomi-ex
|
|||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
[See our wiki.](https://github.com/inorichi/tachiyomi/wiki/FAQ)
|
[See our website.](https://tachiyomi.org/)
|
||||||
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
|
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
@ -29,17 +29,17 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 27
|
compileSdkVersion 28
|
||||||
buildToolsVersion '28.0.3'
|
buildToolsVersion '29.0.2'
|
||||||
publishNonDefault true
|
publishNonDefault true
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "eu.kanade.tachiyomi"
|
applicationId "eu.kanade.tachiyomi"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 27
|
targetSdkVersion 28
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 41
|
versionCode 42
|
||||||
versionName "0.8.4"
|
versionName "0.8.5"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@ -73,7 +73,6 @@ android {
|
|||||||
dimension "default"
|
dimension "default"
|
||||||
}
|
}
|
||||||
dev {
|
dev {
|
||||||
minSdkVersion 21
|
|
||||||
resConfigs "en", "xxhdpi"
|
resConfigs "en", "xxhdpi"
|
||||||
dimension "default"
|
dimension "default"
|
||||||
}
|
}
|
||||||
@ -92,6 +91,14 @@ android {
|
|||||||
checkReleaseBuilds false
|
checkReleaseBuilds false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = 1.8
|
||||||
|
targetCompatibility = 1.8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@ -101,7 +108,7 @@ dependencies {
|
|||||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||||
|
|
||||||
// Android support library
|
// Android support library
|
||||||
final support_library_version = '27.0.2'
|
final support_library_version = '28.0.0'
|
||||||
implementation "com.android.support:support-v4:$support_library_version"
|
implementation "com.android.support:support-v4:$support_library_version"
|
||||||
implementation "com.android.support:appcompat-v7:$support_library_version"
|
implementation "com.android.support:appcompat-v7:$support_library_version"
|
||||||
implementation "com.android.support:cardview-v7:$support_library_version"
|
implementation "com.android.support:cardview-v7:$support_library_version"
|
||||||
@ -111,7 +118,7 @@ dependencies {
|
|||||||
implementation "com.android.support:support-annotations:$support_library_version"
|
implementation "com.android.support:support-annotations:$support_library_version"
|
||||||
implementation "com.android.support:customtabs:$support_library_version"
|
implementation "com.android.support:customtabs:$support_library_version"
|
||||||
|
|
||||||
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
|
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||||
|
|
||||||
implementation 'com.android.support:multidex:1.0.3'
|
implementation 'com.android.support:multidex:1.0.3'
|
||||||
|
|
||||||
@ -119,10 +126,10 @@ dependencies {
|
|||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
implementation 'io.reactivex:rxjava:1.3.6'
|
implementation 'io.reactivex:rxjava:1.3.8'
|
||||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||||
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||||
implementation 'com.github.pwittchen:reactivenetwork:0.7.0'
|
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
implementation "com.squareup.okhttp3:okhttp:3.10.0"
|
implementation "com.squareup.okhttp3:okhttp:3.10.0"
|
||||||
@ -146,7 +153,7 @@ dependencies {
|
|||||||
implementation 'com.github.inorichi:unifile:e9ee588'
|
implementation 'com.github.inorichi:unifile:e9ee588'
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation 'org.jsoup:jsoup:1.10.2'
|
implementation 'org.jsoup:jsoup:1.12.1'
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation 'com.evernote:android-job:1.2.5'
|
implementation 'com.evernote:android-job:1.2.5'
|
||||||
@ -156,7 +163,7 @@ dependencies {
|
|||||||
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation 'android.arch.persistence:db:1.0.0'
|
implementation 'android.arch.persistence:db:1.1.1'
|
||||||
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
||||||
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
|
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
|
||||||
implementation 'io.requery:sqlite-android:3.25.2'
|
implementation 'io.requery:sqlite-android:3.25.2'
|
||||||
@ -179,14 +186,11 @@ dependencies {
|
|||||||
implementation 'jp.wasabeef:glide-transformations:3.1.1'
|
implementation 'jp.wasabeef:glide-transformations:3.1.1'
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation 'com.jakewharton.timber:timber:4.6.1'
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
implementation 'ch.acra:acra:4.9.2'
|
implementation 'ch.acra:acra:4.9.2'
|
||||||
|
|
||||||
// Sort
|
|
||||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||||
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||||
@ -228,13 +232,12 @@ dependencies {
|
|||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
final coroutines_version = '0.22.2'
|
final coroutines_version = '1.3.3'
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.2.71'
|
ext.kotlin_version = '1.3.61'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
@ -247,10 +250,9 @@ repositories {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
||||||
experimental {
|
tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all {
|
||||||
coroutines 'enable'
|
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"]
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
androidExtensions {
|
androidExtensions {
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@ -16,6 +18,7 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@ -62,8 +65,8 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.ShikomoriLoginActivity"
|
android:name=".ui.setting.ShikimoriLoginActivity"
|
||||||
android:label="Shikomori">
|
android:label="Shikimori">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -75,6 +78,20 @@
|
|||||||
android:scheme="tachiyomi" />
|
android:scheme="tachiyomi" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.BangumiLoginActivity"
|
||||||
|
android:label="Bangumi">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="bangumi-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
|
@ -60,7 +60,7 @@ class CoverCache(private val context: Context) {
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
// Remove file.
|
// Remove file.
|
||||||
val file = getCoverFile(thumbnailUrl!!)
|
val file = getCoverFile(thumbnailUrl)
|
||||||
return file.exists() && file.delete()
|
return file.exists() && file.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +82,11 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaViewerPutResolver())
|
.withPutResolver(MangaViewerPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateMangaTitle(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaTitlePutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||||
|
|
||||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
class MangaTitlePutResolver : PutResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
|
val contentValues = mapToContentValues(manga)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
|
put(MangaTable.COL_TITLE, manga.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -20,10 +20,10 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_VIEWER, manga.viewer)
|
put(MangaTable.COL_VIEWER, manga.viewer)
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.download
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@ -28,7 +29,9 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* The root directory for downloads.
|
* The root directory for downloads.
|
||||||
*/
|
*/
|
||||||
private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let {
|
private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let {
|
||||||
UniFile.fromUri(context, Uri.parse(it))
|
val dir = UniFile.fromUri(context, Uri.parse(it))
|
||||||
|
DiskUtil.createNoMediaFile(dir, context)
|
||||||
|
dir
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -44,9 +47,13 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param source the source of the manga.
|
* @param source the source of the manga.
|
||||||
*/
|
*/
|
||||||
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
|
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
|
||||||
return downloadsDir
|
try {
|
||||||
.createDirectory(getSourceDirName(source))
|
return downloadsDir
|
||||||
.createDirectory(getMangaDirName(manga))
|
.createDirectory(getSourceDirName(source))
|
||||||
|
.createDirectory(getMangaDirName(manga))
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -132,7 +132,7 @@ class DownloadService : Service() {
|
|||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ state -> onNetworkStateChanged(state)
|
.subscribe({ state -> onNetworkStateChanged(state)
|
||||||
}, { _ ->
|
}, {
|
||||||
toast(R.string.download_queue_error)
|
toast(R.string.download_queue_error)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
})
|
})
|
||||||
|
@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||||
import eu.kanade.tachiyomi.util.*
|
import eu.kanade.tachiyomi.util.*
|
||||||
import kotlinx.coroutines.experimental.async
|
import kotlinx.coroutines.async
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -102,7 +102,7 @@ class Downloader(
|
|||||||
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
||||||
|
|
||||||
downloadsRelay.call(pending)
|
downloadsRelay.call(pending)
|
||||||
return !pending.isEmpty()
|
return pending.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -199,7 +199,7 @@ class Downloader(
|
|||||||
*/
|
*/
|
||||||
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
|
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
|
||||||
|
val wasEmpty = queue.isEmpty()
|
||||||
// Called in background thread, the operation can be slow with SAF.
|
// Called in background thread, the operation can be slow with SAF.
|
||||||
val chaptersWithoutDir = async {
|
val chaptersWithoutDir = async {
|
||||||
val mangaDir = provider.findMangaDir(manga, source)
|
val mangaDir = provider.findMangaDir(manga, source)
|
||||||
@ -232,7 +232,7 @@ class Downloader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start downloader if needed
|
// Start downloader if needed
|
||||||
if (autoStart) {
|
if (autoStart && wasEmpty) {
|
||||||
DownloadService.start(this@Downloader.context)
|
DownloadService.start(this@Downloader.context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -407,6 +407,8 @@ class Downloader(
|
|||||||
if (download.status == Download.DOWNLOADED) {
|
if (download.status == Download.DOWNLOADED) {
|
||||||
tmpDir.renameTo(dirname)
|
tmpDir.renameTo(dirname)
|
||||||
cache.addChapter(dirname, mangaDir, download.manga)
|
cache.addChapter(dirname, mangaDir, download.manga)
|
||||||
|
|
||||||
|
DiskUtil.createNoMediaFile(tmpDir, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class will provide various functions to Rank mangas to efficiently schedule mangas to update.
|
||||||
|
*/
|
||||||
|
object LibraryUpdateRanker {
|
||||||
|
|
||||||
|
val rankingScheme = listOf(
|
||||||
|
(this::lexicographicRanking)(),
|
||||||
|
(this::latestFirstRanking)())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a total ordering over all the Mangas.
|
||||||
|
*
|
||||||
|
* Assumption: An active [Manga] mActive is expected to have been last updated after an
|
||||||
|
* inactive [Manga] mInactive.
|
||||||
|
*
|
||||||
|
* Using this insight, function returns a Comparator for which mActive appears before mInactive.
|
||||||
|
* @return a Comparator that ranks manga based on relevance.
|
||||||
|
*/
|
||||||
|
fun latestFirstRanking(): Comparator<Manga> {
|
||||||
|
return Comparator { mangaFirst: Manga,
|
||||||
|
mangaSecond: Manga ->
|
||||||
|
compareValues(mangaSecond.last_update, mangaFirst.last_update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a total ordering over all the Mangas.
|
||||||
|
*
|
||||||
|
* Order the manga lexicographically.
|
||||||
|
* @return a Comparator that ranks manga lexicographically based on the title.
|
||||||
|
*/
|
||||||
|
fun lexicographicRanking(): Comparator<Manga> {
|
||||||
|
return Comparator { mangaFirst: Manga,
|
||||||
|
mangaSecond: Manga ->
|
||||||
|
compareValues(mangaFirst.title, mangaSecond.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
@ -204,7 +205,9 @@ class LibraryUpdateService(
|
|||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||||
subscription = Observable
|
subscription = Observable
|
||||||
.defer {
|
.defer {
|
||||||
|
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
||||||
val mangaList = getMangaToUpdate(intent, target)
|
val mangaList = getMangaToUpdate(intent, target)
|
||||||
|
.sortedWith(rankingScheme[selectedScheme])
|
||||||
|
|
||||||
// Update either chapter list or manga details.
|
// Update either chapter list or manga details.
|
||||||
when (target) {
|
when (target) {
|
||||||
@ -246,7 +249,6 @@ class LibraryUpdateService(
|
|||||||
else
|
else
|
||||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val colorFilterValue = "color_filter_value"
|
const val colorFilterValue = "color_filter_value"
|
||||||
|
|
||||||
|
const val colorFilterMode = "color_filter_mode"
|
||||||
|
|
||||||
const val defaultViewer = "pref_default_viewer_key"
|
const val defaultViewer = "pref_default_viewer_key"
|
||||||
|
|
||||||
const val imageScaleType = "pref_image_scale_type_key"
|
const val imageScaleType = "pref_image_scale_type_key"
|
||||||
@ -85,6 +87,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val libraryUpdateCategories = "library_update_categories"
|
const val libraryUpdateCategories = "library_update_categories"
|
||||||
|
|
||||||
|
const val libraryUpdatePrioritization = "library_update_prioritization"
|
||||||
|
|
||||||
const val filterDownloaded = "pref_filter_downloaded_key"
|
const val filterDownloaded = "pref_filter_downloaded_key"
|
||||||
|
|
||||||
const val filterUnread = "pref_filter_unread_key"
|
const val filterUnread = "pref_filter_unread_key"
|
||||||
|
@ -57,6 +57,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0)
|
fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0)
|
||||||
|
|
||||||
|
fun colorFilterMode() = rxPrefs.getInteger(Keys.colorFilterMode, 0)
|
||||||
|
|
||||||
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1)
|
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1)
|
||||||
|
|
||||||
fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 1)
|
fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 1)
|
||||||
@ -141,6 +143,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun libraryUpdateCategories() = rxPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
fun libraryUpdateCategories() = rxPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
||||||
|
|
||||||
|
fun libraryUpdatePrioritization() = rxPrefs.getInteger(Keys.libraryUpdatePrioritization, 0)
|
||||||
|
|
||||||
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
|
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
|
||||||
|
|
||||||
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false)
|
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false)
|
||||||
|
@ -45,7 +45,7 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
|||||||
prefs.edit().putString(key, value).apply()
|
prefs.edit().putString(key, value).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String> {
|
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
|
||||||
return prefs.getStringSet(key, defValues)
|
return prefs.getStringSet(key, defValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,8 @@ import android.content.Context
|
|||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
||||||
import eu.kanade.tachiyomi.data.track.shikomori.Shikomori
|
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||||
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
|
|
||||||
class TrackManager(private val context: Context) {
|
class TrackManager(private val context: Context) {
|
||||||
|
|
||||||
@ -12,7 +13,8 @@ class TrackManager(private val context: Context) {
|
|||||||
const val MYANIMELIST = 1
|
const val MYANIMELIST = 1
|
||||||
const val ANILIST = 2
|
const val ANILIST = 2
|
||||||
const val KITSU = 3
|
const val KITSU = 3
|
||||||
const val SHIKOMORI = 4
|
const val SHIKIMORI = 4
|
||||||
|
const val BANGUMI = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
||||||
@ -21,9 +23,11 @@ class TrackManager(private val context: Context) {
|
|||||||
|
|
||||||
val kitsu = Kitsu(context, KITSU)
|
val kitsu = Kitsu(context, KITSU)
|
||||||
|
|
||||||
val shikomori = Shikomori(context, SHIKOMORI)
|
val shikimori = Shikimori(context, SHIKIMORI)
|
||||||
|
|
||||||
val services = listOf(myAnimeList, aniList, kitsu, shikomori)
|
val bangumi = Bangumi(context, BANGUMI)
|
||||||
|
|
||||||
|
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi)
|
||||||
|
|
||||||
fun getService(id: Int) = services.find { it.id == id }
|
fun getService(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
|
@ -21,13 +21,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
private val jsonMime = MediaType.parse("application/json; charset=utf-8")
|
private val jsonMime = MediaType.parse("application/json; charset=utf-8")
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
|
|
||||||
fun addLibManga(track: Track): Observable<Track> {
|
fun addLibManga(track: Track): Observable<Track> {
|
||||||
val query = """
|
val query = """
|
||||||
mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||||
SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status)
|
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
||||||
{ id status } }
|
| id
|
||||||
"""
|
| status
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|""".trimMargin()
|
||||||
val variables = jsonObject(
|
val variables = jsonObject(
|
||||||
"mangaId" to track.media_id,
|
"mangaId" to track.media_id,
|
||||||
"progress" to track.last_chapter_read,
|
"progress" to track.last_chapter_read,
|
||||||
@ -58,14 +60,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
fun updateLibManga(track: Track): Observable<Track> {
|
fun updateLibManga(track: Track): Observable<Track> {
|
||||||
val query = """
|
val query = """
|
||||||
mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
||||||
SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
||||||
id
|
|id
|
||||||
status
|
|status
|
||||||
progress
|
|progress
|
||||||
}
|
|}
|
||||||
}
|
|}
|
||||||
"""
|
|""".trimMargin()
|
||||||
val variables = jsonObject(
|
val variables = jsonObject(
|
||||||
"listId" to track.library_id,
|
"listId" to track.library_id,
|
||||||
"progress" to track.last_chapter_read,
|
"progress" to track.last_chapter_read,
|
||||||
@ -90,29 +92,29 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
fun search(search: String): Observable<List<TrackSearch>> {
|
fun search(search: String): Observable<List<TrackSearch>> {
|
||||||
val query = """
|
val query = """
|
||||||
query Search(${'$'}query: String) {
|
|query Search(${'$'}query: String) {
|
||||||
Page (perPage: 50) {
|
|Page (perPage: 50) {
|
||||||
media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||||
id
|
|id
|
||||||
title {
|
|title {
|
||||||
romaji
|
|romaji
|
||||||
}
|
|}
|
||||||
coverImage {
|
|coverImage {
|
||||||
large
|
|large
|
||||||
}
|
|}
|
||||||
type
|
|type
|
||||||
status
|
|status
|
||||||
chapters
|
|chapters
|
||||||
description
|
|description
|
||||||
startDate {
|
|startDate {
|
||||||
year
|
|year
|
||||||
month
|
|month
|
||||||
day
|
|day
|
||||||
}
|
|}
|
||||||
}
|
|}
|
||||||
}
|
|}
|
||||||
}
|
|}
|
||||||
"""
|
|""".trimMargin()
|
||||||
val variables = jsonObject(
|
val variables = jsonObject(
|
||||||
"query" to search
|
"query" to search
|
||||||
)
|
)
|
||||||
@ -142,37 +144,37 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun findLibManga(track: Track, userid: Int) : Observable<Track?> {
|
fun findLibManga(track: Track, userid: Int): Observable<Track?> {
|
||||||
val query = """
|
val query = """
|
||||||
query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||||
Page {
|
|Page {
|
||||||
mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||||
id
|
|id
|
||||||
status
|
|status
|
||||||
scoreRaw: score(format: POINT_100)
|
|scoreRaw: score(format: POINT_100)
|
||||||
progress
|
|progress
|
||||||
media{
|
|media {
|
||||||
id
|
|id
|
||||||
title {
|
|title {
|
||||||
romaji
|
|romaji
|
||||||
}
|
|}
|
||||||
coverImage {
|
|coverImage {
|
||||||
large
|
|large
|
||||||
}
|
|}
|
||||||
type
|
|type
|
||||||
status
|
|status
|
||||||
chapters
|
|chapters
|
||||||
description
|
|description
|
||||||
startDate {
|
|startDate {
|
||||||
year
|
|year
|
||||||
month
|
|month
|
||||||
day
|
|day
|
||||||
}
|
|}
|
||||||
}
|
|}
|
||||||
}
|
|}
|
||||||
}
|
|}
|
||||||
}
|
|}
|
||||||
"""
|
|""".trimMargin()
|
||||||
val variables = jsonObject(
|
val variables = jsonObject(
|
||||||
"id" to userid,
|
"id" to userid,
|
||||||
"manga_id" to track.media_id
|
"manga_id" to track.media_id
|
||||||
@ -214,16 +216,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
fun getCurrentUser(): Observable<Pair<Int, String>> {
|
fun getCurrentUser(): Observable<Pair<Int, String>> {
|
||||||
val query = """
|
val query = """
|
||||||
query User
|
|query User {
|
||||||
{
|
|Viewer {
|
||||||
Viewer {
|
|id
|
||||||
id
|
|mediaListOptions {
|
||||||
mediaListOptions {
|
|scoreFormat
|
||||||
scoreFormat
|
|}
|
||||||
}
|
|}
|
||||||
}
|
|}
|
||||||
}
|
|""".trimMargin()
|
||||||
"""
|
|
||||||
val payload = jsonObject(
|
val payload = jsonObject(
|
||||||
"query" to query
|
"query" to query
|
||||||
)
|
)
|
||||||
@ -246,7 +247,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun jsonToALManga(struct: JsonObject): ALManga{
|
private fun jsonToALManga(struct: JsonObject): ALManga {
|
||||||
val date = try {
|
val date = try {
|
||||||
val date = Calendar.getInstance()
|
val date = Calendar.getInstance()
|
||||||
date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1,
|
date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1,
|
||||||
@ -261,11 +262,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
date, struct["chapters"].nullInt ?: 0)
|
date, struct["chapters"].nullInt ?: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun jsonToALUserManga(struct: JsonObject): ALUserManga{
|
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
|
||||||
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj) )
|
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val clientId = "385"
|
private const val clientId = "385"
|
||||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
private const val clientUrl = "tachiyomi://anilist-auth"
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.bangumi
|
||||||
|
|
||||||
|
data class Avatar(
|
||||||
|
val large: String? = "",
|
||||||
|
val medium: String? = "",
|
||||||
|
val small: String? = ""
|
||||||
|
)
|
@ -0,0 +1,144 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.bangumi
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
|
override fun getScoreList(): List<String> {
|
||||||
|
return IntRange(0, 10).map(Int::toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun displayScore(track: Track): String {
|
||||||
|
return track.score.toInt().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(track: Track): Observable<Track> {
|
||||||
|
return api.addLibManga(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(track: Track): Observable<Track> {
|
||||||
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
}
|
||||||
|
return api.updateLibManga(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(track: Track): Observable<Track> {
|
||||||
|
return api.statusLibManga(track)
|
||||||
|
.flatMap {
|
||||||
|
api.findLibManga(track).flatMap { remoteTrack ->
|
||||||
|
if (remoteTrack != null && it != null) {
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.library_id = remoteTrack.library_id
|
||||||
|
track.status = remoteTrack.status
|
||||||
|
track.last_chapter_read = remoteTrack.last_chapter_read
|
||||||
|
refresh(track)
|
||||||
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
add(track)
|
||||||
|
update(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||||
|
return api.search(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun refresh(track: Track): Observable<Track> {
|
||||||
|
return api.statusLibManga(track)
|
||||||
|
.flatMap {
|
||||||
|
track.copyPersonalFrom(it!!)
|
||||||
|
api.findLibManga(track)
|
||||||
|
.map { remoteTrack ->
|
||||||
|
if (remoteTrack != null) {
|
||||||
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
|
track.status = remoteTrack.status
|
||||||
|
}
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val READING = 3
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val ON_HOLD = 4
|
||||||
|
const val DROPPED = 5
|
||||||
|
const val PLANNING = 1
|
||||||
|
|
||||||
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override val name = "Bangumi"
|
||||||
|
|
||||||
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
|
private val interceptor by lazy { BangumiInterceptor(this, gson) }
|
||||||
|
|
||||||
|
private val api by lazy { BangumiApi(client, interceptor) }
|
||||||
|
|
||||||
|
override fun getLogo() = R.drawable.bangumi
|
||||||
|
|
||||||
|
override fun getLogoColor() = Color.rgb(0xF0, 0x91, 0x99)
|
||||||
|
|
||||||
|
override fun getStatusList(): List<Int> {
|
||||||
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
|
when (status) {
|
||||||
|
READING -> getString(R.string.reading)
|
||||||
|
COMPLETED -> getString(R.string.completed)
|
||||||
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
|
DROPPED -> getString(R.string.dropped)
|
||||||
|
PLANNING -> getString(R.string.plan_to_read)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun login(username: String, password: String) = login(password)
|
||||||
|
|
||||||
|
fun login(code: String): Completable {
|
||||||
|
return api.accessToken(code).map { oauth: OAuth? ->
|
||||||
|
interceptor.newAuth(oauth)
|
||||||
|
if (oauth != null) {
|
||||||
|
saveCredentials(oauth.user_id.toString(), oauth.access_token)
|
||||||
|
}
|
||||||
|
}.doOnError {
|
||||||
|
logout()
|
||||||
|
}.toCompletable()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveToken(oauth: OAuth?) {
|
||||||
|
val json = gson.toJson(oauth)
|
||||||
|
preferences.trackToken(this).set(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreToken(): OAuth? {
|
||||||
|
return try {
|
||||||
|
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logout() {
|
||||||
|
super.logout()
|
||||||
|
preferences.trackToken(this).set(null)
|
||||||
|
interceptor.newAuth(null)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,211 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.bangumi
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.array
|
||||||
|
import com.github.salomonbrys.kotson.obj
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
||||||
|
|
||||||
|
private val gson: Gson by injectLazy()
|
||||||
|
private val parser = JsonParser()
|
||||||
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
|
fun addLibManga(track: Track): Observable<Track> {
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
.add("rating", track.score.toInt().toString())
|
||||||
|
.add("status", track.toBangumiStatus())
|
||||||
|
.build()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$apiUrl/collection/${track.media_id}/update")
|
||||||
|
.post(body)
|
||||||
|
.build()
|
||||||
|
return authClient.newCall(request)
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map {
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLibManga(track: Track): Observable<Track> {
|
||||||
|
// chapter update
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
.add("watched_eps", track.last_chapter_read.toString())
|
||||||
|
.build()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$apiUrl/subject/${track.media_id}/update/watched_eps")
|
||||||
|
.post(body)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// read status update
|
||||||
|
val sbody = FormBody.Builder()
|
||||||
|
.add("status", track.toBangumiStatus())
|
||||||
|
.build()
|
||||||
|
val srequest = Request.Builder()
|
||||||
|
.url("$apiUrl/collection/${track.media_id}/update")
|
||||||
|
.post(sbody)
|
||||||
|
.build()
|
||||||
|
return authClient.newCall(srequest)
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map {
|
||||||
|
track
|
||||||
|
}.flatMap {
|
||||||
|
authClient.newCall(request)
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map {
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(search: String): Observable<List<TrackSearch>> {
|
||||||
|
val url = Uri.parse(
|
||||||
|
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}").buildUpon()
|
||||||
|
.appendQueryParameter("max_results", "20")
|
||||||
|
.build()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url.toString())
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
return authClient.newCall(request)
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { netResponse ->
|
||||||
|
var responseBody = netResponse.body()?.string().orEmpty()
|
||||||
|
if (responseBody.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
if(responseBody.contains("\"code\":404")){
|
||||||
|
responseBody = "{\"results\":0,\"list\":[]}"
|
||||||
|
}
|
||||||
|
val response = parser.parse(responseBody).obj["list"]?.array
|
||||||
|
response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||||
|
return TrackSearch.create(TrackManager.BANGUMI).apply {
|
||||||
|
media_id = obj["id"].asInt
|
||||||
|
title = obj["name_cn"].asString
|
||||||
|
cover_url = obj["images"].obj["common"].asString
|
||||||
|
summary = obj["name"].asString
|
||||||
|
tracking_url = obj["url"].asString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun jsonToTrack(mangas: JsonObject): Track {
|
||||||
|
return Track.create(TrackManager.BANGUMI).apply {
|
||||||
|
title = mangas["name"].asString
|
||||||
|
media_id = mangas["id"].asInt
|
||||||
|
score = if (mangas["rating"] != null)
|
||||||
|
(if (mangas["rating"].isJsonObject) mangas["rating"].obj["score"].asFloat else 0f)
|
||||||
|
else 0f
|
||||||
|
status = Bangumi.DEFAULT_STATUS
|
||||||
|
tracking_url = mangas["url"].asString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findLibManga(track: Track): Observable<Track?> {
|
||||||
|
val urlMangas = "$apiUrl/subject/${track.media_id}"
|
||||||
|
val requestMangas = Request.Builder()
|
||||||
|
.url(urlMangas)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return authClient.newCall(requestMangas)
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { netResponse ->
|
||||||
|
// get comic info
|
||||||
|
val responseBody = netResponse.body()?.string().orEmpty()
|
||||||
|
jsonToTrack(parser.parse(responseBody).obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun statusLibManga(track: Track): Observable<Track?> {
|
||||||
|
val urlUserRead = "$apiUrl/collection/${track.media_id}"
|
||||||
|
val requestUserRead = Request.Builder()
|
||||||
|
.url(urlUserRead)
|
||||||
|
.cacheControl(CacheControl.FORCE_NETWORK)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// todo get user readed chapter here
|
||||||
|
return authClient.newCall(requestUserRead)
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { netResponse ->
|
||||||
|
val resp = netResponse.body()?.string()
|
||||||
|
val coll = gson.fromJson(resp, Collection::class.java)
|
||||||
|
track.status = coll.status?.id!!
|
||||||
|
track.last_chapter_read = coll.ep_status!!
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun accessToken(code: String): Observable<OAuth> {
|
||||||
|
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
|
||||||
|
val responseBody = netResponse.body()?.string().orEmpty()
|
||||||
|
if (responseBody.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
gson.fromJson(responseBody, OAuth::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun accessTokenRequest(code: String) = POST(oauthUrl,
|
||||||
|
body = FormBody.Builder()
|
||||||
|
.add("grant_type", "authorization_code")
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("client_secret", clientSecret)
|
||||||
|
.add("code", code)
|
||||||
|
.add("redirect_uri", redirectUrl)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val clientId = "bgm10555cda0762e80ca"
|
||||||
|
private const val clientSecret = "8fff394a8627b4c388cbf349ec865775"
|
||||||
|
|
||||||
|
private const val baseUrl = "https://bangumi.org"
|
||||||
|
private const val apiUrl = "https://api.bgm.tv"
|
||||||
|
private const val oauthUrl = "https://bgm.tv/oauth/access_token"
|
||||||
|
private const val loginUrl = "https://bgm.tv/oauth/authorize"
|
||||||
|
|
||||||
|
private const val redirectUrl = "tachiyomi://bangumi-auth"
|
||||||
|
private const val baseMangaUrl = "$apiUrl/mangas"
|
||||||
|
|
||||||
|
fun mangaUrl(remoteId: Int): String {
|
||||||
|
return "$baseMangaUrl/$remoteId"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun authUrl() =
|
||||||
|
Uri.parse(loginUrl).buildUpon()
|
||||||
|
.appendQueryParameter("client_id", clientId)
|
||||||
|
.appendQueryParameter("response_type", "code")
|
||||||
|
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun refreshTokenRequest(token: String) = POST(oauthUrl,
|
||||||
|
body = FormBody.Builder()
|
||||||
|
.add("grant_type", "refresh_token")
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("client_secret", clientSecret)
|
||||||
|
.add("refresh_token", token)
|
||||||
|
.add("redirect_uri", redirectUrl)
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.bangumi
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth object used for authenticated requests.
|
||||||
|
*/
|
||||||
|
private var oauth: OAuth? = bangumi.restoreToken()
|
||||||
|
|
||||||
|
fun addTocken(tocken: String, oidFormBody: FormBody): FormBody {
|
||||||
|
val newFormBody = FormBody.Builder()
|
||||||
|
for (i in 0 until oidFormBody.size()) {
|
||||||
|
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
|
||||||
|
}
|
||||||
|
newFormBody.add("access_token", tocken)
|
||||||
|
return newFormBody.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
|
val currAuth = oauth ?: throw Exception("Not authenticated with Bangumi")
|
||||||
|
|
||||||
|
if (currAuth.isExpired()) {
|
||||||
|
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refresh_token!!))
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
|
||||||
|
} else {
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var authRequest = if (originalRequest.method() == "GET") originalRequest.newBuilder()
|
||||||
|
.header("User-Agent", "Tachiyomi")
|
||||||
|
.url(originalRequest.url().newBuilder()
|
||||||
|
.addQueryParameter("access_token", currAuth.access_token).build())
|
||||||
|
.build() else originalRequest.newBuilder()
|
||||||
|
.post(addTocken(currAuth.access_token, originalRequest.body() as FormBody))
|
||||||
|
.header("User-Agent", "Tachiyomi")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return chain.proceed(authRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newAuth(oauth: OAuth?) {
|
||||||
|
this.oauth = if (oauth == null) null else OAuth(
|
||||||
|
oauth.access_token,
|
||||||
|
oauth.token_type,
|
||||||
|
System.currentTimeMillis() / 1000,
|
||||||
|
oauth.expires_in,
|
||||||
|
oauth.refresh_token,
|
||||||
|
this.oauth?.user_id)
|
||||||
|
|
||||||
|
bangumi.saveToken(oauth)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.bangumi
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
|
||||||
|
fun Track.toBangumiStatus() = when (status) {
|
||||||
|
Bangumi.READING -> "do"
|
||||||
|
Bangumi.COMPLETED -> "collect"
|
||||||
|
Bangumi.ON_HOLD -> "on_hold"
|
||||||
|
Bangumi.DROPPED -> "dropped"
|
||||||
|
Bangumi.PLANNING -> "wish"
|
||||||
|
else -> throw NotImplementedError("Unknown status")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toTrackStatus(status: String) = when (status) {
|
||||||
|
"do" -> Bangumi.READING
|
||||||
|
"collect" -> Bangumi.COMPLETED
|
||||||
|
"on_hold" -> Bangumi.ON_HOLD
|
||||||
|
"dropped" -> Bangumi.DROPPED
|
||||||
|
"wish" -> Bangumi.PLANNING
|
||||||
|
|
||||||
|
else -> throw Exception("Unknown status")
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.bangumi
|
||||||
|
|
||||||
|
data class Collection(
|
||||||
|
val `private`: Int? = 0,
|
||||||
|
val comment: String? = "",
|
||||||
|
val ep_status: Int? = 0,
|
||||||
|
val lasttouch: Int? = 0,
|
||||||
|
val rating: Int? = 0,
|
||||||
|
val status: Status? = Status(),
|
||||||
|
val tag: List<String?>? = listOf(),
|
||||||
|
val user: User? = User(),
|
||||||
|
val vol_status: Int? = 0
|
||||||
|
)
|
@ -0,0 +1,16 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.bangumi
|
||||||
|
|
||||||
|
data class OAuth(
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
val created_at: Long,
|
||||||
|
val expires_in: Long,
|
||||||
|
val refresh_token: String?,
|
||||||
|
val user_id: Long?
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Access token refersh before expired
|
||||||
|
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.bangumi
|
||||||
|
|
||||||
|
data class Status(
|
||||||
|
val id: Int? = 0,
|
||||||
|
val name: String? = "",
|
||||||
|
val type: String? = ""
|
||||||
|
)
|
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.bangumi
|
||||||
|
|
||||||
|
data class User(
|
||||||
|
val avatar: Avatar? = Avatar(),
|
||||||
|
val id: Int? = 0,
|
||||||
|
val nickname: String? = "",
|
||||||
|
val sign: String? = "",
|
||||||
|
val url: String? = "",
|
||||||
|
val usergroup: Int? = 0,
|
||||||
|
val username: String? = ""
|
||||||
|
)
|
@ -18,13 +18,12 @@ class KitsuSearchManga(obj: JsonObject) {
|
|||||||
private val synopsis by obj.byString
|
private val synopsis by obj.byString
|
||||||
private var startDate = obj.get("startDate").nullString?.let {
|
private var startDate = obj.get("startDate").nullString?.let {
|
||||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
outputDf.format(Date(it!!.toLong() * 1000))
|
outputDf.format(Date(it.toLong() * 1000))
|
||||||
}
|
}
|
||||||
private val endDate = obj.get("endDate").nullString
|
private val endDate = obj.get("endDate").nullString
|
||||||
|
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
||||||
media_id = this@KitsuSearchManga.id
|
media_id = this@KitsuSearchManga.id
|
||||||
title = canonicalTitle
|
title = canonicalTitle
|
||||||
total_chapters = chapterCount ?: 0
|
total_chapters = chapterCount ?: 0
|
||||||
@ -55,7 +54,7 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
|
|||||||
private val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
|
private val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
|
||||||
val progress by obj["attributes"].byInt
|
val progress by obj["attributes"].byInt
|
||||||
|
|
||||||
open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
||||||
media_id = libraryId
|
media_id = libraryId
|
||||||
title = canonicalTitle
|
title = canonicalTitle
|
||||||
total_chapters = chapterCount ?: 0
|
total_chapters = chapterCount ?: 0
|
||||||
|
@ -10,11 +10,11 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val READING = 1
|
const val READING = 1
|
||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
const val ON_HOLD = 3
|
const val ON_HOLD = 3
|
||||||
@ -29,7 +29,8 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
const val LOGGED_IN_COOKIE = "is_logged_in"
|
const val LOGGED_IN_COOKIE = "is_logged_in"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val api by lazy { MyanimelistApi(client) }
|
private val interceptor by lazy { MyAnimeListInterceptor(this) }
|
||||||
|
private val api by lazy { MyanimelistApi(client, interceptor) }
|
||||||
|
|
||||||
override val name: String
|
override val name: String
|
||||||
get() = "MyAnimeList"
|
get() = "MyAnimeList"
|
||||||
@ -62,7 +63,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override fun add(track: Track): Observable<Track> {
|
||||||
return api.addLibManga(track, getCSRF())
|
return api.addLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
override fun update(track: Track): Observable<Track> {
|
||||||
@ -70,11 +71,11 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
track.status = COMPLETED
|
track.status = COMPLETED
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track, getCSRF())
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
override fun bind(track: Track): Observable<Track> {
|
||||||
return api.findLibManga(track, getCSRF())
|
return api.findLibManga(track)
|
||||||
.flatMap { remoteTrack ->
|
.flatMap { remoteTrack ->
|
||||||
if (remoteTrack != null) {
|
if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
@ -93,7 +94,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
override fun refresh(track: Track): Observable<Track> {
|
||||||
return api.getLibManga(track, getCSRF())
|
return api.getLibManga(track)
|
||||||
.map { remoteTrack ->
|
.map { remoteTrack ->
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
@ -104,26 +105,48 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
override fun login(username: String, password: String): Completable {
|
override fun login(username: String, password: String): Completable {
|
||||||
logout()
|
logout()
|
||||||
|
|
||||||
return api.login(username, password)
|
return Observable.fromCallable { api.login(username, password) }
|
||||||
.doOnNext { csrf -> saveCSRF(csrf) }
|
.doOnNext { csrf -> saveCSRF(csrf) }
|
||||||
.doOnNext { saveCredentials(username, password) }
|
.doOnNext { saveCredentials(username, password) }
|
||||||
.doOnError { logout() }
|
.doOnError { logout() }
|
||||||
.toCompletable()
|
.toCompletable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun refreshLogin() {
|
||||||
|
val username = getUsername()
|
||||||
|
val password = getPassword()
|
||||||
|
logout()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val csrf = api.login(username, password)
|
||||||
|
saveCSRF(csrf)
|
||||||
|
saveCredentials(username, password)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logout()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to login again if cookies have been cleared but credentials are still filled
|
||||||
|
fun ensureLoggedIn() {
|
||||||
|
if (isAuthorized) return
|
||||||
|
if (!isLogged) throw Exception("MAL Login Credentials not found")
|
||||||
|
|
||||||
|
refreshLogin()
|
||||||
|
}
|
||||||
|
|
||||||
override fun logout() {
|
override fun logout() {
|
||||||
super.logout()
|
super.logout()
|
||||||
preferences.trackToken(this).delete()
|
preferences.trackToken(this).delete()
|
||||||
networkService.cookieManager.remove(HttpUrl.parse(BASE_URL)!!)
|
networkService.cookieManager.remove(HttpUrl.parse(BASE_URL)!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val isLogged: Boolean
|
val isAuthorized: Boolean
|
||||||
get() = !getUsername().isEmpty() &&
|
get() = super.isLogged &&
|
||||||
!getPassword().isEmpty() &&
|
getCSRF().isNotEmpty() &&
|
||||||
checkCookies() &&
|
checkCookies()
|
||||||
!getCSRF().isEmpty()
|
|
||||||
|
|
||||||
private fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
|
fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
|
||||||
|
|
||||||
private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
|
private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
|
||||||
|
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
import okio.Buffer
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor {
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
myanimelist.ensureLoggedIn()
|
||||||
|
|
||||||
|
val request = chain.request()
|
||||||
|
var response = chain.proceed(updateRequest(request))
|
||||||
|
|
||||||
|
if (response.code() == 400){
|
||||||
|
myanimelist.refreshLogin()
|
||||||
|
response = chain.proceed(updateRequest(request))
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRequest(request: Request): Request {
|
||||||
|
return request.body()?.let {
|
||||||
|
val contentType = it.contentType().toString()
|
||||||
|
val updatedBody = when {
|
||||||
|
contentType.contains("x-www-form-urlencoded") -> updateFormBody(it)
|
||||||
|
contentType.contains("json") -> updateJsonBody(it)
|
||||||
|
else -> it
|
||||||
|
}
|
||||||
|
request.newBuilder().post(updatedBody).build()
|
||||||
|
} ?: request
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bodyToString(requestBody: RequestBody): String {
|
||||||
|
Buffer().use {
|
||||||
|
requestBody.writeTo(it)
|
||||||
|
return it.readUtf8()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFormBody(requestBody: RequestBody): RequestBody {
|
||||||
|
val formString = bodyToString(requestBody)
|
||||||
|
|
||||||
|
return RequestBody.create(requestBody.contentType(),
|
||||||
|
"$formString${if (formString.isNotEmpty()) "&" else ""}${MyanimelistApi.CSRF}=${myanimelist.getCSRF()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateJsonBody(requestBody: RequestBody): RequestBody {
|
||||||
|
val jsonString = bodyToString(requestBody)
|
||||||
|
val newBody = JSONObject(jsonString)
|
||||||
|
.put(MyanimelistApi.CSRF, myanimelist.getCSRF())
|
||||||
|
|
||||||
|
return RequestBody.create(requestBody.contentType(), newBody.toString())
|
||||||
|
}
|
||||||
|
}
|
@ -22,61 +22,122 @@ import java.io.InputStreamReader
|
|||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
|
|
||||||
|
|
||||||
class MyanimelistApi(private val client: OkHttpClient) {
|
class MyanimelistApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
||||||
|
|
||||||
fun addLibManga(track: Track, csrf: String): Observable<Track> {
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
return Observable.defer {
|
|
||||||
client.newCall(POST(url = getAddUrl(), body = getMangaPostPayload(track, csrf)))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { track }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateLibManga(track: Track, csrf: String): Observable<Track> {
|
|
||||||
return Observable.defer {
|
|
||||||
client.newCall(POST(url = getUpdateUrl(), body = getMangaPostPayload(track, csrf)))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { track }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(query: String): Observable<List<TrackSearch>> {
|
fun search(query: String): Observable<List<TrackSearch>> {
|
||||||
return client.newCall(GET(getSearchUrl(query)))
|
return if (query.startsWith(PREFIX_MY)) {
|
||||||
.asObservable()
|
val realQuery = query.removePrefix(PREFIX_MY)
|
||||||
.flatMap { response ->
|
getList()
|
||||||
Observable.from(Jsoup.parse(response.consumeBody())
|
.flatMap { Observable.from(it) }
|
||||||
.select("div.js-categories-seasonal.js-block-list.list")
|
.filter { it.title.contains(realQuery, true) }
|
||||||
.select("table").select("tbody")
|
.toList()
|
||||||
.select("tr").drop(1))
|
}
|
||||||
}
|
else {
|
||||||
.filter { row ->
|
client.newCall(GET(searchUrl(query)))
|
||||||
row.select(TD)[2].text() != "Novel"
|
.asObservable()
|
||||||
}
|
.flatMap { response ->
|
||||||
.map { row ->
|
Observable.from(Jsoup.parse(response.consumeBody())
|
||||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
.select("div.js-categories-seasonal.js-block-list.list")
|
||||||
title = row.searchTitle()
|
.select("table").select("tbody")
|
||||||
media_id = row.searchMediaId()
|
.select("tr").drop(1))
|
||||||
total_chapters = row.searchTotalChapters()
|
|
||||||
summary = row.searchSummary()
|
|
||||||
cover_url = row.searchCoverUrl()
|
|
||||||
tracking_url = mangaUrl(media_id)
|
|
||||||
publishing_status = row.searchPublishingStatus()
|
|
||||||
publishing_type = row.searchPublishingType()
|
|
||||||
start_date = row.searchStartDate()
|
|
||||||
}
|
}
|
||||||
}
|
.filter { row ->
|
||||||
.toList()
|
row.select(TD)[2].text() != "Novel"
|
||||||
|
}
|
||||||
|
.map { row ->
|
||||||
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||||
|
title = row.searchTitle()
|
||||||
|
media_id = row.searchMediaId()
|
||||||
|
total_chapters = row.searchTotalChapters()
|
||||||
|
summary = row.searchSummary()
|
||||||
|
cover_url = row.searchCoverUrl()
|
||||||
|
tracking_url = mangaUrl(media_id)
|
||||||
|
publishing_status = row.searchPublishingStatus()
|
||||||
|
publishing_type = row.searchPublishingType()
|
||||||
|
start_date = row.searchStartDate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getList(csrf: String): Observable<List<TrackSearch>> {
|
fun addLibManga(track: Track): Observable<Track> {
|
||||||
return getListUrl(csrf)
|
return Observable.defer {
|
||||||
|
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track)))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { track }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLibManga(track: Track): Observable<Track> {
|
||||||
|
return Observable.defer {
|
||||||
|
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track)))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { track }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findLibManga(track: Track): Observable<Track?> {
|
||||||
|
return authClient.newCall(GET(url = listEntryUrl(track.media_id)))
|
||||||
|
.asObservable()
|
||||||
|
.map {response ->
|
||||||
|
var libTrack: Track? = null
|
||||||
|
response.use {
|
||||||
|
if (it.priorResponse()?.isRedirect != true) {
|
||||||
|
val trackForm = Jsoup.parse(it.consumeBody())
|
||||||
|
|
||||||
|
libTrack = Track.create(TrackManager.MYANIMELIST).apply {
|
||||||
|
last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
|
||||||
|
total_chapters = trackForm.select("#totalChap").text().toInt()
|
||||||
|
status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
|
||||||
|
score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull() ?: 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
libTrack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLibManga(track: Track): Observable<Track> {
|
||||||
|
return findLibManga(track)
|
||||||
|
.map { it ?: throw Exception("Could not find manga") }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun login(username: String, password: String): String {
|
||||||
|
val csrf = getSessionInfo()
|
||||||
|
|
||||||
|
login(username, password, csrf)
|
||||||
|
|
||||||
|
return csrf
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSessionInfo(): String {
|
||||||
|
val response = client.newCall(GET(loginUrl())).execute()
|
||||||
|
|
||||||
|
return Jsoup.parse(response.consumeBody())
|
||||||
|
.select("meta[name=csrf_token]")
|
||||||
|
.attr("content")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun login(username: String, password: String, csrf: String) {
|
||||||
|
val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute()
|
||||||
|
|
||||||
|
response.use {
|
||||||
|
if (response.priorResponse()?.code() != 302) throw Exception("Authentication error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getList(): Observable<List<TrackSearch>> {
|
||||||
|
return getListUrl()
|
||||||
.flatMap { url ->
|
.flatMap { url ->
|
||||||
getListXml(url)
|
getListXml(url)
|
||||||
}
|
}
|
||||||
.flatMap { doc ->
|
.flatMap { doc ->
|
||||||
Observable.from(doc.select("manga"))
|
Observable.from(doc.select("manga"))
|
||||||
}
|
}
|
||||||
.map { it ->
|
.map {
|
||||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||||
title = it.selectText("manga_title")!!
|
title = it.selectText("manga_title")!!
|
||||||
media_id = it.selectInt("manga_mangadb_id")
|
media_id = it.selectInt("manga_mangadb_id")
|
||||||
@ -90,107 +151,8 @@ class MyanimelistApi(private val client: OkHttpClient) {
|
|||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getListXml(url: String): Observable<Document> {
|
private fun getListUrl(): Observable<String> {
|
||||||
return client.newCall(GET(url))
|
return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
|
||||||
.asObservable()
|
|
||||||
.map { response ->
|
|
||||||
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findLibManga(track: Track, csrf: String): Observable<Track?> {
|
|
||||||
return getList(csrf)
|
|
||||||
.map { list -> list.find { it.media_id == track.media_id } }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLibManga(track: Track, csrf: String): Observable<Track> {
|
|
||||||
return findLibManga(track, csrf)
|
|
||||||
.map { it ?: throw Exception("Could not find manga") }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun login(username: String, password: String): Observable<String> {
|
|
||||||
return getSessionInfo()
|
|
||||||
.flatMap { csrf ->
|
|
||||||
login(username, password, csrf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSessionInfo(): Observable<String> {
|
|
||||||
return client.newCall(GET(getLoginUrl()))
|
|
||||||
.asObservable()
|
|
||||||
.map { response ->
|
|
||||||
Jsoup.parse(response.consumeBody())
|
|
||||||
.select("meta[name=csrf_token]")
|
|
||||||
.attr("content")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun login(username: String, password: String, csrf: String): Observable<String> {
|
|
||||||
return client.newCall(POST(url = getLoginUrl(), body = getLoginPostBody(username, password, csrf)))
|
|
||||||
.asObservable()
|
|
||||||
.map { response ->
|
|
||||||
response.use {
|
|
||||||
if (response.priorResponse()?.code() != 302) throw Exception("Authentication error")
|
|
||||||
}
|
|
||||||
csrf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLoginPostBody(username: String, password: String, csrf: String): RequestBody {
|
|
||||||
return FormBody.Builder()
|
|
||||||
.add("user_name", username)
|
|
||||||
.add("password", password)
|
|
||||||
.add("cookie", "1")
|
|
||||||
.add("sublogin", "Login")
|
|
||||||
.add("submit", "1")
|
|
||||||
.add(CSRF, csrf)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getExportPostBody(csrf: String): RequestBody {
|
|
||||||
return FormBody.Builder()
|
|
||||||
.add("type", "2")
|
|
||||||
.add("subexport", "Export My List")
|
|
||||||
.add(CSRF, csrf)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMangaPostPayload(track: Track, csrf: String): RequestBody {
|
|
||||||
val body = JSONObject()
|
|
||||||
.put("manga_id", track.media_id)
|
|
||||||
.put("status", track.status)
|
|
||||||
.put("score", track.score)
|
|
||||||
.put("num_read_chapters", track.last_chapter_read)
|
|
||||||
.put(CSRF, csrf)
|
|
||||||
|
|
||||||
return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), body.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLoginUrl() = Uri.parse(baseUrl).buildUpon()
|
|
||||||
.appendPath("login.php")
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
private fun getSearchUrl(query: String): String {
|
|
||||||
val col = "c[]"
|
|
||||||
return Uri.parse(baseUrl).buildUpon()
|
|
||||||
.appendPath("manga.php")
|
|
||||||
.appendQueryParameter("q", query)
|
|
||||||
.appendQueryParameter(col, "a")
|
|
||||||
.appendQueryParameter(col, "b")
|
|
||||||
.appendQueryParameter(col, "c")
|
|
||||||
.appendQueryParameter(col, "d")
|
|
||||||
.appendQueryParameter(col, "e")
|
|
||||||
.appendQueryParameter(col, "g")
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getExportListUrl() = Uri.parse(baseUrl).buildUpon()
|
|
||||||
.appendPath("panel.php")
|
|
||||||
.appendQueryParameter("go", "export")
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
private fun getListUrl(csrf: String): Observable<String> {
|
|
||||||
return client.newCall(POST(url = getExportListUrl(), body = getExportPostBody(csrf)))
|
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.map {response ->
|
.map {response ->
|
||||||
baseUrl + Jsoup.parse(response.consumeBody())
|
baseUrl + Jsoup.parse(response.consumeBody())
|
||||||
@ -200,17 +162,17 @@ class MyanimelistApi(private val client: OkHttpClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUpdateUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
private fun getListXml(url: String): Observable<Document> {
|
||||||
.appendPath("edit.json")
|
return authClient.newCall(GET(url))
|
||||||
.toString()
|
.asObservable()
|
||||||
|
.map { response ->
|
||||||
private fun getAddUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
||||||
.appendPath( "add.json")
|
}
|
||||||
.toString()
|
}
|
||||||
|
|
||||||
private fun Response.consumeBody(): String? {
|
private fun Response.consumeBody(): String? {
|
||||||
use {
|
use {
|
||||||
if (it.code() != 200) throw Exception("Login error")
|
if (it.code() != 200) throw Exception("HTTP error ${it.code()}")
|
||||||
return it.body()?.string()
|
return it.body()?.string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,37 +191,105 @@ class MyanimelistApi(private val client: OkHttpClient) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val baseUrl = "https://myanimelist.net"
|
const val CSRF = "csrf_token"
|
||||||
|
|
||||||
|
private const val baseUrl = "https://myanimelist.net"
|
||||||
private const val baseMangaUrl = "$baseUrl/manga/"
|
private const val baseMangaUrl = "$baseUrl/manga/"
|
||||||
private const val baseModifyListUrl = "$baseUrl/ownlist/manga"
|
private const val baseModifyListUrl = "$baseUrl/ownlist/manga"
|
||||||
|
private const val PREFIX_MY = "my:"
|
||||||
|
private const val TD = "td"
|
||||||
|
|
||||||
fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
||||||
|
|
||||||
fun Element.searchTitle() = select("strong").text()!!
|
private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
|
||||||
|
.appendPath("login.php")
|
||||||
|
.toString()
|
||||||
|
|
||||||
fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
private fun searchUrl(query: String): String {
|
||||||
|
val col = "c[]"
|
||||||
|
return Uri.parse(baseUrl).buildUpon()
|
||||||
|
.appendPath("manga.php")
|
||||||
|
.appendQueryParameter("q", query)
|
||||||
|
.appendQueryParameter(col, "a")
|
||||||
|
.appendQueryParameter(col, "b")
|
||||||
|
.appendQueryParameter(col, "c")
|
||||||
|
.appendQueryParameter(col, "d")
|
||||||
|
.appendQueryParameter(col, "e")
|
||||||
|
.appendQueryParameter(col, "g")
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
|
||||||
fun Element.searchCoverUrl() = select("img")
|
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
|
||||||
|
.appendPath("panel.php")
|
||||||
|
.appendQueryParameter("go", "export")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
private fun updateUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||||
|
.appendPath("edit.json")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||||
|
.appendPath( "add.json")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
private fun listEntryUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
|
||||||
|
.appendPath(mediaId.toString())
|
||||||
|
.appendPath("edit")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
|
||||||
|
return FormBody.Builder()
|
||||||
|
.add("user_name", username)
|
||||||
|
.add("password", password)
|
||||||
|
.add("cookie", "1")
|
||||||
|
.add("sublogin", "Login")
|
||||||
|
.add("submit", "1")
|
||||||
|
.add(CSRF, csrf)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exportPostBody(): RequestBody {
|
||||||
|
return FormBody.Builder()
|
||||||
|
.add("type", "2")
|
||||||
|
.add("subexport", "Export My List")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mangaPostPayload(track: Track): RequestBody {
|
||||||
|
val body = JSONObject()
|
||||||
|
.put("manga_id", track.media_id)
|
||||||
|
.put("status", track.status)
|
||||||
|
.put("score", track.score)
|
||||||
|
.put("num_read_chapters", track.last_chapter_read)
|
||||||
|
|
||||||
|
return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), body.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.searchTitle() = select("strong").text()!!
|
||||||
|
|
||||||
|
private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
||||||
|
|
||||||
|
private fun Element.searchCoverUrl() = select("img")
|
||||||
.attr("data-src")
|
.attr("data-src")
|
||||||
.split("\\?")[0]
|
.split("\\?")[0]
|
||||||
.replace("/r/50x70/", "/")
|
.replace("/r/50x70/", "/")
|
||||||
|
|
||||||
fun Element.searchMediaId() = select("div.picSurround")
|
private fun Element.searchMediaId() = select("div.picSurround")
|
||||||
.select("a").attr("id")
|
.select("a").attr("id")
|
||||||
.replace("sarea", "")
|
.replace("sarea", "")
|
||||||
.toInt()
|
.toInt()
|
||||||
|
|
||||||
fun Element.searchSummary() = select("div.pt4")
|
private fun Element.searchSummary() = select("div.pt4")
|
||||||
.first()
|
.first()
|
||||||
.ownText()!!
|
.ownText()!!
|
||||||
|
|
||||||
fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") PUBLISHING else FINISHED
|
private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished"
|
||||||
|
|
||||||
fun Element.searchPublishingType() = select(TD)[2].text()!!
|
private fun Element.searchPublishingType() = select(TD)[2].text()!!
|
||||||
|
|
||||||
fun Element.searchStartDate() = select(TD)[6].text()!!
|
private fun Element.searchStartDate() = select(TD)[6].text()!!
|
||||||
|
|
||||||
fun getStatus(status: String) = when (status) {
|
private fun getStatus(status: String) = when (status) {
|
||||||
"Reading" -> 1
|
"Reading" -> 1
|
||||||
"Completed" -> 2
|
"Completed" -> 2
|
||||||
"On-Hold" -> 3
|
"On-Hold" -> 3
|
||||||
@ -267,10 +297,5 @@ class MyanimelistApi(private val client: OkHttpClient) {
|
|||||||
"Plan to Read" -> 6
|
"Plan to Read" -> 6
|
||||||
else -> 1
|
else -> 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const val CSRF = "csrf_token"
|
|
||||||
const val TD = "td"
|
|
||||||
private const val FINISHED = "Finished"
|
|
||||||
private const val PUBLISHING = "Publishing"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.shikomori
|
package eu.kanade.tachiyomi.data.track.shikimori
|
||||||
|
|
||||||
data class OAuth(
|
data class OAuth(
|
||||||
val access_token: String,
|
val access_token: String,
|
@ -1,7 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.shikomori
|
package eu.kanade.tachiyomi.data.track.shikimori
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.util.Log
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@ -11,7 +12,7 @@ import rx.Completable
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class Shikomori(private val context: Context, id: Int) : TrackService(id) {
|
class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
override fun getScoreList(): List<String> {
|
override fun getScoreList(): List<String> {
|
||||||
return IntRange(0, 10).map(Int::toString)
|
return IntRange(0, 10).map(Int::toString)
|
||||||
@ -75,15 +76,15 @@ class Shikomori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
const val DEFAULT_SCORE = 0
|
const val DEFAULT_SCORE = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override val name = "Shikomori"
|
override val name = "Shikimori"
|
||||||
|
|
||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
private val interceptor by lazy { ShikomoriInterceptor(this, gson) }
|
private val interceptor by lazy { ShikimoriInterceptor(this, gson) }
|
||||||
|
|
||||||
private val api by lazy { ShikomoriApi(client, interceptor) }
|
private val api by lazy { ShikimoriApi(client, interceptor) }
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.shikomori
|
override fun getLogo() = R.drawable.shikimori
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.shikomori
|
package eu.kanade.tachiyomi.data.track.shikimori
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.array
|
import com.github.salomonbrys.kotson.array
|
||||||
@ -18,7 +18,7 @@ import okhttp3.*
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInterceptor) {
|
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
|
||||||
|
|
||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
private val parser = JsonParser()
|
private val parser = JsonParser()
|
||||||
@ -33,7 +33,7 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
|||||||
"target_type" to "Manga",
|
"target_type" to "Manga",
|
||||||
"chapters" to track.last_chapter_read,
|
"chapters" to track.last_chapter_read,
|
||||||
"score" to track.score.toInt(),
|
"score" to track.score.toInt(),
|
||||||
"status" to track.toShikomoriStatus()
|
"status" to track.toShikimoriStatus()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val body = RequestBody.create(jsonime, payload.toString())
|
val body = RequestBody.create(jsonime, payload.toString())
|
||||||
@ -74,7 +74,7 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||||
return TrackSearch.create(TrackManager.SHIKOMORI).apply {
|
return TrackSearch.create(TrackManager.SHIKIMORI).apply {
|
||||||
media_id = obj["id"].asInt
|
media_id = obj["id"].asInt
|
||||||
title = obj["name"].asString
|
title = obj["name"].asString
|
||||||
total_chapters = obj["chapters"].asInt
|
total_chapters = obj["chapters"].asInt
|
||||||
@ -87,14 +87,15 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToTrack(obj: JsonObject): Track {
|
private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track {
|
||||||
return Track.create(TrackManager.SHIKOMORI).apply {
|
return Track.create(TrackManager.SHIKIMORI).apply {
|
||||||
|
title = mangas["name"].asString
|
||||||
media_id = obj["id"].asInt
|
media_id = obj["id"].asInt
|
||||||
title = ""
|
total_chapters = mangas["chapters"].asInt
|
||||||
last_chapter_read = obj["chapters"].asInt
|
last_chapter_read = obj["chapters"].asInt
|
||||||
total_chapters = obj["chapters"].asInt
|
|
||||||
score = (obj["score"].asInt).toFloat()
|
score = (obj["score"].asInt).toFloat()
|
||||||
status = toTrackStatus(obj["status"].asString)
|
status = toTrackStatus(obj["status"].asString)
|
||||||
|
tracking_url = baseUrl + mangas["url"].asString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,21 +109,36 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
|||||||
.url(url.toString())
|
.url(url.toString())
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
|
||||||
|
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||||
|
.appendPath(track.media_id.toString())
|
||||||
|
.build()
|
||||||
|
val requestMangas = Request.Builder()
|
||||||
|
.url(urlMangas.toString())
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
return authClient.newCall(requestMangas)
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
.map { netResponse ->
|
.map { netResponse ->
|
||||||
val responseBody = netResponse.body()?.string().orEmpty()
|
val responseBody = netResponse.body()?.string().orEmpty()
|
||||||
if (responseBody.isEmpty()) {
|
parser.parse(responseBody).obj
|
||||||
throw Exception("Null Response")
|
}.flatMap { mangas ->
|
||||||
}
|
authClient.newCall(request)
|
||||||
val response = parser.parse(responseBody).array
|
.asObservableSuccess()
|
||||||
if (response.size() > 1) {
|
.map { netResponse ->
|
||||||
throw Exception("Too much mangas in response")
|
val responseBody = netResponse.body()?.string().orEmpty()
|
||||||
}
|
if (responseBody.isEmpty()) {
|
||||||
val entry = response.map {
|
throw Exception("Null Response")
|
||||||
jsonToTrack(it.obj)
|
}
|
||||||
}
|
val response = parser.parse(responseBody).array
|
||||||
entry.firstOrNull()
|
if (response.size() > 1) {
|
||||||
|
throw Exception("Too much mangas in response")
|
||||||
|
}
|
||||||
|
val entry = response.map {
|
||||||
|
jsonToTrack(it.obj, mangas)
|
||||||
|
}
|
||||||
|
entry.firstOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,10 +172,10 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
|||||||
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
|
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
|
||||||
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
|
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
|
||||||
|
|
||||||
private const val baseUrl = "https://shikimori.org"
|
private const val baseUrl = "https://shikimori.one"
|
||||||
private const val apiUrl = "https://shikimori.org/api"
|
private const val apiUrl = "https://shikimori.one/api"
|
||||||
private const val oauthUrl = "https://shikimori.org/oauth/token"
|
private const val oauthUrl = "https://shikimori.one/oauth/token"
|
||||||
private const val loginUrl = "https://shikimori.org/oauth/authorize"
|
private const val loginUrl = "https://shikimori.one/oauth/authorize"
|
||||||
|
|
||||||
private const val redirectUrl = "tachiyomi://shikimori-auth"
|
private const val redirectUrl = "tachiyomi://shikimori-auth"
|
||||||
private const val baseMangaUrl = "$apiUrl/mangas"
|
private const val baseMangaUrl = "$apiUrl/mangas"
|
@ -1,26 +1,26 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.shikomori
|
package eu.kanade.tachiyomi.data.track.shikimori
|
||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
class ShikomoriInterceptor(val shikomori: Shikomori, val gson: Gson) : Interceptor {
|
class ShikimoriInterceptor(val shikimori: Shikimori, val gson: Gson) : Interceptor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth object used for authenticated requests.
|
* OAuth object used for authenticated requests.
|
||||||
*/
|
*/
|
||||||
private var oauth: OAuth? = shikomori.restoreToken()
|
private var oauth: OAuth? = shikimori.restoreToken()
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
val currAuth = oauth ?: throw Exception("Not authenticated with Shikomori")
|
val currAuth = oauth ?: throw Exception("Not authenticated with Shikimori")
|
||||||
|
|
||||||
val refreshToken = currAuth.refresh_token!!
|
val refreshToken = currAuth.refresh_token!!
|
||||||
|
|
||||||
// Refresh access token if expired.
|
// Refresh access token if expired.
|
||||||
if (currAuth.isExpired()) {
|
if (currAuth.isExpired()) {
|
||||||
val response = chain.proceed(ShikomoriApi.refreshTokenRequest(refreshToken))
|
val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken))
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
|
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
|
||||||
} else {
|
} else {
|
||||||
@ -38,6 +38,6 @@ class ShikomoriInterceptor(val shikomori: Shikomori, val gson: Gson) : Intercept
|
|||||||
|
|
||||||
fun newAuth(oauth: OAuth?) {
|
fun newAuth(oauth: OAuth?) {
|
||||||
this.oauth = oauth
|
this.oauth = oauth
|
||||||
shikomori.saveToken(oauth)
|
shikimori.saveToken(oauth)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.shikimori
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
|
||||||
|
fun Track.toShikimoriStatus() = when (status) {
|
||||||
|
Shikimori.READING -> "watching"
|
||||||
|
Shikimori.COMPLETED -> "completed"
|
||||||
|
Shikimori.ON_HOLD -> "on_hold"
|
||||||
|
Shikimori.DROPPED -> "dropped"
|
||||||
|
Shikimori.PLANNING -> "planned"
|
||||||
|
Shikimori.REPEATING -> "rewatching"
|
||||||
|
else -> throw NotImplementedError("Unknown status")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toTrackStatus(status: String) = when (status) {
|
||||||
|
"watching" -> Shikimori.READING
|
||||||
|
"completed" -> Shikimori.COMPLETED
|
||||||
|
"on_hold" -> Shikimori.ON_HOLD
|
||||||
|
"dropped" -> Shikimori.DROPPED
|
||||||
|
"planned" -> Shikimori.PLANNING
|
||||||
|
"rewatching" -> Shikimori.REPEATING
|
||||||
|
|
||||||
|
else -> throw Exception("Unknown status")
|
||||||
|
}
|
@ -1,24 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.shikomori
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
|
|
||||||
fun Track.toShikomoriStatus() = when (status) {
|
|
||||||
Shikomori.READING -> "watching"
|
|
||||||
Shikomori.COMPLETED -> "completed"
|
|
||||||
Shikomori.ON_HOLD -> "on_hold"
|
|
||||||
Shikomori.DROPPED -> "dropped"
|
|
||||||
Shikomori.PLANNING -> "planned"
|
|
||||||
Shikomori.REPEATING -> "rewatching"
|
|
||||||
else -> throw NotImplementedError("Unknown status")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toTrackStatus(status: String) = when (status) {
|
|
||||||
"watching" -> Shikomori.READING
|
|
||||||
"completed" -> Shikomori.COMPLETED
|
|
||||||
"on_hold" -> Shikomori.ON_HOLD
|
|
||||||
"dropped" -> Shikomori.DROPPED
|
|
||||||
"planned" -> Shikomori.PLANNING
|
|
||||||
"rewatching" -> Shikomori.REPEATING
|
|
||||||
|
|
||||||
else -> throw Exception("Unknown status")
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
|
||||||
|
|
||||||
sealed class GithubUpdateResult {
|
|
||||||
|
|
||||||
class NewUpdate(val release: GithubRelease): GithubUpdateResult()
|
|
||||||
class NoNewUpdate : GithubUpdateResult()
|
|
||||||
}
|
|
@ -0,0 +1,13 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
interface Release {
|
||||||
|
|
||||||
|
val info: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get download link of latest release.
|
||||||
|
* @return download link of latest release.
|
||||||
|
*/
|
||||||
|
val downloadLink: String
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
|
import eu.kanade.tachiyomi.data.updater.devrepo.DevRepoUpdateChecker
|
||||||
|
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
abstract class UpdateChecker {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getUpdateChecker(): UpdateChecker {
|
||||||
|
return if (BuildConfig.DEBUG) {
|
||||||
|
DevRepoUpdateChecker()
|
||||||
|
} else {
|
||||||
|
GithubUpdateChecker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns observable containing release information
|
||||||
|
*/
|
||||||
|
abstract fun checkForUpdate(): Observable<UpdateResult>
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
abstract class UpdateResult {
|
||||||
|
|
||||||
|
open class NewUpdate<T : Release>(val release: T): UpdateResult()
|
||||||
|
open class NoNewUpdate: UpdateResult()
|
||||||
|
|
||||||
|
}
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.updater
|
|||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import com.evernote.android.job.Job
|
import com.evernote.android.job.Job
|
||||||
import com.evernote.android.job.JobManager
|
import com.evernote.android.job.JobManager
|
||||||
@ -13,10 +14,15 @@ import eu.kanade.tachiyomi.util.notificationManager
|
|||||||
class UpdaterJob : Job() {
|
class UpdaterJob : Job() {
|
||||||
|
|
||||||
override fun onRunJob(params: Params): Result {
|
override fun onRunJob(params: Params): Result {
|
||||||
return GithubUpdateChecker()
|
// Android 4.x is no longer supported
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
return Result.SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
return UpdateChecker.getUpdateChecker()
|
||||||
.checkForUpdate()
|
.checkForUpdate()
|
||||||
.map { result ->
|
.map { result ->
|
||||||
if (result is GithubUpdateResult.NewUpdate) {
|
if (result is UpdateResult.NewUpdate<*>) {
|
||||||
val url = result.release.downloadLink
|
val url = result.release.downloadLink
|
||||||
|
|
||||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||||
@ -33,9 +39,9 @@ class UpdaterJob : Job() {
|
|||||||
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Job.Result.SUCCESS
|
Result.SUCCESS
|
||||||
}
|
}
|
||||||
.onErrorReturn { Job.Result.FAILURE }
|
.onErrorReturn { Result.FAILURE }
|
||||||
// Sadly, the task needs to be synchronous.
|
// Sadly, the task needs to be synchronous.
|
||||||
.toBlocking()
|
.toBlocking()
|
||||||
.single()
|
.single()
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater.devrepo
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.updater.Release
|
||||||
|
|
||||||
|
class DevRepoRelease(override val info: String) : Release {
|
||||||
|
|
||||||
|
override val downloadLink: String
|
||||||
|
get() = LATEST_URL
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LATEST_URL = "https://tachiyomi.kanade.eu/latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater.devrepo
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
|
import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
||||||
|
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.asObservable
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class DevRepoUpdateChecker : UpdateChecker() {
|
||||||
|
|
||||||
|
private val client: OkHttpClient by lazy {
|
||||||
|
Injekt.get<NetworkHelper>().client.newBuilder()
|
||||||
|
.followRedirects(false)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val versionRegex: Regex by lazy {
|
||||||
|
Regex("tachiyomi-r(\\d+).apk")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkForUpdate(): Observable<UpdateResult> {
|
||||||
|
return client.newCall(GET(DevRepoRelease.LATEST_URL)).asObservable()
|
||||||
|
.map { response ->
|
||||||
|
// Get latest repo version number from header in format "Location: tachiyomi-r1512.apk"
|
||||||
|
val latestVersionNumber: String = versionRegex.find(response.header("Location")!!)!!.groupValues[1]
|
||||||
|
|
||||||
|
if (latestVersionNumber.toInt() > BuildConfig.COMMIT_COUNT.toInt()) {
|
||||||
|
DevRepoUpdateResult.NewUpdate(DevRepoRelease("v$latestVersionNumber"))
|
||||||
|
} else {
|
||||||
|
DevRepoUpdateResult.NoNewUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater.devrepo
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||||
|
|
||||||
|
sealed class DevRepoUpdateResult : UpdateResult() {
|
||||||
|
|
||||||
|
class NewUpdate(release: DevRepoRelease): UpdateResult.NewUpdate<DevRepoRelease>(release)
|
||||||
|
class NoNewUpdate: UpdateResult.NoNewUpdate()
|
||||||
|
|
||||||
|
}
|
@ -1,24 +1,25 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
package eu.kanade.tachiyomi.data.updater.github
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import eu.kanade.tachiyomi.data.updater.Release
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release object.
|
* Release object.
|
||||||
* Contains information about the latest release from Github.
|
* Contains information about the latest release from Github.
|
||||||
*
|
*
|
||||||
* @param version version of latest release.
|
* @param version version of latest release.
|
||||||
* @param changeLog log of latest release.
|
* @param info log of latest release.
|
||||||
* @param assets assets of latest release.
|
* @param assets assets of latest release.
|
||||||
*/
|
*/
|
||||||
class GithubRelease(@SerializedName("tag_name") val version: String,
|
class GithubRelease(@SerializedName("tag_name") val version: String,
|
||||||
@SerializedName("body") val changeLog: String,
|
@SerializedName("body") override val info: String,
|
||||||
@SerializedName("assets") private val assets: List<Assets>) {
|
@SerializedName("assets") private val assets: List<Assets>): Release {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get download link of latest release from the assets.
|
* Get download link of latest release from the assets.
|
||||||
* @return download link of latest release.
|
* @return download link of latest release.
|
||||||
*/
|
*/
|
||||||
val downloadLink: String
|
override val downloadLink: String
|
||||||
get() = assets[0].downloadLink
|
get() = assets[0].downloadLink
|
||||||
|
|
||||||
/**
|
/**
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
package eu.kanade.tachiyomi.data.updater.github
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
@ -1,16 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
package eu.kanade.tachiyomi.data.updater.github
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
|
import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
||||||
|
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
class GithubUpdateChecker {
|
class GithubUpdateChecker : UpdateChecker() {
|
||||||
|
|
||||||
private val service: GithubService = GithubService.create()
|
private val service: GithubService = GithubService.create()
|
||||||
|
|
||||||
/**
|
override fun checkForUpdate(): Observable<UpdateResult> {
|
||||||
* Returns observable containing release information
|
|
||||||
*/
|
|
||||||
fun checkForUpdate(): Observable<GithubUpdateResult> {
|
|
||||||
return service.getLatestVersion().map { release ->
|
return service.getLatestVersion().map { release ->
|
||||||
val newVersion = release.version.replace("[^\\d.]".toRegex(), "")
|
val newVersion = release.version.replace("[^\\d.]".toRegex(), "")
|
||||||
|
|
||||||
@ -22,4 +21,5 @@ class GithubUpdateChecker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater.github
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||||
|
|
||||||
|
sealed class GithubUpdateResult : UpdateResult() {
|
||||||
|
|
||||||
|
class NewUpdate(release: GithubRelease): UpdateResult.NewUpdate<GithubRelease>(release)
|
||||||
|
class NoNewUpdate : UpdateResult.NoNewUpdate()
|
||||||
|
|
||||||
|
}
|
@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
|||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.util.launchNow
|
import eu.kanade.tachiyomi.util.launchNow
|
||||||
import kotlinx.coroutines.experimental.async
|
import kotlinx.coroutines.async
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
@ -7,6 +7,7 @@ import com.github.salomonbrys.kotson.string
|
|||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
@ -36,17 +37,23 @@ internal class ExtensionGithubApi {
|
|||||||
|
|
||||||
val json = gson.fromJson<JsonArray>(text)
|
val json = gson.fromJson<JsonArray>(text)
|
||||||
|
|
||||||
return json.map { element ->
|
return json
|
||||||
val name = element["name"].string.substringAfter("Tachiyomi: ")
|
.filter { element ->
|
||||||
val pkgName = element["pkg"].string
|
val versionName = element["version"].string
|
||||||
val apkName = element["apk"].string
|
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
||||||
val versionName = element["version"].string
|
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
||||||
val versionCode = element["code"].int
|
}
|
||||||
val lang = element["lang"].string
|
.map { element ->
|
||||||
val icon = "$repoUrl/icon/${apkName.replace(".apk", ".png")}"
|
val name = element["name"].string.substringAfter("Tachiyomi: ")
|
||||||
|
val pkgName = element["pkg"].string
|
||||||
|
val apkName = element["apk"].string
|
||||||
|
val versionName = element["version"].string
|
||||||
|
val versionCode = element["code"].int
|
||||||
|
val lang = element["lang"].string
|
||||||
|
val icon = "$repoUrl/icon/${apkName.replace(".apk", ".png")}"
|
||||||
|
|
||||||
Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon)
|
Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApkUrl(extension: Extension.Available): String {
|
fun getApkUrl(extension: Extension.Available): String {
|
||||||
|
@ -39,7 +39,7 @@ class ExtensionInstallActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkInstallationResult(resultCode: Int) {
|
private fun checkInstallationResult(resultCode: Int) {
|
||||||
val downloadId = intent.extras.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||||
val success = resultCode == RESULT_OK
|
val success = resultCode == RESULT_OK
|
||||||
|
|
||||||
val extensionManager = Injekt.get<ExtensionManager>()
|
val extensionManager = Injekt.get<ExtensionManager>()
|
||||||
|
@ -7,7 +7,10 @@ import android.content.IntentFilter
|
|||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
import eu.kanade.tachiyomi.util.launchNow
|
import eu.kanade.tachiyomi.util.launchNow
|
||||||
import kotlinx.coroutines.experimental.async
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
|
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
|
||||||
@ -91,7 +94,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
|
|||||||
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult {
|
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult {
|
||||||
val pkgName = getPackageNameFromIntent(intent) ?:
|
val pkgName = getPackageNameFromIntent(intent) ?:
|
||||||
return LoadResult.Error("Package name not found")
|
return LoadResult.Error("Package name not found")
|
||||||
return async { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await()
|
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT, { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }).await()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,8 +13,8 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.util.Hash
|
import eu.kanade.tachiyomi.util.Hash
|
||||||
import kotlinx.coroutines.experimental.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.experimental.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -27,8 +27,8 @@ internal object ExtensionLoader {
|
|||||||
|
|
||||||
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||||
private const val LIB_VERSION_MIN = 1
|
const val LIB_VERSION_MIN = 1.0
|
||||||
private const val LIB_VERSION_MAX = 1
|
const val LIB_VERSION_MAX = 1.2
|
||||||
|
|
||||||
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
||||||
|
|
||||||
@ -100,10 +100,16 @@ internal object ExtensionLoader {
|
|||||||
val versionName = pkgInfo.versionName
|
val versionName = pkgInfo.versionName
|
||||||
val versionCode = pkgInfo.versionCode
|
val versionCode = pkgInfo.versionCode
|
||||||
|
|
||||||
|
if (versionName.isNullOrEmpty()) {
|
||||||
|
val exception = Exception("Missing versionName for extension $extName")
|
||||||
|
Timber.w(exception)
|
||||||
|
return LoadResult.Error(exception)
|
||||||
|
}
|
||||||
|
|
||||||
// Validate lib version
|
// Validate lib version
|
||||||
val majorLibVersion = versionName.substringBefore('.').toInt()
|
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
||||||
if (majorLibVersion < LIB_VERSION_MIN || majorLibVersion > LIB_VERSION_MAX) {
|
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
||||||
val exception = Exception("Lib version is $majorLibVersion, while only versions " +
|
val exception = Exception("Lib version is $libVersion, while only versions " +
|
||||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed")
|
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed")
|
||||||
Timber.w(exception)
|
Timber.w(exception)
|
||||||
return LoadResult.Error(exception)
|
return LoadResult.Error(exception)
|
||||||
@ -121,7 +127,7 @@ internal object ExtensionLoader {
|
|||||||
|
|
||||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||||
|
|
||||||
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
||||||
.split(";")
|
.split(";")
|
||||||
.map {
|
.map {
|
||||||
val sourceClass = it.trim()
|
val sourceClass = it.trim()
|
||||||
|
@ -46,12 +46,22 @@ class AndroidCookieJar(context: Context) : CookieJar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(url: HttpUrl) {
|
fun remove(url: HttpUrl, cookieNames: List<String>? = null, maxAge: Int = -1) {
|
||||||
val cookies = manager.getCookie(url.toString()) ?: return
|
val urlString = url.toString()
|
||||||
val domain = ".${url.host()}"
|
val cookies = manager.getCookie(urlString) ?: return
|
||||||
|
|
||||||
|
fun List<String>.filterNames(): List<String> {
|
||||||
|
return if (cookieNames != null) {
|
||||||
|
this.filter { it in cookieNames }
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cookies.split(";")
|
cookies.split(";")
|
||||||
.map { it.substringBefore("=") }
|
.map { it.substringBefore("=") }
|
||||||
.onEach { manager.setCookie(domain, "$it=;Max-Age=-1") }
|
.filterNames()
|
||||||
|
.onEach { manager.setCookie(urlString, "$it=;Max-Age=$maxAge") }
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
syncManager.sync()
|
syncManager.sync()
|
||||||
@ -66,5 +76,4 @@ class AndroidCookieJar(context: Context) : CookieJar {
|
|||||||
syncManager.sync()
|
syncManager.sync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,11 @@ import android.content.Context
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import eu.kanade.tachiyomi.util.WebViewClientCompat
|
import eu.kanade.tachiyomi.util.WebViewClientCompat
|
||||||
import okhttp3.Interceptor
|
import okhttp3.*
|
||||||
import okhttp3.Request
|
import uy.kohesive.injekt.injectLazy
|
||||||
import okhttp3.Response
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -22,6 +20,8 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
private val networkHelper: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
|
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
|
||||||
* blocking the main thread too much. If used too often we could consider moving it to the
|
* blocking the main thread too much. If used too often we could consider moving it to the
|
||||||
@ -39,14 +39,21 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
initWebView
|
initWebView
|
||||||
|
|
||||||
val response = chain.proceed(chain.request())
|
val originalRequest = chain.request()
|
||||||
|
val response = chain.proceed(originalRequest)
|
||||||
|
|
||||||
// Check if Cloudflare anti-bot is on
|
// Check if Cloudflare anti-bot is on
|
||||||
if (response.code() == 503 && response.header("Server") in serverCheck) {
|
if (response.code() == 503 && response.header("Server") in serverCheck) {
|
||||||
try {
|
try {
|
||||||
response.close()
|
response.close()
|
||||||
val solutionRequest = resolveWithWebView(chain.request())
|
networkHelper.cookieManager.remove(originalRequest.url(), listOf("__cfduid", "cf_clearance"), 0)
|
||||||
return chain.proceed(solutionRequest)
|
val oldCookie = networkHelper.cookieManager.get(originalRequest.url())
|
||||||
|
.firstOrNull { it.name() == "cf_clearance" }
|
||||||
|
return if (resolveWithWebView(originalRequest, oldCookie)) {
|
||||||
|
chain.proceed(originalRequest)
|
||||||
|
} else {
|
||||||
|
throw IOException("Failed to bypass Cloudflare!")
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||||
// we don't crash the entire app
|
// we don't crash the entire app
|
||||||
@ -57,19 +64,15 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isChallengeSolutionUrl(url: String): Boolean {
|
|
||||||
return "chk_jschl" in url
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
private fun resolveWithWebView(request: Request): Request {
|
private fun resolveWithWebView(request: Request, oldCookie: Cookie?): Boolean {
|
||||||
// We need to lock this thread until the WebView finds the challenge solution url, because
|
// We need to lock this thread until the WebView finds the challenge solution url, because
|
||||||
// OkHttp doesn't support asynchronous interceptors.
|
// OkHttp doesn't support asynchronous interceptors.
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
var webView: WebView? = null
|
var webView: WebView? = null
|
||||||
var solutionUrl: String? = null
|
|
||||||
var challengeFound = false
|
var challengeFound = false
|
||||||
|
var cloudflareBypassed = false
|
||||||
|
|
||||||
val origRequestUrl = request.url().toString()
|
val origRequestUrl = request.url().toString()
|
||||||
val headers = request.headers().toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
val headers = request.headers().toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
||||||
@ -81,26 +84,17 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
view.settings.userAgentString = request.header("User-Agent")
|
view.settings.userAgentString = request.header("User-Agent")
|
||||||
view.webViewClient = object : WebViewClientCompat() {
|
view.webViewClient = object : WebViewClientCompat() {
|
||||||
|
|
||||||
override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
if (isChallengeSolutionUrl(url)) {
|
fun isCloudFlareBypassed(): Boolean {
|
||||||
solutionUrl = url
|
return networkHelper.cookieManager.get(HttpUrl.parse(origRequestUrl)!!)
|
||||||
|
.firstOrNull { it.name() == "cf_clearance" }
|
||||||
|
.let { it != null && it != oldCookie }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCloudFlareBypassed()) {
|
||||||
|
cloudflareBypassed = true
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
return solutionUrl != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shouldInterceptRequestCompat(
|
|
||||||
view: WebView,
|
|
||||||
url: String
|
|
||||||
): WebResourceResponse? {
|
|
||||||
if (solutionUrl != null) {
|
|
||||||
// Intercept any request when we have the solution.
|
|
||||||
return WebResourceResponse("text/plain", "UTF-8", null)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageFinished(view: WebView, url: String) {
|
|
||||||
// Http error codes are only received since M
|
// Http error codes are only received since M
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||||
url == origRequestUrl && !challengeFound
|
url == origRequestUrl && !challengeFound
|
||||||
@ -140,15 +134,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
webView?.destroy()
|
webView?.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
val solution = solutionUrl ?: throw Exception("Challenge not found")
|
return cloudflareBypassed
|
||||||
|
|
||||||
return Request.Builder().get()
|
|
||||||
.url(solution)
|
|
||||||
.headers(request.headers())
|
|
||||||
.addHeader("Referer", origRequestUrl)
|
|
||||||
.addHeader("Accept", "text/html,application/xhtml+xml,application/xml")
|
|
||||||
.addHeader("Accept-Language", "en")
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,20 +2,24 @@ package eu.kanade.tachiyomi.source
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.util.ChapterRecognition
|
import eu.kanade.tachiyomi.util.ChapterRecognition
|
||||||
|
import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.EpubFile
|
import eu.kanade.tachiyomi.util.EpubFile
|
||||||
import eu.kanade.tachiyomi.util.ImageUtil
|
import eu.kanade.tachiyomi.util.ImageUtil
|
||||||
import junrar.Archive
|
import junrar.Archive
|
||||||
import junrar.rarfile.FileHeader
|
import junrar.rarfile.FileHeader
|
||||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.Comparator
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
@ -125,7 +129,6 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
override fun fetchMangaDetails(manga: SManga) = Observable.just(manga)
|
override fun fetchMangaDetails(manga: SManga) = Observable.just(manga)
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
|
||||||
val chapters = getBaseDirectories(context)
|
val chapters = getBaseDirectories(context)
|
||||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
@ -146,7 +149,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
}
|
}
|
||||||
.sortedWith(Comparator { c1, c2 ->
|
.sortedWith(Comparator { c1, c2 ->
|
||||||
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||||
if (c == 0) comparator.compare(c2.name, c1.name) else c
|
if (c == 0) CaseInsensitiveNaturalComparator.compare(c2.name, c1.name) else c
|
||||||
})
|
})
|
||||||
|
|
||||||
return Observable.just(chapters)
|
return Observable.just(chapters)
|
||||||
@ -189,20 +192,19 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
||||||
val format = getFormat(chapter)
|
val format = getFormat(chapter)
|
||||||
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
|
||||||
return when (format) {
|
return when (format) {
|
||||||
is Format.Directory -> {
|
is Format.Directory -> {
|
||||||
val entry = format.file.listFiles()
|
val entry = format.file.listFiles()
|
||||||
.sortedWith(Comparator<File> { f1, f2 -> comparator.compare(f1.name, f2.name) })
|
.sortedWith(Comparator<File> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) })
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.name, { FileInputStream(it) }) }
|
.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, it.inputStream())}
|
entry?.let { updateCover(context, manga, it.inputStream())}
|
||||||
}
|
}
|
||||||
is Format.Zip -> {
|
is Format.Zip -> {
|
||||||
ZipFile(format.file).use { zip ->
|
ZipFile(format.file).use { zip ->
|
||||||
val entry = zip.entries().toList()
|
val entry = zip.entries().toList()
|
||||||
.sortedWith(Comparator<ZipEntry> { f1, f2 -> comparator.compare(f1.name, f2.name) })
|
.sortedWith(Comparator<ZipEntry> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) })
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.name, { zip.getInputStream(it) }) }
|
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, zip.getInputStream(it) )}
|
entry?.let { updateCover(context, manga, zip.getInputStream(it) )}
|
||||||
}
|
}
|
||||||
@ -210,8 +212,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
is Format.Rar -> {
|
is Format.Rar -> {
|
||||||
Archive(format.file).use { archive ->
|
Archive(format.file).use { archive ->
|
||||||
val entry = archive.fileHeaders
|
val entry = archive.fileHeaders
|
||||||
.sortedWith(Comparator<FileHeader> { f1, f2 -> comparator.compare(f1.fileNameString, f2.fileNameString) })
|
.sortedWith(Comparator<FileHeader> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.fileNameString, f2.fileNameString) })
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileNameString, { archive.getInputStream(it) }) }
|
.find { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, archive.getInputStream(it) )}
|
entry?.let { updateCover(context, manga, archive.getInputStream(it) )}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
|
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
|
||||||
divider = a.getDrawable(0)
|
divider = a.getDrawable(0)!!
|
||||||
a.recycle()
|
a.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,11 +17,13 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.info.MangaWebViewController
|
||||||
import eu.kanade.tachiyomi.util.*
|
import eu.kanade.tachiyomi.util.*
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
import kotlinx.android.synthetic.main.catalogue_controller.*
|
import kotlinx.android.synthetic.main.catalogue_controller.*
|
||||||
@ -173,6 +175,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
|||||||
RecyclerView(view.context).apply {
|
RecyclerView(view.context).apply {
|
||||||
id = R.id.recycler
|
id = R.id.recycler
|
||||||
layoutManager = LinearLayoutManager(context)
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -199,7 +202,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
|||||||
catalogue_view.addView(recycler, 1)
|
catalogue_view.addView(recycler, 1)
|
||||||
|
|
||||||
if (oldPosition != RecyclerView.NO_POSITION) {
|
if (oldPosition != RecyclerView.NO_POSITION) {
|
||||||
recycler.layoutManager.scrollToPosition(oldPosition)
|
recycler.layoutManager?.scrollToPosition(oldPosition)
|
||||||
}
|
}
|
||||||
this.recycler = recycler
|
this.recycler = recycler
|
||||||
}
|
}
|
||||||
@ -259,15 +262,38 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
|
super.onPrepareOptionsMenu(menu)
|
||||||
|
|
||||||
|
val isHttpSource = presenter.source is HttpSource
|
||||||
|
menu.findItem(R.id.action_open_in_browser).isVisible = isHttpSource
|
||||||
|
menu.findItem(R.id.action_open_in_web_view).isVisible = isHttpSource
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_display_mode -> swapDisplayMode()
|
R.id.action_display_mode -> swapDisplayMode()
|
||||||
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
|
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
|
||||||
|
R.id.action_open_in_browser -> openInBrowser()
|
||||||
|
R.id.action_open_in_web_view -> openInWebView()
|
||||||
else -> return super.onOptionsItemSelected(item)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openInBrowser() {
|
||||||
|
val source = presenter.source as? HttpSource ?: return
|
||||||
|
|
||||||
|
activity?.openInBrowser(source.baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openInWebView() {
|
||||||
|
val source = presenter.source as? HttpSource ?: return
|
||||||
|
|
||||||
|
router.pushController(MangaWebViewController(source.id, source.baseUrl)
|
||||||
|
.withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restarts the request with a new query.
|
* Restarts the request with a new query.
|
||||||
*
|
*
|
||||||
@ -316,19 +342,22 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
|||||||
adapter.onLoadMoreComplete(null)
|
adapter.onLoadMoreComplete(null)
|
||||||
hideProgressBar()
|
hideProgressBar()
|
||||||
|
|
||||||
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
|
|
||||||
|
|
||||||
snack?.dismiss()
|
snack?.dismiss()
|
||||||
snack = catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) {
|
|
||||||
setAction(R.string.action_retry) {
|
if (catalogue_view != null) {
|
||||||
// If not the first page, show bottom progress bar.
|
val message = if (error is NoResultsException) catalogue_view.context.getString(R.string.no_results_found) else (error.message ?: "")
|
||||||
if (adapter.mainItemCount > 0) {
|
|
||||||
val item = progressItem ?: return@setAction
|
snack = catalogue_view.snack(message, Snackbar.LENGTH_INDEFINITE) {
|
||||||
adapter.addScrollableFooterWithDelay(item, 0, true)
|
setAction(R.string.action_retry) {
|
||||||
} else {
|
// If not the first page, show bottom progress bar.
|
||||||
showProgressBar()
|
if (adapter.mainItemCount > 0) {
|
||||||
|
val item = progressItem ?: return@setAction
|
||||||
|
adapter.addScrollableFooterWithDelay(item, 0, true)
|
||||||
|
} else {
|
||||||
|
showProgressBar()
|
||||||
|
}
|
||||||
|
presenter.requestNext()
|
||||||
}
|
}
|
||||||
presenter.requestNext()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -475,19 +504,21 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
|||||||
adapter?.notifyItemChanged(position)
|
adapter?.notifyItemChanged(position)
|
||||||
|
|
||||||
val categories = presenter.getCategories()
|
val categories = presenter.getCategories()
|
||||||
val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
|
val defaultCategoryId = preferences.defaultCategory()
|
||||||
if (defaultCategory != null) {
|
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
||||||
presenter.moveMangaToCategory(manga, defaultCategory)
|
when {
|
||||||
} else if (categories.size <= 1) { // default or the one from the user
|
defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory)
|
||||||
presenter.moveMangaToCategory(manga, categories.firstOrNull())
|
defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category
|
||||||
} else {
|
presenter.moveMangaToCategory(manga, null)
|
||||||
val ids = presenter.getMangaCategoryIds(manga)
|
else -> {
|
||||||
val preselected = ids.mapNotNull { id ->
|
val ids = presenter.getMangaCategoryIds(manga)
|
||||||
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
val preselected = ids.mapNotNull { id ->
|
||||||
}.toTypedArray()
|
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||||
.showDialog(router)
|
.showDialog(router)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||||
}
|
}
|
||||||
|
@ -316,9 +316,9 @@ open class BrowseCataloguePresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the default, and user categories.
|
* Get user categories.
|
||||||
*
|
*
|
||||||
* @return List of categories, default plus user categories
|
* @return List of categories, not including the default category
|
||||||
*/
|
*/
|
||||||
fun getCategories(): List<Category> {
|
fun getCategories(): List<Category> {
|
||||||
return db.getCategories().executeAsBlocking()
|
return db.getCategories().executeAsBlocking()
|
||||||
|
@ -28,7 +28,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
|
|||||||
val view = inflate(R.layout.catalogue_drawer_content)
|
val view = inflate(R.layout.catalogue_drawer_content)
|
||||||
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
|
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
|
||||||
addView(view)
|
addView(view)
|
||||||
title.text = context?.getString(R.string.source_search_options)
|
title.text = context.getString(R.string.source_search_options)
|
||||||
search_btn.setOnClickListener { onSearchClicked() }
|
search_btn.setOnClickListener { onSearchClicked() }
|
||||||
reset_btn.setOnClickListener { onResetClicked() }
|
reset_btn.setOnClickListener { onResetClicked() }
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
|
|||||||
*/
|
*/
|
||||||
private var bundle = Bundle()
|
private var bundle = Bundle()
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any?>?) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any?>) {
|
||||||
super.onBindViewHolder(holder, position, payloads)
|
super.onBindViewHolder(holder, position, payloads)
|
||||||
restoreHolderState(holder)
|
restoreHolderState(holder)
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@ class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
|
|||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY)
|
bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,9 +31,6 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
|
|||||||
// Set layout horizontal.
|
// Set layout horizontal.
|
||||||
recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
|
recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
recycler.adapter = mangaAdapter
|
recycler.adapter = mangaAdapter
|
||||||
|
|
||||||
nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp,
|
|
||||||
view.context.getResourceColor(android.R.attr.textColorHint))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,15 +51,15 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
|
|||||||
when {
|
when {
|
||||||
results == null -> {
|
results == null -> {
|
||||||
progress.visible()
|
progress.visible()
|
||||||
nothing_found.gone()
|
showHolder()
|
||||||
}
|
}
|
||||||
results.isEmpty() -> {
|
results.isEmpty() -> {
|
||||||
progress.gone()
|
progress.gone()
|
||||||
nothing_found.visible()
|
hideHolder()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
progress.gone()
|
progress.gone()
|
||||||
nothing_found.gone()
|
showHolder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (results !== lastBoundResults) {
|
if (results !== lastBoundResults) {
|
||||||
@ -96,4 +93,15 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
|
|||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showHolder() {
|
||||||
|
title.visible()
|
||||||
|
source_card.visible()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideHolder() {
|
||||||
|
title.gone()
|
||||||
|
source_card.gone()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
@ -157,9 +158,9 @@ open class CatalogueSearchPresenter(
|
|||||||
fetchSourcesSubscription?.unsubscribe()
|
fetchSourcesSubscription?.unsubscribe()
|
||||||
fetchSourcesSubscription = Observable.from(sources)
|
fetchSourcesSubscription = Observable.from(sources)
|
||||||
.flatMap({ source ->
|
.flatMap({ source ->
|
||||||
source.fetchSearchManga(1, query, FilterList())
|
Observable.defer { source.fetchSearchManga(1, query, FilterList()) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.onExceptionResumeNext(Observable.empty()) // Ignore timeouts.
|
.onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions
|
||||||
.map { it.mangas.take(10) } // Get at most 10 manga from search result.
|
.map { it.mangas.take(10) } // Get at most 10 manga from search result.
|
||||||
.map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
|
.map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
|
||||||
.doOnNext { fetchImage(it, source) } // Load manga covers.
|
.doOnNext { fetchImage(it, source) } // Load manga covers.
|
||||||
@ -239,7 +240,7 @@ open class CatalogueSearchPresenter(
|
|||||||
* @param sManga the manga from the source.
|
* @param sManga the manga from the source.
|
||||||
* @return a manga from the database.
|
* @return a manga from the database.
|
||||||
*/
|
*/
|
||||||
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
protected open fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||||
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
|
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
|
||||||
if (localManga == null) {
|
if (localManga == null) {
|
||||||
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.ui.extension
|
package eu.kanade.tachiyomi.ui.extension
|
||||||
|
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
|
import android.support.v7.widget.SearchView
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
||||||
|
import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@ -28,6 +32,10 @@ open class ExtensionController : NucleusController<ExtensionPresenter>(),
|
|||||||
*/
|
*/
|
||||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||||
|
|
||||||
|
private var extensions: List<ExtensionItem> = emptyList()
|
||||||
|
|
||||||
|
private var query = ""
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
}
|
}
|
||||||
@ -84,6 +92,30 @@ open class ExtensionController : NucleusController<ExtensionPresenter>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.extension_main, menu)
|
||||||
|
|
||||||
|
val searchItem = menu.findItem(R.id.action_search)
|
||||||
|
val searchView = searchItem.actionView as SearchView
|
||||||
|
searchView.maxWidth = Int.MAX_VALUE
|
||||||
|
|
||||||
|
if (!query.isEmpty()) {
|
||||||
|
searchItem.expandActionView()
|
||||||
|
searchView.setQuery(query, true)
|
||||||
|
searchView.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
searchView.queryTextChanges()
|
||||||
|
.filter { router.backstack.lastOrNull()?.controller() == this }
|
||||||
|
.subscribeUntilDestroy {
|
||||||
|
query = it.toString()
|
||||||
|
drawExtensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixes problem with the overflow icon showing up in lieu of search
|
||||||
|
searchItem.fixExpand()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onItemClick(position: Int): Boolean {
|
override fun onItemClick(position: Int): Boolean {
|
||||||
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
|
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
|
||||||
if (extension is Extension.Installed) {
|
if (extension is Extension.Installed) {
|
||||||
@ -114,7 +146,19 @@ open class ExtensionController : NucleusController<ExtensionPresenter>(),
|
|||||||
|
|
||||||
fun setExtensions(extensions: List<ExtensionItem>) {
|
fun setExtensions(extensions: List<ExtensionItem>) {
|
||||||
ext_swipe_refresh?.isRefreshing = false
|
ext_swipe_refresh?.isRefreshing = false
|
||||||
adapter?.updateDataSet(extensions)
|
this.extensions = extensions
|
||||||
|
drawExtensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun drawExtensions() {
|
||||||
|
if (!query.isBlank()) {
|
||||||
|
adapter?.updateDataSet(
|
||||||
|
extensions.filter {
|
||||||
|
it.extension.name.contains(query, ignoreCase = true)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
adapter?.updateDataSet(extensions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadUpdate(item: ExtensionItem) {
|
fun downloadUpdate(item: ExtensionItem) {
|
||||||
|
@ -47,7 +47,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun createPresenter(): ExtensionDetailsPresenter {
|
override fun createPresenter(): ExtensionDetailsPresenter {
|
||||||
return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY))
|
return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): String? {
|
override fun getTitle(): String? {
|
||||||
|
@ -13,7 +13,7 @@ class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecora
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
|
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
|
||||||
divider = a.getDrawable(0)
|
divider = a.getDrawable(0)!!
|
||||||
a.recycle()
|
a.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,18 +3,14 @@ package eu.kanade.tachiyomi.ui.extension
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import kotlinx.android.synthetic.main.extension_card_header.*
|
import kotlinx.android.synthetic.main.extension_card_header.title
|
||||||
|
|
||||||
class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<*>) :
|
class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<*>) :
|
||||||
BaseFlexibleViewHolder(view, adapter) {
|
BaseFlexibleViewHolder(view, adapter) {
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
fun bind(item: ExtensionGroupItem) {
|
fun bind(item: ExtensionGroupItem) {
|
||||||
title.text = when {
|
title.text = item.name
|
||||||
item.installed -> itemView.context.getString(R.string.ext_installed)
|
|
||||||
else -> itemView.context.getString(R.string.ext_available)
|
|
||||||
} + " (" + item.size + ")"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,12 @@ import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item that contains the language header.
|
* Item that contains the group header.
|
||||||
*
|
*
|
||||||
* @param code The lang code.
|
* @param name The header name.
|
||||||
|
* @param size The number of items in the group.
|
||||||
*/
|
*/
|
||||||
data class ExtensionGroupItem(val installed: Boolean, val size: Int) : AbstractHeaderItem<ExtensionGroupHolder>() {
|
data class ExtensionGroupItem(val name: String, val size: Int) : AbstractHeaderItem<ExtensionGroupHolder>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the layout resource of this item.
|
* Returns the layout resource of this item.
|
||||||
@ -38,13 +39,13 @@ data class ExtensionGroupItem(val installed: Boolean, val size: Int) : AbstractH
|
|||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other is ExtensionGroupItem) {
|
if (other is ExtensionGroupItem) {
|
||||||
return installed == other.installed
|
return name == other.name
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return installed.hashCode()
|
return name.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.ui.extension
|
package eu.kanade.tachiyomi.ui.extension
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -49,6 +52,8 @@ open class ExtensionPresenter(
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
|
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
|
||||||
|
val context = Injekt.get<Application>()
|
||||||
|
|
||||||
val (installed, untrusted, available) = tuple
|
val (installed, untrusted, available) = tuple
|
||||||
|
|
||||||
val items = mutableListOf<ExtensionItem>()
|
val items = mutableListOf<ExtensionItem>()
|
||||||
@ -62,7 +67,7 @@ open class ExtensionPresenter(
|
|||||||
.sortedBy { it.pkgName }
|
.sortedBy { it.pkgName }
|
||||||
|
|
||||||
if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
|
if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
|
||||||
val header = ExtensionGroupItem(true, installedSorted.size + untrustedSorted.size)
|
val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size)
|
||||||
items += installedSorted.map { extension ->
|
items += installedSorted.map { extension ->
|
||||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
||||||
}
|
}
|
||||||
@ -71,10 +76,17 @@ open class ExtensionPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (availableSorted.isNotEmpty()) {
|
if (availableSorted.isNotEmpty()) {
|
||||||
val header = ExtensionGroupItem(false, availableSorted.size)
|
val availableGroupedByLang = availableSorted
|
||||||
items += availableSorted.map { extension ->
|
.groupBy { LocaleHelper.getDisplayName(it.lang, context) }
|
||||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
.toSortedMap()
|
||||||
}
|
|
||||||
|
availableGroupedByLang
|
||||||
|
.forEach {
|
||||||
|
val header = ExtensionGroupItem(it.key, it.value.size)
|
||||||
|
items += it.value.map { extension ->
|
||||||
|
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.extensions = items
|
this.extensions = items
|
||||||
|
@ -24,10 +24,10 @@ class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
|||||||
.positiveText(R.string.ext_trust)
|
.positiveText(R.string.ext_trust)
|
||||||
.negativeText(R.string.ext_uninstall)
|
.negativeText(R.string.ext_uninstall)
|
||||||
.onPositive { _, _ ->
|
.onPositive { _, _ ->
|
||||||
(targetController as? Listener)?.trustSignature(args.getString(SIGNATURE_KEY))
|
(targetController as? Listener)?.trustSignature(args.getString(SIGNATURE_KEY)!!)
|
||||||
}
|
}
|
||||||
.onNegative { _, _ ->
|
.onNegative { _, _ ->
|
||||||
(targetController as? Listener)?.uninstallExtension(args.getString(PKGNAME_KEY))
|
(targetController as? Listener)?.uninstallExtension(args.getString(PKGNAME_KEY)!!)
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -185,7 +185,7 @@ class LibraryPresenter(
|
|||||||
|
|
||||||
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
|
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
|
||||||
when (sortingMode) {
|
when (sortingMode) {
|
||||||
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title)
|
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
|
||||||
LibrarySort.LAST_READ -> {
|
LibrarySort.LAST_READ -> {
|
||||||
// Get index of manga, set equal to list if size unknown.
|
// Get index of manga, set equal to list if size unknown.
|
||||||
val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size
|
val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size
|
||||||
|
@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
|
|||||||
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
|
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
|
||||||
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
|
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
|
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
|
||||||
|
import eu.kanade.tachiyomi.util.openInBrowser
|
||||||
import kotlinx.android.synthetic.main.main_activity.*
|
import kotlinx.android.synthetic.main.main_activity.*
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
@ -91,6 +92,9 @@ class MainActivity : BaseActivity() {
|
|||||||
R.id.nav_drawer_settings -> {
|
R.id.nav_drawer_settings -> {
|
||||||
router.pushController(SettingsMainController().withFadeTransaction())
|
router.pushController(SettingsMainController().withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
R.id.nav_drawer_help -> {
|
||||||
|
openInBrowser(URL_HELP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drawer.closeDrawer(GravityCompat.START)
|
drawer.closeDrawer(GravityCompat.START)
|
||||||
@ -271,6 +275,8 @@ class MainActivity : BaseActivity() {
|
|||||||
const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
|
const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
|
||||||
const val INTENT_SEARCH_QUERY = "query"
|
const val INTENT_SEARCH_QUERY = "query"
|
||||||
const val INTENT_SEARCH_FILTER = "filter"
|
const val INTENT_SEARCH_FILTER = "filter"
|
||||||
|
|
||||||
|
private const val URL_HELP = "https://tachiyomi.org/help/"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,6 @@ class MangaController : RxController, TabbedController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
|
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
|
||||||
const val MANGA_EXTRA = "manga"
|
const val MANGA_EXTRA = "manga"
|
||||||
|
|
||||||
@ -187,9 +186,8 @@ class MangaController : RxController, TabbedController {
|
|||||||
const val CHAPTERS_CONTROLLER = 1
|
const val CHAPTERS_CONTROLLER = 1
|
||||||
const val TRACK_CONTROLLER = 2
|
const val TRACK_CONTROLLER = 2
|
||||||
|
|
||||||
private val tabField = TabLayout.Tab::class.java.getDeclaredField("mView")
|
private val tabField = TabLayout.Tab::class.java.getDeclaredField("view")
|
||||||
.apply { isAccessible = true }
|
.apply { isAccessible = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -404,8 +404,12 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
|
|||||||
presenter.deleteChapters(chapters)
|
presenter.deleteChapters(chapters)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onChaptersDeleted() {
|
fun onChaptersDeleted(chapters: List<ChapterItem>) {
|
||||||
dismissDeletingDialog()
|
dismissDeletingDialog()
|
||||||
|
//this is needed so the downloaded text gets removed from the item
|
||||||
|
chapters.forEach {
|
||||||
|
adapter?.updateItem(it)
|
||||||
|
}
|
||||||
adapter?.notifyDataSetChanged()
|
adapter?.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,7 +278,7 @@ class ChaptersPresenter(
|
|||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst({ view, _ ->
|
.subscribeFirst({ view, _ ->
|
||||||
view.onChaptersDeleted()
|
view.onChaptersDeleted(chapters)
|
||||||
}, ChaptersController::onChaptersDeletedError)
|
}, ChaptersController::onChaptersDeletedError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
|||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.getResourceColor
|
import eu.kanade.tachiyomi.util.getResourceColor
|
||||||
|
import eu.kanade.tachiyomi.util.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.snack
|
import eu.kanade.tachiyomi.util.snack
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import eu.kanade.tachiyomi.util.truncateCenter
|
import eu.kanade.tachiyomi.util.truncateCenter
|
||||||
@ -87,6 +88,9 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
// Set onclickListener to toggle favorite when FAB clicked.
|
// Set onclickListener to toggle favorite when FAB clicked.
|
||||||
fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
|
fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
|
||||||
|
|
||||||
|
// Set onLongClickListener to manage categories when FAB is clicked.
|
||||||
|
fab_favorite.longClicks().subscribeUntilDestroy{ onFabLongClick() }
|
||||||
|
|
||||||
// Set SwipeRefresh to refresh manga data.
|
// Set SwipeRefresh to refresh manga data.
|
||||||
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
|
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
|
||||||
|
|
||||||
@ -287,15 +291,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
val context = view?.context ?: return
|
val context = view?.context ?: return
|
||||||
val source = presenter.source as? HttpSource ?: return
|
val source = presenter.source as? HttpSource ?: return
|
||||||
|
|
||||||
try {
|
context.openInBrowser(source.mangaDetailsRequest(presenter.manga).url().toString())
|
||||||
val url = Uri.parse(source.mangaDetailsRequest(presenter.manga).url().toString())
|
|
||||||
val intent = CustomTabsIntent.Builder()
|
|
||||||
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
|
|
||||||
.build()
|
|
||||||
intent.launchUrl(activity, url)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
context.toast(e.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openInWebView() {
|
private fun openInWebView() {
|
||||||
@ -386,11 +382,12 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
toggleFavorite()
|
toggleFavorite()
|
||||||
if (manga.favorite) {
|
if (manga.favorite) {
|
||||||
val categories = presenter.getCategories()
|
val categories = presenter.getCategories()
|
||||||
val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
|
val defaultCategoryId = preferences.defaultCategory()
|
||||||
|
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
||||||
when {
|
when {
|
||||||
defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory)
|
defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory)
|
||||||
categories.size <= 1 -> // default or the one from the user
|
defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category
|
||||||
presenter.moveMangaToCategory(manga, categories.firstOrNull())
|
presenter.moveMangaToCategory(manga, null)
|
||||||
else -> {
|
else -> {
|
||||||
val ids = presenter.getMangaCategoryIds(manga)
|
val ids = presenter.getMangaCategoryIds(manga)
|
||||||
val preselected = ids.mapNotNull { id ->
|
val preselected = ids.mapNotNull { id ->
|
||||||
@ -407,6 +404,30 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the fab is long clicked.
|
||||||
|
*/
|
||||||
|
private fun onFabLongClick() {
|
||||||
|
val manga = presenter.manga
|
||||||
|
if (!manga.favorite) {
|
||||||
|
toggleFavorite()
|
||||||
|
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||||
|
}
|
||||||
|
val categories = presenter.getCategories()
|
||||||
|
if (categories.isEmpty()) {
|
||||||
|
// no categories exist, display a message about adding categories
|
||||||
|
activity?.toast(activity?.getString(R.string.action_add_category))
|
||||||
|
} else {
|
||||||
|
val ids = presenter.getMangaCategoryIds(manga)
|
||||||
|
val preselected = ids.mapNotNull { id ->
|
||||||
|
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||||
|
.showDialog(router)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
||||||
val manga = mangas.firstOrNull() ?: return
|
val manga = mangas.firstOrNull() ?: return
|
||||||
presenter.moveMangaToCategories(manga, categories)
|
presenter.moveMangaToCategories(manga, categories)
|
||||||
|
@ -130,9 +130,9 @@ class MangaInfoPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the default, and user categories.
|
* Get user categories.
|
||||||
*
|
*
|
||||||
* @return List of categories, default plus user categories
|
* @return List of categories, not including the default category
|
||||||
*/
|
*/
|
||||||
fun getCategories(): List<Category> {
|
fun getCategories(): List<Category> {
|
||||||
return db.getCategories().executeAsBlocking()
|
return db.getCategories().executeAsBlocking()
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.info
|
package eu.kanade.tachiyomi.ui.manga.info
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.*
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
@ -16,6 +14,10 @@ class MangaWebViewController(bundle: Bundle? = null) : BaseController(bundle) {
|
|||||||
|
|
||||||
private val sourceManager by injectLazy<SourceManager>()
|
private val sourceManager by injectLazy<SourceManager>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
constructor(sourceId: Long, url: String) : this(Bundle().apply {
|
constructor(sourceId: Long, url: String) : this(Bundle().apply {
|
||||||
putLong(SOURCE_KEY, sourceId)
|
putLong(SOURCE_KEY, sourceId)
|
||||||
putString(URL_KEY, url)
|
putString(URL_KEY, url)
|
||||||
@ -43,6 +45,40 @@ class MangaWebViewController(bundle: Bundle? = null) : BaseController(bundle) {
|
|||||||
web.loadUrl(url, headers)
|
web.loadUrl(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.web_view, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
|
val web = view as WebView
|
||||||
|
menu.findItem(R.id.action_forward).isVisible = web.canGoForward()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.action_forward -> {
|
||||||
|
val web = view as WebView
|
||||||
|
if (web.canGoForward()) web.goForward()
|
||||||
|
}
|
||||||
|
R.id.action_refresh -> {
|
||||||
|
val web = view as WebView
|
||||||
|
web.reload()
|
||||||
|
}
|
||||||
|
R.id.action_close -> router.popController(this)
|
||||||
|
else -> return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleBack(): Boolean {
|
||||||
|
val web = view as WebView
|
||||||
|
if (web.canGoBack()) {
|
||||||
|
web.goBack()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.handleBack()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
val web = view as WebView
|
val web = view as WebView
|
||||||
web.stopLoading()
|
web.stopLoading()
|
||||||
|
@ -52,27 +52,27 @@ class TrackSearchAdapter(context: Context)
|
|||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.into(view.track_search_cover)
|
.into(view.track_search_cover)
|
||||||
|
}
|
||||||
|
|
||||||
if (track.publishing_status.isNullOrBlank()) {
|
if (track.publishing_status.isNullOrBlank()) {
|
||||||
view.track_search_status.gone()
|
view.track_search_status.gone()
|
||||||
view.track_search_status_result.gone()
|
view.track_search_status_result.gone()
|
||||||
} else {
|
} else {
|
||||||
view.track_search_status_result.text = track.publishing_status.capitalize()
|
view.track_search_status_result.text = track.publishing_status.capitalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.publishing_type.isNullOrBlank()) {
|
if (track.publishing_type.isNullOrBlank()) {
|
||||||
view.track_search_type.gone()
|
view.track_search_type.gone()
|
||||||
view.track_search_type_result.gone()
|
view.track_search_type_result.gone()
|
||||||
} else {
|
} else {
|
||||||
view.track_search_type_result.text = track.publishing_type.capitalize()
|
view.track_search_type_result.text = track.publishing_type.capitalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.start_date.isNullOrBlank()) {
|
if (track.start_date.isNullOrBlank()) {
|
||||||
view.track_search_start.gone()
|
view.track_search_start.gone()
|
||||||
view.track_search_start_result.gone()
|
view.track_search_start_result.gone()
|
||||||
} else {
|
} else {
|
||||||
view.track_search_start_result.text = track.start_date
|
view.track_search_start_result.text = track.start_date
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,6 +146,9 @@ class MigrationPresenter(
|
|||||||
}
|
}
|
||||||
manga.favorite = true
|
manga.favorite = true
|
||||||
db.updateMangaFavorite(manga).executeAsBlocking()
|
db.updateMangaFavorite(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
|
||||||
|
db.updateMangaTitle(manga).executeAsBlocking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.migration
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchCardItem
|
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchCardItem
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchItem
|
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchItem
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
|
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
|
||||||
@ -21,4 +22,11 @@ class SearchPresenter(
|
|||||||
//Set the catalogue search item as highlighted if the source matches that of the selected manga
|
//Set the catalogue search item as highlighted if the source matches that of the selected manga
|
||||||
return CatalogueSearchItem(source, results, source.id == manga.source)
|
return CatalogueSearchItem(source, results, source.id == manga.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||||
|
val localManga = super.networkToLocalManga(sManga, sourceId)
|
||||||
|
// For migration, displayed title should always match source rather than local DB
|
||||||
|
localManga.title = sManga.title
|
||||||
|
return localManga
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
|||||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
|
import kotlinx.android.synthetic.main.catalogue_main_controller_card.title
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item that contains the selection header.
|
* Item that contains the selection header.
|
||||||
@ -36,7 +36,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
|
|||||||
|
|
||||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : BaseFlexibleViewHolder(view, adapter) {
|
class Holder(view: View, adapter: FlexibleAdapter<*>) : BaseFlexibleViewHolder(view, adapter) {
|
||||||
init {
|
init {
|
||||||
title.text = "Please select a source to migrate from"
|
title.text = view.context.getString(R.string.migration_selection_prompt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,8 +119,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
setContentView(R.layout.reader_activity)
|
setContentView(R.layout.reader_activity)
|
||||||
|
|
||||||
if (presenter.needsInit()) {
|
if (presenter.needsInit()) {
|
||||||
val manga = intent.extras.getLong("manga", -1)
|
val manga = intent.extras!!.getLong("manga", -1)
|
||||||
val chapter = intent.extras.getLong("chapter", -1)
|
val chapter = intent.extras!!.getLong("chapter", -1)
|
||||||
|
|
||||||
if (manga == -1L || chapter == -1L) {
|
if (manga == -1L || chapter == -1L) {
|
||||||
finish()
|
finish()
|
||||||
@ -574,6 +574,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
|
|
||||||
subscriptions += preferences.colorFilter().asObservable()
|
subscriptions += preferences.colorFilter().asObservable()
|
||||||
.subscribe { setColorFilter(it) }
|
.subscribe { setColorFilter(it) }
|
||||||
|
|
||||||
|
subscriptions += preferences.colorFilterMode().asObservable()
|
||||||
|
.subscribe { setColorFilter(preferences.colorFilter().getOrDefault()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -722,7 +725,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
*/
|
*/
|
||||||
private fun setColorFilterValue(value: Int) {
|
private fun setColorFilterValue(value: Int) {
|
||||||
color_overlay.visibility = View.VISIBLE
|
color_overlay.visibility = View.VISIBLE
|
||||||
color_overlay.setBackgroundColor(value)
|
color_overlay.setFilterColor(value, preferences.colorFilterMode().getOrDefault())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.util.plusAssign
|
import eu.kanade.tachiyomi.util.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
|
||||||
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
|
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
|
||||||
import kotlinx.android.synthetic.main.reader_color_filter.*
|
import kotlinx.android.synthetic.main.reader_color_filter.*
|
||||||
import kotlinx.android.synthetic.main.reader_color_filter_sheet.*
|
import kotlinx.android.synthetic.main.reader_color_filter_sheet.*
|
||||||
@ -54,6 +55,9 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
|
|||||||
subscriptions += preferences.colorFilter().asObservable()
|
subscriptions += preferences.colorFilter().asObservable()
|
||||||
.subscribe { setColorFilter(it, view) }
|
.subscribe { setColorFilter(it, view) }
|
||||||
|
|
||||||
|
subscriptions += preferences.colorFilterMode().asObservable()
|
||||||
|
.subscribe { setColorFilter(preferences.colorFilter().getOrDefault(), view) }
|
||||||
|
|
||||||
subscriptions += preferences.customBrightness().asObservable()
|
subscriptions += preferences.customBrightness().asObservable()
|
||||||
.subscribe { setCustomBrightness(it, view) }
|
.subscribe { setCustomBrightness(it, view) }
|
||||||
|
|
||||||
@ -84,6 +88,11 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
|
|||||||
preferences.customBrightness().set(isChecked)
|
preferences.customBrightness().set(isChecked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
color_filter_mode.onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
|
||||||
|
preferences.colorFilterMode().set(position)
|
||||||
|
}
|
||||||
|
color_filter_mode.setSelection(preferences.colorFilterMode().getOrDefault(), false)
|
||||||
|
|
||||||
seekbar_color_filter_alpha.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
|
seekbar_color_filter_alpha.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
|
||||||
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
|
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
@ -248,7 +257,7 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
|
|||||||
*/
|
*/
|
||||||
private fun setColorFilterValue(@ColorInt color: Int, view: View) = with(view) {
|
private fun setColorFilterValue(@ColorInt color: Int, view: View) = with(view) {
|
||||||
color_overlay.visibility = View.VISIBLE
|
color_overlay.visibility = View.VISIBLE
|
||||||
color_overlay.setBackgroundColor(color)
|
color_overlay.setFilterColor(color, preferences.colorFilterMode().getOrDefault())
|
||||||
setValues(color, view)
|
setValues(color, view)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.reader
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.*
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
class ReaderColorFilterView(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null
|
||||||
|
) : View(context, attrs) {
|
||||||
|
|
||||||
|
private val colorFilterPaint: Paint = Paint()
|
||||||
|
|
||||||
|
fun setFilterColor(color: Int, filterMode: Int) {
|
||||||
|
colorFilterPaint.setColor(color)
|
||||||
|
colorFilterPaint.xfermode = PorterDuffXfermode(when (filterMode) {
|
||||||
|
1 -> PorterDuff.Mode.MULTIPLY
|
||||||
|
2 -> PorterDuff.Mode.SCREEN
|
||||||
|
3 -> PorterDuff.Mode.OVERLAY
|
||||||
|
4 -> PorterDuff.Mode.LIGHTEN
|
||||||
|
5 -> PorterDuff.Mode.DARKEN
|
||||||
|
else -> PorterDuff.Mode.SRC_OVER
|
||||||
|
})
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
canvas.drawPaint(colorFilterPaint)
|
||||||
|
}
|
||||||
|
}
|
@ -147,10 +147,9 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the user pressed the back button and is going to leave the reader. Used to
|
* Called when the user pressed the back button and is going to leave the reader. Used to
|
||||||
* update tracking services and trigger deletion of the downloaded chapters.
|
* trigger deletion of the downloaded chapters.
|
||||||
*/
|
*/
|
||||||
fun onBackPressed() {
|
fun onBackPressed() {
|
||||||
updateTrackLastChapterRead()
|
|
||||||
deletePendingChapters()
|
deletePendingChapters()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,7 +307,7 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Called every time a page changes on the reader. Used to mark the flag of chapters being
|
* Called every time a page changes on the reader. Used to mark the flag of chapters being
|
||||||
* read, enqueue downloaded chapter deletion, and updating the active chapter if this
|
* read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this
|
||||||
* [page]'s chapter is different from the currently active.
|
* [page]'s chapter is different from the currently active.
|
||||||
*/
|
*/
|
||||||
fun onPageSelected(page: ReaderPage) {
|
fun onPageSelected(page: ReaderPage) {
|
||||||
@ -320,6 +319,7 @@ class ReaderPresenter(
|
|||||||
selectedChapter.chapter.last_page_read = page.index
|
selectedChapter.chapter.last_page_read = page.index
|
||||||
if (selectedChapter.pages?.lastIndex == page.index) {
|
if (selectedChapter.pages?.lastIndex == page.index) {
|
||||||
selectedChapter.chapter.read = true
|
selectedChapter.chapter.read = true
|
||||||
|
updateTrackChapterRead(selectedChapter)
|
||||||
enqueueDeleteReadChapters(selectedChapter)
|
enqueueDeleteReadChapters(selectedChapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,7 +434,8 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
// Build destination file.
|
// Build destination file.
|
||||||
val filename = DiskUtil.buildValidFilename(
|
val filename = DiskUtil.buildValidFilename(
|
||||||
"${manga.title} - ${chapter.name}") + " - ${page.number}.${type.extension}"
|
"${manga.title} - ${chapter.name}".take(225)
|
||||||
|
) + " - ${page.number}.${type.extension}"
|
||||||
|
|
||||||
val destFile = File(directory, filename)
|
val destFile = File(directory, filename)
|
||||||
stream().use { input ->
|
stream().use { input ->
|
||||||
@ -497,7 +498,7 @@ class ReaderPresenter(
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst(
|
.subscribeFirst(
|
||||||
{ view, file -> view.onShareImageResult(file) },
|
{ view, file -> view.onShareImageResult(file) },
|
||||||
{ view, error -> /* Empty */ }
|
{ _, _ -> /* Empty */ }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -553,21 +554,11 @@ class ReaderPresenter(
|
|||||||
* Starts the service that updates the last chapter read in sync services. This operation
|
* Starts the service that updates the last chapter read in sync services. This operation
|
||||||
* will run in a background thread and errors are ignored.
|
* will run in a background thread and errors are ignored.
|
||||||
*/
|
*/
|
||||||
private fun updateTrackLastChapterRead() {
|
private fun updateTrackChapterRead(readerChapter: ReaderChapter) {
|
||||||
if (!preferences.autoUpdateTrack()) return
|
if (!preferences.autoUpdateTrack()) return
|
||||||
val viewerChapters = viewerChaptersRelay.value ?: return
|
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
|
|
||||||
val currChapter = viewerChapters.currChapter.chapter
|
val chapterRead = readerChapter.chapter.chapter_number.toInt()
|
||||||
val prevChapter = viewerChapters.prevChapter?.chapter
|
|
||||||
|
|
||||||
// Get the last chapter read from the reader.
|
|
||||||
val lastChapterRead = if (currChapter.read)
|
|
||||||
currChapter.chapter_number.toInt()
|
|
||||||
else if (prevChapter != null && prevChapter.read)
|
|
||||||
prevChapter.chapter_number.toInt()
|
|
||||||
else
|
|
||||||
return
|
|
||||||
|
|
||||||
val trackManager = Injekt.get<TrackManager>()
|
val trackManager = Injekt.get<TrackManager>()
|
||||||
|
|
||||||
@ -575,8 +566,8 @@ class ReaderPresenter(
|
|||||||
.flatMapCompletable { trackList ->
|
.flatMapCompletable { trackList ->
|
||||||
Completable.concat(trackList.map { track ->
|
Completable.concat(trackList.map { track ->
|
||||||
val service = trackManager.getService(track.sync_id)
|
val service = trackManager.getService(track.sync_id)
|
||||||
if (service != null && service.isLogged && lastChapterRead > track.last_chapter_read) {
|
if (service != null && service.isLogged && chapterRead > track.last_chapter_read) {
|
||||||
track.last_chapter_read = lastChapterRead
|
track.last_chapter_read = chapterRead
|
||||||
|
|
||||||
// We wan't these to execute even if the presenter is destroyed and leaks
|
// We wan't these to execute even if the presenter is destroyed and leaks
|
||||||
// for a while. The view can still be garbage collected.
|
// for a while. The view can still be garbage collected.
|
||||||
|
@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.reader.loader
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator
|
||||||
import eu.kanade.tachiyomi.util.ImageUtil
|
import eu.kanade.tachiyomi.util.ImageUtil
|
||||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
@ -18,11 +18,9 @@ class DirectoryPageLoader(val file: File) : PageLoader() {
|
|||||||
* comparator.
|
* comparator.
|
||||||
*/
|
*/
|
||||||
override fun getPages(): Observable<List<ReaderPage>> {
|
override fun getPages(): Observable<List<ReaderPage>> {
|
||||||
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
|
||||||
|
|
||||||
return file.listFiles()
|
return file.listFiles()
|
||||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||||
.sortedWith(Comparator<File> { f1, f2 -> comparator.compare(f1.name, f2.name) })
|
.sortedWith(Comparator<File> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) })
|
||||||
.mapIndexed { i, file ->
|
.mapIndexed { i, file ->
|
||||||
val streamFn = { FileInputStream(file) }
|
val streamFn = { FileInputStream(file) }
|
||||||
ReaderPage(i).apply {
|
ReaderPage(i).apply {
|
||||||
|
@ -32,9 +32,9 @@ class DownloadPageLoader(
|
|||||||
return downloadManager.buildPageList(source, manga, chapter.chapter)
|
return downloadManager.buildPageList(source, manga, chapter.chapter)
|
||||||
.map { pages ->
|
.map { pages ->
|
||||||
pages.map { page ->
|
pages.map { page ->
|
||||||
ReaderPage(page.index, page.url, page.imageUrl, {
|
ReaderPage(page.index, page.url, page.imageUrl) {
|
||||||
context.contentResolver.openInputStream(page.uri)
|
context.contentResolver.openInputStream(page.uri)
|
||||||
}).apply {
|
}.apply {
|
||||||
status = Page.READY
|
status = Page.READY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.PriorityBlockingQueue
|
import java.util.concurrent.PriorityBlockingQueue
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loader used to load chapters from an online source.
|
* Loader used to load chapters from an online source.
|
||||||
@ -37,18 +38,20 @@ class HttpPageLoader(
|
|||||||
*/
|
*/
|
||||||
private val subscriptions = CompositeSubscription()
|
private val subscriptions = CompositeSubscription()
|
||||||
|
|
||||||
|
private val preloadSize = 4
|
||||||
|
|
||||||
init {
|
init {
|
||||||
subscriptions += Observable.defer { Observable.just(queue.take().page) }
|
subscriptions += Observable.defer { Observable.just(queue.take().page) }
|
||||||
.filter { it.status == Page.QUEUE }
|
.filter { it.status == Page.QUEUE }
|
||||||
.concatMap { source.fetchImageFromCacheThenNet(it) }
|
.concatMap { source.fetchImageFromCacheThenNet(it) }
|
||||||
.repeat()
|
.repeat()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
}, { error ->
|
}, { error ->
|
||||||
if (error !is InterruptedException) {
|
if (error !is InterruptedException) {
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -80,13 +83,13 @@ class HttpPageLoader(
|
|||||||
*/
|
*/
|
||||||
override fun getPages(): Observable<List<ReaderPage>> {
|
override fun getPages(): Observable<List<ReaderPage>> {
|
||||||
return chapterCache
|
return chapterCache
|
||||||
.getPageListFromCache(chapter.chapter)
|
.getPageListFromCache(chapter.chapter)
|
||||||
.onErrorResumeNext { source.fetchPageList(chapter.chapter) }
|
.onErrorResumeNext { source.fetchPageList(chapter.chapter) }
|
||||||
.map { pages ->
|
.map { pages ->
|
||||||
pages.mapIndexed { index, page -> // Don't trust sources and use our own indexing
|
pages.mapIndexed { index, page -> // Don't trust sources and use our own indexing
|
||||||
ReaderPage(index, page.url, page.imageUrl)
|
ReaderPage(index, page.url, page.imageUrl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -110,29 +113,41 @@ class HttpPageLoader(
|
|||||||
val statusSubject = SerializedSubject(PublishSubject.create<Int>())
|
val statusSubject = SerializedSubject(PublishSubject.create<Int>())
|
||||||
page.setStatusSubject(statusSubject)
|
page.setStatusSubject(statusSubject)
|
||||||
|
|
||||||
|
val queuedPages = mutableListOf<PriorityPage>()
|
||||||
if (page.status == Page.QUEUE) {
|
if (page.status == Page.QUEUE) {
|
||||||
queue.offer(PriorityPage(page, 1))
|
queuedPages += PriorityPage(page, 1).also { queue.offer(it) }
|
||||||
}
|
}
|
||||||
|
queuedPages += preloadNextPages(page, preloadSize)
|
||||||
preloadNextPages(page, 4)
|
|
||||||
|
|
||||||
statusSubject.startWith(page.status)
|
statusSubject.startWith(page.status)
|
||||||
|
.doOnUnsubscribe {
|
||||||
|
queuedPages.forEach {
|
||||||
|
if (it.page.status == Page.QUEUE) {
|
||||||
|
queue.remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.unsubscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preloads the given [amount] of pages after the [currentPage] with a lower priority.
|
* Preloads the given [amount] of pages after the [currentPage] with a lower priority.
|
||||||
|
* @return a list of [PriorityPage] that were added to the [queue]
|
||||||
*/
|
*/
|
||||||
private fun preloadNextPages(currentPage: ReaderPage, amount: Int) {
|
private fun preloadNextPages(currentPage: ReaderPage, amount: Int): List<PriorityPage> {
|
||||||
val pageIndex = currentPage.index
|
val pageIndex = currentPage.index
|
||||||
val pages = currentPage.chapter.pages ?: return
|
val pages = currentPage.chapter.pages ?: return emptyList()
|
||||||
if (pageIndex == pages.lastIndex) return
|
if (pageIndex == pages.lastIndex) return emptyList()
|
||||||
val nextPages = pages.subList(pageIndex + 1, Math.min(pageIndex + 1 + amount, pages.size))
|
|
||||||
for (nextPage in nextPages) {
|
return pages
|
||||||
if (nextPage.status == Page.QUEUE) {
|
.subList(pageIndex + 1, min(pageIndex + 1 + amount, pages.size))
|
||||||
queue.offer(PriorityPage(nextPage, 0))
|
.mapNotNull {
|
||||||
}
|
if (it.status == Page.QUEUE) {
|
||||||
}
|
PriorityPage(it, 0).apply { queue.offer(this) }
|
||||||
|
} else null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -148,7 +163,7 @@ class HttpPageLoader(
|
|||||||
/**
|
/**
|
||||||
* Data class used to keep ordering of pages in order to maintain priority.
|
* Data class used to keep ordering of pages in order to maintain priority.
|
||||||
*/
|
*/
|
||||||
private data class PriorityPage(
|
private class PriorityPage(
|
||||||
val page: ReaderPage,
|
val page: ReaderPage,
|
||||||
val priority: Int
|
val priority: Int
|
||||||
): Comparable<PriorityPage> {
|
): Comparable<PriorityPage> {
|
||||||
|
@ -2,10 +2,10 @@ package eu.kanade.tachiyomi.ui.reader.loader
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator
|
||||||
import eu.kanade.tachiyomi.util.ImageUtil
|
import eu.kanade.tachiyomi.util.ImageUtil
|
||||||
import junrar.Archive
|
import junrar.Archive
|
||||||
import junrar.rarfile.FileHeader
|
import junrar.rarfile.FileHeader
|
||||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -42,11 +42,9 @@ class RarPageLoader(file: File) : PageLoader() {
|
|||||||
* comparator.
|
* comparator.
|
||||||
*/
|
*/
|
||||||
override fun getPages(): Observable<List<ReaderPage>> {
|
override fun getPages(): Observable<List<ReaderPage>> {
|
||||||
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
|
||||||
|
|
||||||
return archive.fileHeaders
|
return archive.fileHeaders
|
||||||
.filter { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } }
|
.filter { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } }
|
||||||
.sortedWith(Comparator<FileHeader> { f1, f2 -> comparator.compare(f1.fileNameString, f2.fileNameString) })
|
.sortedWith(Comparator<FileHeader> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.fileNameString, f2.fileNameString) })
|
||||||
.mapIndexed { i, header ->
|
.mapIndexed { i, header ->
|
||||||
val streamFn = { getStream(header) }
|
val streamFn = { getStream(header) }
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.reader.loader
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator
|
||||||
import eu.kanade.tachiyomi.util.ImageUtil
|
import eu.kanade.tachiyomi.util.ImageUtil
|
||||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
@ -32,11 +32,9 @@ class ZipPageLoader(file: File) : PageLoader() {
|
|||||||
* comparator.
|
* comparator.
|
||||||
*/
|
*/
|
||||||
override fun getPages(): Observable<List<ReaderPage>> {
|
override fun getPages(): Observable<List<ReaderPage>> {
|
||||||
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
|
||||||
|
|
||||||
return zip.entries().toList()
|
return zip.entries().toList()
|
||||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||||
.sortedWith(Comparator<ZipEntry> { f1, f2 -> comparator.compare(f1.name, f2.name) })
|
.sortedWith(Comparator<ZipEntry> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) })
|
||||||
.mapIndexed { i, entry ->
|
.mapIndexed { i, entry ->
|
||||||
val streamFn = { zip.getInputStream(entry) }
|
val streamFn = { zip.getInputStream(entry) }
|
||||||
ReaderPage(i).apply {
|
ReaderPage(i).apply {
|
||||||
|
@ -19,6 +19,7 @@ import com.bumptech.glide.load.DataSource
|
|||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||||
|
import com.bumptech.glide.load.resource.gif.GifDrawable
|
||||||
import com.bumptech.glide.request.RequestListener
|
import com.bumptech.glide.request.RequestListener
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import com.bumptech.glide.request.transition.NoTransition
|
import com.bumptech.glide.request.transition.NoTransition
|
||||||
@ -457,6 +458,9 @@ class PagerPageHolder(
|
|||||||
dataSource: DataSource?,
|
dataSource: DataSource?,
|
||||||
isFirstResource: Boolean
|
isFirstResource: Boolean
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
if (resource is GifDrawable) {
|
||||||
|
resource.setLoopCount(GifDrawable.LOOP_INTRINSIC)
|
||||||
|
}
|
||||||
onImageDecoded()
|
onImageDecoded()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,7 @@ class PagerTransitionHolder(
|
|||||||
* Text view used to display the text of the current and next/prev chapters.
|
* Text view used to display the text of the current and next/prev chapters.
|
||||||
*/
|
*/
|
||||||
private var textView = TextView(context).apply {
|
private var textView = TextView(context).apply {
|
||||||
|
textSize = 17.5F
|
||||||
wrapContent()
|
wrapContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,14 +70,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
|||||||
pager.adapter = adapter
|
pager.adapter = adapter
|
||||||
pager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
|
pager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
|
||||||
override fun onPageSelected(position: Int) {
|
override fun onPageSelected(position: Int) {
|
||||||
val page = adapter.items.getOrNull(position)
|
onPageChange(position)
|
||||||
if (page != null && currentPage != page) {
|
|
||||||
currentPage = page
|
|
||||||
when (page) {
|
|
||||||
is ReaderPage -> onPageSelected(page, position)
|
|
||||||
is ChapterTransition -> onTransitionSelected(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPageScrollStateChanged(state: Int) {
|
override fun onPageScrollStateChanged(state: Int) {
|
||||||
@ -129,25 +122,38 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from the ViewPager listener when a [page] is marked as active. It notifies the
|
* Called when a new page (either a [ReaderPage] or [ChapterTransition]) is marked as active
|
||||||
* activity of the change and requests the preload of the next chapter if this is the last page.
|
|
||||||
*/
|
*/
|
||||||
private fun onPageSelected(page: ReaderPage, position: Int) {
|
private fun onPageChange(position: Int) {
|
||||||
val pages = page.chapter.pages!! // Won't be null because it's the loaded chapter
|
val page = adapter.items.getOrNull(position)
|
||||||
Timber.d("onPageSelected: ${page.number}/${pages.size}")
|
if (page != null && currentPage != page) {
|
||||||
activity.onPageSelected(page)
|
currentPage = page
|
||||||
|
when (page) {
|
||||||
if (page === pages.last()) {
|
is ReaderPage -> onReaderPageSelected(page)
|
||||||
Timber.d("Request preload next chapter because we're at the last page")
|
is ChapterTransition -> onTransitionSelected(page)
|
||||||
val transition = adapter.items.getOrNull(position + 1) as? ChapterTransition.Next
|
|
||||||
if (transition?.to != null) {
|
|
||||||
activity.requestPreloadChapter(transition.to)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from the ViewPager listener when a [transition] is marked as active. It request the
|
* Called when a [ReaderPage] is marked as active. It notifies the
|
||||||
|
* activity of the change and requests the preload of the next chapter if this is the last page.
|
||||||
|
*/
|
||||||
|
private fun onReaderPageSelected(page: ReaderPage) {
|
||||||
|
val pages = page.chapter.pages!! // Won't be null because it's the loaded chapter
|
||||||
|
Timber.d("onReaderPageSelected: ${page.number}/${pages.size}")
|
||||||
|
activity.onPageSelected(page)
|
||||||
|
|
||||||
|
if (page === pages.last()) {
|
||||||
|
Timber.d("Request preload next chapter because we're at the last page")
|
||||||
|
adapter.nextTransition?.to?.let {
|
||||||
|
activity.requestPreloadChapter(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a [ChapterTransition] is marked as active. It request the
|
||||||
* preload of the destination chapter of the transition.
|
* preload of the destination chapter of the transition.
|
||||||
*/
|
*/
|
||||||
private fun onTransitionSelected(transition: ChapterTransition) {
|
private fun onTransitionSelected(transition: ChapterTransition) {
|
||||||
@ -194,10 +200,15 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
|||||||
* Tells this viewer to move to the given [page].
|
* Tells this viewer to move to the given [page].
|
||||||
*/
|
*/
|
||||||
override fun moveToPage(page: ReaderPage) {
|
override fun moveToPage(page: ReaderPage) {
|
||||||
Timber.d("moveToPage")
|
Timber.d("moveToPage ${page.number}")
|
||||||
val position = adapter.items.indexOf(page)
|
val position = adapter.items.indexOf(page)
|
||||||
if (position != -1) {
|
if (position != -1) {
|
||||||
|
val currentPosition = pager.currentItem
|
||||||
pager.setCurrentItem(position, true)
|
pager.setCurrentItem(position, true)
|
||||||
|
// manually call onPageChange since ViewPager listener is not triggered in this case
|
||||||
|
if (currentPosition == position) {
|
||||||
|
onPageChange(position)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Page $page not found in adapter")
|
Timber.d("Page $page not found in adapter")
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user