mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 11:07:51 +02:00
Compare commits
136 Commits
Author | SHA1 | Date | |
---|---|---|---|
4a9151e4aa | |||
020cc89576 | |||
25898d34ca | |||
6394388714 | |||
d4101c7bdf | |||
c93bf89cbe | |||
88d1f29fe2 | |||
c437a33f2a | |||
e3259f39f1 | |||
cb357b0a16 | |||
a7faf445c4 | |||
82a08f24c0 | |||
afa89ac125 | |||
2060b5cd34 | |||
d69730a333 | |||
9714a30148 | |||
23c0f2c313 | |||
5c4139be45 | |||
6b1a3a20e5 | |||
4ae00c80ca | |||
827792c4f0 | |||
abbe700dac | |||
1347bfe243 | |||
a76ee95b6d | |||
f3689f09cd | |||
d545cfd38c | |||
3631a9fac2 | |||
aee4ad2d3f | |||
f88c86c799 | |||
60ac27e401 | |||
d0567de4e6 | |||
ca30fd6088 | |||
1470e9d5ca | |||
f45efe2aa8 | |||
5b6c475817 | |||
4abd2d709f | |||
d97aff85b3 | |||
deec65446f | |||
5c662b1ae1 | |||
f648940388 | |||
5aae17754f | |||
0ef0f6ece1 | |||
bfd46f28e0 | |||
eaece18afc | |||
67d39b037c | |||
dd3f5a146d | |||
9fdc5b4b9d | |||
1f32d13698 | |||
886b1019ed | |||
3e8ed8a171 | |||
8307daee63 | |||
9b40d10352 | |||
f2a06eab37 | |||
74fd70416f | |||
b85c164195 | |||
75c41b645a | |||
54c8b3ef29 | |||
56bde40035 | |||
ff4a015baa | |||
0db4fcc27e | |||
f3080b6277 | |||
69cbbd5811 | |||
0b85760939 | |||
03f3a4805f | |||
d95adf2631 | |||
e971d40e06 | |||
c65a01a5f0 | |||
8586014e17 | |||
bdfae4ba04 | |||
75cb94b51a | |||
2f6d163a7a | |||
ecfe72bcad | |||
e6ff9e18cc | |||
3c550c1781 | |||
537693f5cf | |||
5ae0589547 | |||
71fc6fc257 | |||
c0d7b16ee6 | |||
f3f7aa9e1d | |||
43355970db | |||
bfa386acba | |||
e8b432485d | |||
a12a34e3bb | |||
b79855c01d | |||
17fe501a6d | |||
8201b367ec | |||
6c242084ca | |||
aefe7b176a | |||
6059b85e58 | |||
aa46c52eee | |||
d3cbfbdb59 | |||
cc9b77b876 | |||
1568ac9e8a | |||
1129dacdfa | |||
fab7967018 | |||
bb40a4d6b8 | |||
90d27147e6 | |||
634247c590 | |||
fd8f7ea693 | |||
5eeb497f2b | |||
505e642691 | |||
74a7e2a17e | |||
5fec956ce6 | |||
1794782323 | |||
0210ee8747 | |||
ca412832ef | |||
1089c25b8f | |||
e85841784c | |||
ca2236958a | |||
c7686323b7 | |||
73d1a1a05e | |||
211f7b591b | |||
72ea256906 | |||
f521622d4d | |||
dc5283ce9a | |||
256a4197c9 | |||
a5a12f8b3a | |||
bbe180ecd1 | |||
67678cd49e | |||
097d4fe34c | |||
5914346ace | |||
062788f222 | |||
55be9b9ca5 | |||
0da2f91771 | |||
ff190e02d4 | |||
29fd5747eb | |||
fa8f5bc0d8 | |||
2118434823 | |||
2eeac0bf8b | |||
89b293fecd | |||
3f758d5981 | |||
e838bb43d2 | |||
b7b83305b2 | |||
8df3080e0d | |||
f88794c752 | |||
cc9e2cee1f |
30
.github/CONTRIBUTING.md
vendored
30
.github/CONTRIBUTING.md
vendored
@ -1,6 +1,17 @@
|
||||
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||
2. If you are unsure, ask here: [](https://discord.gg/WrBkRk4)
|
||||
3. What is your type of issue?
|
||||
* [Catalogue request](#catalogue-requests)
|
||||
* [Bugs](#bugs)
|
||||
* [Feature requests](#feature-requests)
|
||||
* [Translations](https://github.com/inorichi/tachiyomi/wiki/Translation)
|
||||
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
|
||||
|
||||
***
|
||||
|
||||
# Catalogue requests
|
||||
|
||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions/issues, not here
|
||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
||||
|
||||
# Bugs
|
||||
* Include version (Setting > About > Version)
|
||||
@ -8,17 +19,9 @@
|
||||
* Dev version is equal to the number of commits as seen in the main page
|
||||
* Include steps to reproduce (if not obvious from description)
|
||||
* Include screenshot (if needed)
|
||||
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
|
||||
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||
* For large logs use http://pastebin.com/ (or similar)
|
||||
* For multipart issues **use list** like this:
|
||||
* [x] Done
|
||||
* [ ] Not done
|
||||
```
|
||||
* [x] Done
|
||||
* [ ] Not done
|
||||
```
|
||||
* Don't put together too many unrelated requests into one issue
|
||||
* Don't group unrelated requests into one issue
|
||||
|
||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||
|
||||
@ -28,8 +31,3 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
||||
|
||||
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
|
||||
* Include screenshot (if needed)
|
||||
|
||||
# Translations
|
||||
|
||||
File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated.
|
||||
Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs)
|
||||
|
6
.github/ISSUE_TEMPLATE.md
vendored
6
.github/ISSUE_TEMPLATE.md
vendored
@ -1,7 +1 @@
|
||||
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
|
||||
|
||||
Remove line above and describe your issue here. Fill out version below. Use Preview.
|
||||
|
||||
|
||||
Version: r000 or v0.0.0
|
||||
(other relevant info like OS)
|
||||
|
80
.travis.yml
80
.travis.yml
@ -1,41 +1,57 @@
|
||||
language: android
|
||||
android:
|
||||
components:
|
||||
- platform-tools
|
||||
- tools
|
||||
|
||||
# The BuildTools version used by your project
|
||||
- build-tools-25.0.1
|
||||
- android-25
|
||||
- extra-android-m2repository
|
||||
- extra-google-m2repository
|
||||
- extra-android-support
|
||||
- extra-google-google_play_services
|
||||
|
||||
- build-tools-26.0.2
|
||||
- android-26
|
||||
- extra-android-m2repository
|
||||
- extra-google-m2repository
|
||||
- extra-android-support
|
||||
- extra-google-google_play_services
|
||||
licenses:
|
||||
- android-sdk-license-.+
|
||||
- '.+'
|
||||
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
|
||||
before_script:
|
||||
- chmod +x gradlew
|
||||
|
||||
- android-sdk-license-.+
|
||||
before_install:
|
||||
- mkdir "$ANDROID_HOME/licenses" || true
|
||||
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
|
||||
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
||||
|
||||
#Build, and run tests
|
||||
script: "./gradlew clean buildStandardDebug"
|
||||
sudo: false
|
||||
|
||||
- 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;
|
||||
tar xf secrets.tar;
|
||||
mv debug.keystore "$HOME/.android";
|
||||
fi
|
||||
- git clone https://github.com/urho3d/android-ndk.git $HOME/android-ndk-root
|
||||
- export ANDROID_NDK_HOME=$HOME/android-ndk-root
|
||||
- mkdir "$ANDROID_HOME/licenses" || true
|
||||
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
|
||||
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
||||
script: ".travis/build.sh"
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
- "$HOME/.gradle/caches/"
|
||||
- "$HOME/.gradle/wrapper/"
|
||||
- "$HOME/.android/build-cache"
|
||||
deploy:
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA=
|
||||
file: tachiyomi-v*.apk
|
||||
file_glob: true
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
repo: inorichi/tachiyomi
|
||||
- provider: script
|
||||
script: ".travis/deploy.sh"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
branch: master
|
||||
condition: "-z $TRAVIS_TAG"
|
||||
repo: inorichi/tachiyomi
|
||||
env:
|
||||
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"
|
||||
global:
|
||||
- secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU=
|
||||
- secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI=
|
||||
- secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E=
|
||||
- secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c=
|
||||
- secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI=
|
||||
- secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY=
|
||||
- secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04=
|
||||
|
20
.travis/build.sh
Executable file
20
.travis/build.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
git fetch --unshallow #required for commit count
|
||||
|
||||
if [ -z "$TRAVIS_TAG" ]; then
|
||||
./gradlew clean assembleStandardDebug
|
||||
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
export ARTIFACT="tachiyomi-r${COMMIT_COUNT}.apk"
|
||||
|
||||
mv app/build/outputs/apk/standard/debug/app-standard-debug.apk $ARTIFACT
|
||||
else
|
||||
./gradlew clean assembleStandardRelease
|
||||
|
||||
TOOLS="${ANDROID_HOME}/build-tools/26.0.1"
|
||||
export ARTIFACT="tachiyomi-${TRAVIS_TAG}.apk"
|
||||
|
||||
${TOOLS}/zipalign -v -p 4 app/build/outputs/apk/standard/release/app-standard-release-unsigned.apk app-aligned.apk
|
||||
${TOOLS}/apksigner sign --ks $STORE_PATH --ks-key-alias $STORE_ALIAS --ks-pass env:STORE_PASS --key-pass env:KEY_PASS --out $ARTIFACT app-aligned.apk
|
||||
fi
|
15
.travis/deploy.sh
Executable file
15
.travis/deploy.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
pattern="tachiyomi-r*"
|
||||
files=( $pattern )
|
||||
export ARTIFACT="${files[0]}"
|
||||
|
||||
if [ -z "$ARTIFACT" ]; then
|
||||
echo "Artifact not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export SSHOPTIONS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${DEPLOY_KEY}"
|
||||
|
||||
scp $SSHOPTIONS $ARTIFACT $DEPLOY_USER@$DEPLOY_HOST:builds/
|
||||
ssh $SSHOPTIONS $DEPLOY_USER@$DEPLOY_HOST ln -sf $ARTIFACT builds/latest
|
BIN
.travis/secrets.tar.enc
Normal file
BIN
.travis/secrets.tar.enc
Normal file
Binary file not shown.
11
README.md
11
README.md
@ -1,10 +1,11 @@
|
||||
| Build | Download | F-Droid |
|
||||
|-------|----------|-------------|
|
||||
| [](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) |
|
||||
| Build | Download | F-Droid | Contribute | Contact |
|
||||
|-------|----------|---------|------------|---------|
|
||||
| [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | [](https://github.com/inorichi/tachiyomi/wiki/Translation) | [](https://discord.gg/WrBkRk4) |
|
||||
|
||||
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
|
||||
### **Contact us on [Discord](https://discord.gg/WrBkRk4)**
|
||||
If you want to open an issue, please read [contributing guidelines](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md). Your issue may be closed otherwise.
|
||||
|
||||
**Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened issues.**
|
||||
***
|
||||
|
||||
Tachiyomi is a free and open source manga reader for Android.
|
||||
|
||||
|
215
app/build.gradle
215
app/build.gradle
@ -3,10 +3,10 @@ import java.text.SimpleDateFormat
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.github.zellius.shortcut-helper'
|
||||
|
||||
if (file("custom.gradle").exists()) {
|
||||
apply from: "custom.gradle"
|
||||
}
|
||||
shortcutHelper.filePath = './shortcuts.xml'
|
||||
|
||||
ext {
|
||||
// Git is needed in your system PATH for these commands to work.
|
||||
@ -29,17 +29,17 @@ ext {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
compileSdkVersion 26
|
||||
buildToolsVersion "26.0.2"
|
||||
publishNonDefault true
|
||||
|
||||
defaultConfig {
|
||||
applicationId "eu.kanade.tachiyomi"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 25
|
||||
targetSdkVersion 26
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
versionCode 22
|
||||
versionName "0.5.2"
|
||||
versionCode 27
|
||||
versionName "0.6.4"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
@ -67,15 +67,20 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "default"
|
||||
|
||||
productFlavors {
|
||||
standard {
|
||||
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
||||
dimension "default"
|
||||
}
|
||||
fdroid {
|
||||
dimension "default"
|
||||
}
|
||||
dev {
|
||||
minSdkVersion 21
|
||||
resConfigs "en", "xxhdpi"
|
||||
dimension "default"
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,127 +97,142 @@ android {
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// Modified dependencies
|
||||
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
|
||||
compile 'com.github.inorichi:junrar-android:634c1f5'
|
||||
implementation 'com.github.inorichi:subsampling-scale-image-view:c19b883'
|
||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||
|
||||
// Android support library
|
||||
final support_library_version = '25.3.1'
|
||||
compile "com.android.support:support-v4:$support_library_version"
|
||||
compile "com.android.support:appcompat-v7:$support_library_version"
|
||||
compile "com.android.support:cardview-v7:$support_library_version"
|
||||
compile "com.android.support:design:$support_library_version"
|
||||
compile "com.android.support:recyclerview-v7:$support_library_version"
|
||||
compile "com.android.support:support-annotations:$support_library_version"
|
||||
compile "com.android.support:customtabs:$support_library_version"
|
||||
final support_library_version = '26.1.0'
|
||||
implementation "com.android.support:support-v4:$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:design:$support_library_version"
|
||||
implementation "com.android.support:recyclerview-v7:$support_library_version"
|
||||
implementation "com.android.support:preference-v7:$support_library_version"
|
||||
implementation "com.android.support:support-annotations:$support_library_version"
|
||||
implementation "com.android.support:customtabs:$support_library_version"
|
||||
|
||||
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||
|
||||
compile 'com.android.support:multidex:1.0.1'
|
||||
implementation 'com.android.support:multidex:1.0.1'
|
||||
|
||||
// ReactiveX
|
||||
compile 'io.reactivex:rxandroid:1.2.1'
|
||||
compile 'io.reactivex:rxjava:1.2.9'
|
||||
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
implementation 'io.reactivex:rxjava:1.3.3'
|
||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||
implementation 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||
|
||||
// Network client
|
||||
compile "com.squareup.okhttp3:okhttp:3.6.0"
|
||||
compile 'com.squareup.okio:okio:1.11.0'
|
||||
implementation "com.squareup.okhttp3:okhttp:3.9.0"
|
||||
implementation 'com.squareup.okio:okio:1.13.0'
|
||||
|
||||
// REST
|
||||
final retrofit_version = '2.2.0'
|
||||
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||
final retrofit_version = '2.3.0'
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||
|
||||
// JSON
|
||||
compile 'com.google.code.gson:gson:2.8.0'
|
||||
compile 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||
implementation 'com.google.code.gson:gson:2.8.2'
|
||||
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||
|
||||
// YAML
|
||||
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
|
||||
implementation 'com.github.bmoliveira:snake-yaml:v1.18-android'
|
||||
|
||||
// JavaScript engine
|
||||
compile 'com.squareup.duktape:duktape-android:1.1.0'
|
||||
implementation 'com.squareup.duktape:duktape-android:1.2.0'
|
||||
|
||||
// Disk
|
||||
compile 'com.jakewharton:disklrucache:2.0.2'
|
||||
compile 'com.github.seven332:unifile:1.0.0'
|
||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||
implementation 'com.github.seven332:unifile:1.0.0'
|
||||
|
||||
// HTML parser
|
||||
compile 'org.jsoup:jsoup:1.10.2'
|
||||
implementation 'org.jsoup:jsoup:1.10.2'
|
||||
|
||||
// Job scheduling
|
||||
compile 'com.evernote:android-job:1.1.8'
|
||||
compile 'com.google.android.gms:play-services-gcm:10.2.0'
|
||||
implementation 'com.evernote:android-job:1.2.0'
|
||||
implementation 'com.google.android.gms:play-services-gcm:11.6.0'
|
||||
|
||||
// Changelog
|
||||
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||
|
||||
// Database
|
||||
compile "com.pushtorefresh.storio:sqlite:1.12.3"
|
||||
implementation "com.pushtorefresh.storio:sqlite:1.13.0"
|
||||
|
||||
// Model View Presenter
|
||||
final nucleus_version = '3.0.0'
|
||||
compile "info.android15.nucleus:nucleus:$nucleus_version"
|
||||
compile "info.android15.nucleus:nucleus-support-v4:$nucleus_version"
|
||||
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||
implementation "info.android15.nucleus:nucleus:$nucleus_version"
|
||||
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||
|
||||
// Dependency injection
|
||||
compile "uy.kohesive.injekt:injekt-core:1.16.1"
|
||||
implementation "uy.kohesive.injekt:injekt-core:1.16.1"
|
||||
|
||||
// Image library
|
||||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
||||
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
|
||||
final glide_version = '4.3.1'
|
||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||
|
||||
// Transformations
|
||||
compile 'jp.wasabeef:glide-transformations:2.0.2'
|
||||
implementation 'jp.wasabeef:glide-transformations:3.0.1'
|
||||
|
||||
// Logging
|
||||
compile 'com.jakewharton.timber:timber:4.5.1'
|
||||
implementation 'com.jakewharton.timber:timber:4.6.0'
|
||||
|
||||
// Crash reports
|
||||
compile 'ch.acra:acra:4.9.2'
|
||||
implementation 'ch.acra:acra:4.9.2'
|
||||
|
||||
// Sort
|
||||
compile 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
|
||||
// UI
|
||||
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||
compile 'eu.davidea:flexible-adapter:5.0.0-rc1'
|
||||
compile 'com.github.inorichi:FlexibleAdapter:93985fe' // v4.2.0 to be removed
|
||||
compile 'com.nononsenseapps:filepicker:2.5.2'
|
||||
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
||||
compile 'com.afollestad.material-dialogs:core:0.9.4.2'
|
||||
compile 'net.xpece.android:support-preference:1.2.5'
|
||||
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||
compile 'de.hdodenhof:circleimageview:2.1.0'
|
||||
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||
implementation 'eu.davidea:flexible-adapter:5.0.0-rc3'
|
||||
implementation 'com.nononsenseapps:filepicker:2.5.2'
|
||||
implementation 'com.github.amulyakhare:TextDrawable:558677e'
|
||||
implementation('com.afollestad.material-dialogs:core:0.9.4.7') {
|
||||
exclude group: "com.android.support", module: "support-v13"
|
||||
}
|
||||
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
||||
implementation 'com.github.mthli:Slice:v1.2'
|
||||
|
||||
// Conductor
|
||||
implementation "com.bluelinelabs:conductor:2.1.4"
|
||||
implementation 'com.github.inorichi:conductor-support-preference:26.0.2'
|
||||
|
||||
// RxBindings
|
||||
final rxbindings_version = '1.0.1'
|
||||
implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
|
||||
implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
|
||||
implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
|
||||
implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
|
||||
|
||||
// Tests
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.assertj:assertj-core:1.7.1'
|
||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:1.7.1'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
|
||||
final robolectric_version = '3.1.4'
|
||||
testCompile "org.robolectric:robolectric:$robolectric_version"
|
||||
testCompile "org.robolectric:shadows-multidex:$robolectric_version"
|
||||
testCompile "org.robolectric:shadows-play-services:$robolectric_version"
|
||||
testImplementation "org.robolectric:robolectric:$robolectric_version"
|
||||
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
||||
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
final coroutines_version = '0.19.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.1.1'
|
||||
ext.kotlin_version = '1.1.51'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
@ -225,49 +245,8 @@ repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
// Workaround to force a support lib version
|
||||
configurations.all {
|
||||
resolutionStrategy.eachDependency { details ->
|
||||
def requested = details.requested
|
||||
if (requested.group == 'com.android.support') {
|
||||
if (!requested.name.startsWith("multidex")) {
|
||||
details.useVersion '25.3.1'
|
||||
}
|
||||
}
|
||||
kotlin {
|
||||
experimental {
|
||||
coroutines 'enable'
|
||||
}
|
||||
}
|
||||
|
||||
// add support for placeholders in resource files
|
||||
//https://code.google.com/p/android/issues/detail?id=69224
|
||||
def replacePlaceholdersInFile(basePath, fileName, placeholders) {
|
||||
def file = new File(basePath, fileName);
|
||||
|
||||
if (!file.exists()) {
|
||||
logger.quiet("Unable to replace placeholders in " + file.toString() + ". File cannot be found.")
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Replacing placeholders in " + file.toString())
|
||||
logger.debug("Placeholders: " + placeholders.toString())
|
||||
|
||||
def content = file.getText('UTF-8')
|
||||
|
||||
placeholders.each { entry ->
|
||||
content = content.replaceAll("\\\$\\{${entry.key}\\}", entry.value)
|
||||
}
|
||||
|
||||
file.write(content, 'UTF-8')
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
output.processResources.doFirst {
|
||||
// prepare placeholder map from manifestPlaceholders including applicationId placeholder
|
||||
def placeholders = variant.mergedFlavor.manifestPlaceholders + [applicationId: variant.applicationId]
|
||||
|
||||
replacePlaceholdersInFile(resDir, 'xml-v25/shortcuts.xml', placeholders)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
app/proguard-rules.pro
vendored
54
app/proguard-rules.pro
vendored
@ -1,27 +1,30 @@
|
||||
-dontobfuscate
|
||||
|
||||
-dontwarn eu.kanade.tachiyomi.**
|
||||
-keep class eu.kanade.tachiyomi.**
|
||||
-keep class eu.kanade.tachiyomi.source.model.** { *; }
|
||||
|
||||
-keep class com.hippo.image.** { *; }
|
||||
-keep interface com.hippo.image.** { *; }
|
||||
-dontwarn nucleus.view.NucleusActionBarActivity
|
||||
|
||||
# Extensions may require methods unused in the core app
|
||||
-keep class org.jsoup.** { *; }
|
||||
-keep class kotlin.** { *; }
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep class com.github.salomonbrys.kotson.** { *; }
|
||||
|
||||
# OkHttp
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation*
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep interface okhttp3.** { *; }
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
|
||||
# Okio
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
-dontwarn java.nio.file.*
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
-dontwarn okio.**
|
||||
-dontwarn javax.annotation.**
|
||||
-dontwarn retrofit2.Platform$Java8
|
||||
|
||||
# Glide specific rules #
|
||||
# https://github.com/bumptech/glide
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
@ -43,27 +46,26 @@
|
||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||
}
|
||||
|
||||
# Retrofit 2.X
|
||||
## https://square.github.io/retrofit/ ##
|
||||
### Support v7, Design
|
||||
# http://stackoverflow.com/questions/29679177/cardview-shadow-not-appearing-in-lollipop-after-obfuscate-with-proguard/29698051
|
||||
-keep class android.support.v7.widget.RoundRectDrawable { *; }
|
||||
|
||||
-dontwarn retrofit2.**
|
||||
-keep class retrofit2.** { *; }
|
||||
-keepattributes Signature
|
||||
-keepattributes Exceptions
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@retrofit2.http.* <methods>;
|
||||
}
|
||||
|
||||
# AppCombat
|
||||
-keep public class android.support.v7.widget.** { *; }
|
||||
-keep public class android.support.v7.internal.widget.** { *; }
|
||||
-keep public class android.support.v7.internal.view.menu.** { *; }
|
||||
-keep public class android.support.v7.graphics.drawable.** { *; }
|
||||
|
||||
-keep public class * extends android.support.v4.view.ActionProvider {
|
||||
public <init>(android.content.Context);
|
||||
}
|
||||
|
||||
-dontwarn android.support.**
|
||||
-dontwarn android.support.design.**
|
||||
-keep class android.support.design.** { *; }
|
||||
-keep interface android.support.design.** { *; }
|
||||
-keep public class android.support.design.R$* { *; }
|
||||
|
||||
|
||||
# ReactiveNetwork
|
||||
-dontwarn com.github.pwittchen.reactivenetwork.**
|
||||
|
||||
@ -73,15 +75,8 @@
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Gson specific classes
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
#-keep class com.google.gson.stream.** { *; }
|
||||
|
||||
# Application classes that will be serialized/deserialized over Gson
|
||||
-keep class com.google.gson.examples.android.model.** { *; }
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
@ -91,7 +86,6 @@
|
||||
|
||||
# SnakeYaml
|
||||
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
||||
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
||||
-dontwarn org.yaml.snakeyaml.**
|
||||
|
||||
# Duktape
|
||||
|
@ -9,8 +9,7 @@
|
||||
android:shortcutShortLabel="@string/label_library">
|
||||
<intent
|
||||
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
|
||||
android:targetPackage="${applicationId}" />
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
@ -21,8 +20,7 @@
|
||||
android:shortcutShortLabel="@string/short_recent_updates">
|
||||
<intent
|
||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
|
||||
android:targetPackage="${applicationId}" />
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
@ -33,8 +31,7 @@
|
||||
android:shortcutShortLabel="@string/label_recent_manga">
|
||||
<intent
|
||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
|
||||
android:targetPackage="${applicationId}" />
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
@ -45,7 +42,6 @@
|
||||
android:shortcutShortLabel="@string/label_catalogues">
|
||||
<intent
|
||||
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
|
||||
android:targetPackage="${applicationId}" />
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="eu.kanade.tachiyomi">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@ -9,9 +8,6 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_PHONE_STATE"
|
||||
tools:node="remove" />
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||
|
||||
<application
|
||||
@ -23,30 +19,20 @@
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:theme="@style/Theme.Tachiyomi">
|
||||
<activity android:name=".ui.main.MainActivity">
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts"/>
|
||||
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.manga.MangaActivity"
|
||||
android:exported="true"
|
||||
android:parentActivityName=".ui.main.MainActivity" />
|
||||
<activity
|
||||
android:name=".ui.reader.ReaderActivity"
|
||||
android:theme="@style/Theme.Reader" />
|
||||
<activity
|
||||
android:name=".ui.setting.SettingsActivity"
|
||||
android:label="@string/label_settings"
|
||||
android:parentActivityName=".ui.main.MainActivity" />
|
||||
<activity
|
||||
android:name=".ui.category.CategoryActivity"
|
||||
android:label="@string/label_categories"
|
||||
android:parentActivityName=".ui.main.MainActivity" />
|
||||
<activity
|
||||
android:name=".widget.CustomLayoutPickerActivity"
|
||||
android:label="@string/app_name"
|
||||
@ -65,9 +51,6 @@
|
||||
android:scheme="tachiyomi" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.download.DownloadActivity"
|
||||
android:launchMode="singleTop" />
|
||||
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
@ -113,10 +96,6 @@
|
||||
android:name=".data.backup.BackupRestoreService"
|
||||
android:exported="false"/>
|
||||
|
||||
<meta-data
|
||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||
android:value="GlideModule" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -7,6 +7,7 @@ import android.support.multidex.MultiDex
|
||||
import com.evernote.android.job.JobManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
import org.acra.ACRA
|
||||
@ -34,6 +35,7 @@ open class App : Application() {
|
||||
|
||||
setupAcra()
|
||||
setupJobManager()
|
||||
setupNotificationChannels()
|
||||
|
||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||
}
|
||||
@ -65,4 +67,8 @@ open class App : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun setupNotificationChannels() {
|
||||
Notifications.createChannels(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
object Constants {
|
||||
const val NOTIFICATION_LIBRARY_PROGRESS_ID = 1
|
||||
const val NOTIFICATION_LIBRARY_RESULT_ID = 2
|
||||
const val NOTIFICATION_UPDATER_ID = 3
|
||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 4
|
||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 5
|
||||
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 6
|
||||
}
|
63
app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
Normal file
63
app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
Normal file
@ -0,0 +1,63 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
||||
import java.io.File
|
||||
|
||||
object Migrations {
|
||||
|
||||
/**
|
||||
* Performs a migration when the application is updated.
|
||||
*
|
||||
* @param preferences Preferences of the application.
|
||||
* @return true if a migration is performed, false otherwise.
|
||||
*/
|
||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||
val context = preferences.context
|
||||
val oldVersion = preferences.lastVersionCode().getOrDefault()
|
||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||
|
||||
if (oldVersion == 0) return false
|
||||
|
||||
if (oldVersion < 14) {
|
||||
// Restore jobs after upgrading to evernote's job scheduler.
|
||||
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
||||
UpdateCheckerJob.setupTask()
|
||||
}
|
||||
LibraryUpdateJob.setupTask()
|
||||
}
|
||||
if (oldVersion < 15) {
|
||||
// Delete internal chapter cache dir.
|
||||
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
|
||||
}
|
||||
if (oldVersion < 19) {
|
||||
// Move covers to external files dir.
|
||||
val oldDir = File(context.externalCacheDir, "cover_disk_cache")
|
||||
if (oldDir.exists()) {
|
||||
val destDir = context.getExternalFilesDir("covers")
|
||||
if (destDir != null) {
|
||||
oldDir.listFiles().forEach {
|
||||
it.renameTo(File(destDir, it.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 26) {
|
||||
// Delete external chapter cache dir.
|
||||
val extCache = context.externalCacheDir
|
||||
if (extCache != null) {
|
||||
val chapterCache = File(extCache, "chapter_disk_cache")
|
||||
if (chapterCache.exists()) {
|
||||
chapterCache.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
|
||||
|
||||
object BackupConst {
|
||||
|
||||
const val INTENT_FILTER = "SettingsBackupFragment"
|
||||
const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG"
|
||||
const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG"
|
||||
const val ACTION_ERROR_BACKUP_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_BACKUP_DIALOG"
|
||||
const val ACTION_ERROR_RESTORE_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_RESTORE_DIALOG"
|
||||
const val ACTION_RESTORE_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_RESTORE_COMPLETED_DIALOG"
|
||||
const val ACTION = "$ID.$INTENT_FILTER.ACTION"
|
||||
const val EXTRA_PROGRESS = "$ID.$INTENT_FILTER.EXTRA_PROGRESS"
|
||||
const val EXTRA_AMOUNT = "$ID.$INTENT_FILTER.EXTRA_AMOUNT"
|
||||
const val EXTRA_ERRORS = "$ID.$INTENT_FILTER.EXTRA_ERRORS"
|
||||
const val EXTRA_CONTENT = "$ID.$INTENT_FILTER.EXTRA_CONTENT"
|
||||
const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE"
|
||||
const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI"
|
||||
const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME"
|
||||
const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH"
|
||||
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
|
||||
}
|
@ -13,8 +13,6 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsBackupFragment
|
||||
import eu.kanade.tachiyomi.util.AndroidComponentUtil
|
||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||
import timber.log.Timber
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
@ -28,8 +26,6 @@ class BackupCreateService : IntentService(NAME) {
|
||||
// Name of class
|
||||
private const val NAME = "BackupCreateService"
|
||||
|
||||
// Uri as string
|
||||
private const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||
// Backup called from job
|
||||
private const val EXTRA_IS_JOB = "$ID.$NAME.EXTRA_IS_JOB"
|
||||
// Options for backup
|
||||
@ -54,18 +50,15 @@ class BackupCreateService : IntentService(NAME) {
|
||||
* @param flags determines what to backup
|
||||
* @param isJob backup called from job
|
||||
*/
|
||||
fun makeBackup(context: Context, path: String, flags: Int, isJob: Boolean = false) {
|
||||
fun makeBackup(context: Context, uri: Uri, flags: Int, isJob: Boolean = false) {
|
||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||
putExtra(EXTRA_URI, path)
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
putExtra(EXTRA_IS_JOB, isJob)
|
||||
putExtra(EXTRA_FLAGS, flags)
|
||||
}
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
fun isRunning(context: Context): Boolean {
|
||||
return AndroidComponentUtil.isServiceRunning(context, BackupCreateService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private val backupManager by lazy { BackupManager(this) }
|
||||
@ -74,11 +67,11 @@ class BackupCreateService : IntentService(NAME) {
|
||||
if (intent == null) return
|
||||
|
||||
// Get values
|
||||
val uri = intent.getStringExtra(EXTRA_URI)
|
||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||
val isJob = intent.getBooleanExtra(EXTRA_IS_JOB, false)
|
||||
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
|
||||
// Create backup
|
||||
createBackupFromApp(Uri.parse(uri), flags, isJob)
|
||||
createBackupFromApp(uri, flags, isJob)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,9 +84,6 @@ class BackupCreateService : IntentService(NAME) {
|
||||
// Create root object
|
||||
val root = JsonObject()
|
||||
|
||||
// Create information object
|
||||
val information = JsonObject()
|
||||
|
||||
// Create manga array
|
||||
val mangaEntries = JsonArray()
|
||||
|
||||
@ -150,9 +140,9 @@ class BackupCreateService : IntentService(NAME) {
|
||||
}
|
||||
|
||||
// Show completed dialog
|
||||
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_BACKUP_COMPLETED_DIALOG)
|
||||
putExtra(SettingsBackupFragment.EXTRA_URI, file.uri.toString())
|
||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_BACKUP_COMPLETED_DIALOG)
|
||||
putExtra(BackupConst.EXTRA_URI, file.uri.toString())
|
||||
}
|
||||
sendLocalBroadcast(intent)
|
||||
}
|
||||
@ -160,9 +150,9 @@ class BackupCreateService : IntentService(NAME) {
|
||||
Timber.e(e)
|
||||
if (!isJob) {
|
||||
// Show error dialog
|
||||
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_ERROR_BACKUP_DIALOG)
|
||||
putExtra(SettingsBackupFragment.EXTRA_ERROR_MESSAGE, e.message)
|
||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_BACKUP_DIALOG)
|
||||
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, e.message)
|
||||
}
|
||||
sendLocalBroadcast(intent)
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.net.Uri
|
||||
import com.evernote.android.job.Job
|
||||
import com.evernote.android.job.JobManager
|
||||
import com.evernote.android.job.JobRequest
|
||||
@ -7,14 +8,15 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class BackupCreatorJob : Job() {
|
||||
|
||||
override fun onRunJob(params: Params): Result {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val path = preferences.backupsDirectory().getOrDefault()
|
||||
val uri = Uri.fromFile(File(preferences.backupsDirectory().getOrDefault()))
|
||||
val flags = BackupCreateService.BACKUP_ALL
|
||||
BackupCreateService.makeBackup(context,path,flags,true)
|
||||
BackupCreateService.makeBackup(context, uri, flags, true)
|
||||
return Result.SUCCESS
|
||||
}
|
||||
|
||||
@ -27,7 +29,6 @@ class BackupCreatorJob : Job() {
|
||||
if (interval > 0) {
|
||||
JobRequest.Builder(TAG)
|
||||
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
|
||||
.setPersisted(true)
|
||||
.setUpdateCurrent(true)
|
||||
.build()
|
||||
.schedule()
|
||||
|
@ -28,7 +28,6 @@ import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.*
|
||||
|
||||
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
|
||||
|
@ -10,7 +10,6 @@ import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||
@ -23,9 +22,8 @@ import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.*
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsBackupFragment
|
||||
import eu.kanade.tachiyomi.util.AndroidComponentUtil
|
||||
import eu.kanade.tachiyomi.util.chop
|
||||
import eu.kanade.tachiyomi.util.isServiceRunning
|
||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
@ -37,7 +35,6 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
|
||||
/**
|
||||
* Restores backup from json file
|
||||
@ -45,11 +42,6 @@ import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
class BackupRestoreService : Service() {
|
||||
|
||||
companion object {
|
||||
// Name of service
|
||||
private const val NAME = "BackupRestoreService"
|
||||
|
||||
// Uri as string
|
||||
private const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||
|
||||
/**
|
||||
* Returns the status of the service.
|
||||
@ -58,7 +50,7 @@ class BackupRestoreService : Service() {
|
||||
* @return true if the service is running, false otherwise.
|
||||
*/
|
||||
fun isRunning(context: Context): Boolean {
|
||||
return AndroidComponentUtil.isServiceRunning(context, BackupRestoreService::class.java)
|
||||
return context.isServiceRunning(BackupRestoreService::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,10 +59,10 @@ class BackupRestoreService : Service() {
|
||||
* @param context context of application
|
||||
* @param uri path of Uri
|
||||
*/
|
||||
fun start(context: Context, uri: String) {
|
||||
fun start(context: Context, uri: Uri) {
|
||||
if (!isRunning(context)) {
|
||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||
putExtra(EXTRA_URI, uri)
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
}
|
||||
context.startService(intent)
|
||||
}
|
||||
@ -165,14 +157,14 @@ class BackupRestoreService : Service() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent == null) return Service.START_NOT_STICKY
|
||||
|
||||
val file = UniFile.fromUri(this, Uri.parse(intent.getStringExtra(EXTRA_URI)))
|
||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||
|
||||
// Unsubscribe from any previous subscription if needed.
|
||||
subscription?.unsubscribe()
|
||||
|
||||
subscription = Observable.using(
|
||||
{ db.lowLevel().beginTransaction() },
|
||||
{ getRestoreObservable(file).doOnNext{ db.lowLevel().setTransactionSuccessful() } },
|
||||
{ getRestoreObservable(uri).doOnNext{ db.lowLevel().setTransactionSuccessful() } },
|
||||
{ executor.execute { db.lowLevel().endTransaction() } })
|
||||
.doAfterTerminate { stopSelf(startId) }
|
||||
.subscribeOn(Schedulers.from(executor))
|
||||
@ -184,35 +176,39 @@ class BackupRestoreService : Service() {
|
||||
/**
|
||||
* Returns an [Observable] containing restore process.
|
||||
*
|
||||
* @param file restore file
|
||||
* @param uri restore file
|
||||
* @return [Observable<Manga>]
|
||||
*/
|
||||
private fun getRestoreObservable(file: UniFile): Observable<List<Manga>> {
|
||||
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
val reader = JsonReader(file.openInputStream().bufferedReader())
|
||||
val json = JsonParser().parse(reader).asJsonObject
|
||||
return Observable.just(Unit)
|
||||
.map {
|
||||
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
|
||||
val json = JsonParser().parse(reader).asJsonObject
|
||||
|
||||
// Get parser version
|
||||
val version = json.get(VERSION)?.asInt ?: 1
|
||||
// Get parser version
|
||||
val version = json.get(VERSION)?.asInt ?: 1
|
||||
|
||||
// Initialize manager
|
||||
backupManager = BackupManager(this, version)
|
||||
// Initialize manager
|
||||
backupManager = BackupManager(this, version)
|
||||
|
||||
val mangasJson = json.get(MANGAS).asJsonArray
|
||||
val mangasJson = json.get(MANGAS).asJsonArray
|
||||
|
||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||
restoreProgress = 0
|
||||
errors.clear()
|
||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||
restoreProgress = 0
|
||||
errors.clear()
|
||||
|
||||
// Restore categories
|
||||
json.get(CATEGORIES)?.let {
|
||||
backupManager.restoreCategories(it.asJsonArray)
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
||||
}
|
||||
// Restore categories
|
||||
json.get(CATEGORIES)?.let {
|
||||
backupManager.restoreCategories(it.asJsonArray)
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
||||
}
|
||||
|
||||
return Observable.from(mangasJson)
|
||||
mangasJson
|
||||
}
|
||||
.flatMap { Observable.from(it) }
|
||||
.concatMap {
|
||||
val obj = it.asJsonObject
|
||||
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
||||
@ -237,12 +233,12 @@ class BackupRestoreService : Service() {
|
||||
val endTime = System.currentTimeMillis()
|
||||
val time = endTime - startTime
|
||||
val logFile = writeErrorLog()
|
||||
val completeIntent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||
putExtra(SettingsBackupFragment.EXTRA_TIME, time)
|
||||
putExtra(SettingsBackupFragment.EXTRA_ERRORS, errors.size)
|
||||
putExtra(SettingsBackupFragment.EXTRA_ERROR_FILE_PATH, logFile.parent)
|
||||
putExtra(SettingsBackupFragment.EXTRA_ERROR_FILE, logFile.name)
|
||||
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_RESTORE_COMPLETED_DIALOG)
|
||||
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.EXTRA_TIME, time)
|
||||
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
|
||||
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
|
||||
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG)
|
||||
}
|
||||
sendLocalBroadcast(completeIntent)
|
||||
|
||||
@ -250,9 +246,9 @@ class BackupRestoreService : Service() {
|
||||
.doOnError { error ->
|
||||
Timber.e(error)
|
||||
writeErrorLog()
|
||||
val errorIntent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_ERROR_RESTORE_DIALOG)
|
||||
putExtra(SettingsBackupFragment.EXTRA_ERROR_MESSAGE, error.message)
|
||||
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG)
|
||||
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
|
||||
}
|
||||
sendLocalBroadcast(errorIntent)
|
||||
}
|
||||
@ -325,8 +321,8 @@ class BackupRestoreService : Service() {
|
||||
manga
|
||||
}
|
||||
.filter { it.id != null }
|
||||
.flatMap { manga ->
|
||||
chapterFetchObservable(source, manga, chapters)
|
||||
.flatMap {
|
||||
chapterFetchObservable(source, it, chapters)
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map { manga }
|
||||
}
|
||||
@ -393,7 +389,7 @@ class BackupRestoreService : Service() {
|
||||
|
||||
|
||||
/**
|
||||
* Called to update dialog in [SettingsBackupFragment]
|
||||
* Called to update dialog in [BackupConst]
|
||||
*
|
||||
* @param progress restore progress
|
||||
* @param amount total restoreAmount of manga
|
||||
@ -401,12 +397,12 @@ class BackupRestoreService : Service() {
|
||||
*/
|
||||
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
|
||||
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
|
||||
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||
putExtra(SettingsBackupFragment.EXTRA_PROGRESS, progress)
|
||||
putExtra(SettingsBackupFragment.EXTRA_AMOUNT, amount)
|
||||
putExtra(SettingsBackupFragment.EXTRA_CONTENT, content)
|
||||
putExtra(SettingsBackupFragment.EXTRA_ERRORS, errors)
|
||||
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_SET_PROGRESS_DIALOG)
|
||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.EXTRA_PROGRESS, progress)
|
||||
putExtra(BackupConst.EXTRA_AMOUNT, amount)
|
||||
putExtra(BackupConst.EXTRA_CONTENT, content)
|
||||
putExtra(BackupConst.EXTRA_ERRORS, errors)
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
|
||||
}
|
||||
sendLocalBroadcast(intent)
|
||||
}
|
||||
|
@ -45,8 +45,7 @@ class ChapterCache(private val context: Context) {
|
||||
private val gson: Gson by injectLazy()
|
||||
|
||||
/** Cache class used for cache management. */
|
||||
private val diskCache = DiskLruCache.open(
|
||||
File(context.externalCacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||
private val diskCache = DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||
PARAMETER_APP_VERSION,
|
||||
PARAMETER_VALUE_COUNT,
|
||||
PARAMETER_CACHE_SIZE)
|
||||
@ -82,10 +81,10 @@ class ChapterCache(private val context: Context) {
|
||||
|
||||
try {
|
||||
// Remove the extension from the file to get the key of the cache
|
||||
val key = file.substring(0, file.lastIndexOf("."))
|
||||
val key = file.substringBeforeLast(".")
|
||||
// Remove file from cache.
|
||||
return diskCache.remove(key)
|
||||
} catch (e: IOException) {
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -187,12 +186,12 @@ class ChapterCache(private val context: Context) {
|
||||
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
||||
|
||||
// Get OutputStream and write image with Okio.
|
||||
response.body().source().saveTo(editor.newOutputStream(0))
|
||||
response.body()!!.source().saveTo(editor.newOutputStream(0))
|
||||
|
||||
diskCache.flush()
|
||||
editor.commit()
|
||||
} finally {
|
||||
response.body().close()
|
||||
response.body()?.close()
|
||||
editor?.abortUnlessCommitted()
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ class CoverCache(private val context: Context) {
|
||||
/**
|
||||
* Cache directory used for cache management.
|
||||
*/
|
||||
private val cacheDir = context.getExternalFilesDir("covers")
|
||||
private val cacheDir = context.getExternalFilesDir("covers") ?:
|
||||
File(context.filesDir, "covers").also { it.mkdirs() }
|
||||
|
||||
/**
|
||||
* Returns the cover from cache.
|
||||
|
@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
|
||||
/**
|
||||
* Version of the database.
|
||||
*/
|
||||
const val DATABASE_VERSION = 4
|
||||
const val DATABASE_VERSION = 5
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||
@ -51,6 +51,9 @@ class DbOpenHelper(context: Context)
|
||||
if (oldVersion < 4) {
|
||||
db.execSQL(ChapterTable.bookmarkUpdateQuery)
|
||||
}
|
||||
if (oldVersion < 5) {
|
||||
db.execSQL(ChapterTable.addScanlator)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigure(db: SQLiteDatabase) {
|
||||
|
@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
||||
@ -48,6 +49,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
||||
put(COL_URL, obj.url)
|
||||
put(COL_NAME, obj.name)
|
||||
put(COL_READ, obj.read)
|
||||
put(COL_SCANLATOR, obj.scanlator)
|
||||
put(COL_BOOKMARK, obj.bookmark)
|
||||
put(COL_DATE_FETCH, obj.date_fetch)
|
||||
put(COL_DATE_UPLOAD, obj.date_upload)
|
||||
@ -64,6 +66,7 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
|
||||
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
||||
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
||||
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
||||
|
@ -65,9 +65,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
}
|
||||
}
|
||||
|
||||
open class MangaGetResolver : DefaultGetResolver<Manga>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
|
||||
interface BaseMangaGetResolver {
|
||||
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||
@ -86,6 +85,13 @@ open class MangaGetResolver : DefaultGetResolver<Manga>() {
|
||||
}
|
||||
}
|
||||
|
||||
open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Manga {
|
||||
return mapBaseFromCursor(MangaImpl(), cursor)
|
||||
}
|
||||
}
|
||||
|
||||
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
|
||||
|
@ -10,6 +10,8 @@ class ChapterImpl : Chapter {
|
||||
|
||||
override lateinit var name: String
|
||||
|
||||
override var scanlator: String? = null
|
||||
|
||||
override var read: Boolean = false
|
||||
|
||||
override var bookmark: Boolean = false
|
||||
@ -29,9 +31,7 @@ class ChapterImpl : Chapter {
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val chapter = other as Chapter
|
||||
|
||||
return url == chapter.url
|
||||
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
@ -0,0 +1,9 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class LibraryManga : MangaImpl() {
|
||||
|
||||
var unread: Int = 0
|
||||
|
||||
var category: Int = 0
|
||||
|
||||
}
|
@ -16,10 +16,6 @@ interface Manga : SManga {
|
||||
|
||||
var chapter_flags: Int
|
||||
|
||||
var unread: Int
|
||||
|
||||
var category: Int
|
||||
|
||||
fun setChapterOrder(order: Int) {
|
||||
setFlags(order, SORT_MASK)
|
||||
}
|
||||
|
@ -7,4 +7,4 @@ package eu.kanade.tachiyomi.data.database.models
|
||||
* @param chapter object containing chater
|
||||
* @param history object containing history
|
||||
*/
|
||||
class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
||||
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
||||
|
@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class MangaImpl : Manga {
|
||||
open class MangaImpl : Manga {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
@ -32,10 +32,6 @@ class MangaImpl : Manga {
|
||||
|
||||
override var chapter_flags: Int = 0
|
||||
|
||||
@Transient override var unread: Int = 0
|
||||
|
||||
@Transient override var category: Int = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
@ -4,10 +4,12 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
@ -22,10 +24,10 @@ interface MangaQueries : DbProvider {
|
||||
.prepare()
|
||||
|
||||
fun getLibraryMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.listOfObjects(LibraryManga::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(libraryQuery)
|
||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
|
||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
||||
.build())
|
||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
@ -97,4 +99,7 @@ interface MangaQueries : DbProvider {
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
|
||||
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare();
|
||||
}
|
@ -93,6 +93,15 @@ fun getLastReadMangaQuery() = """
|
||||
ORDER BY max DESC
|
||||
"""
|
||||
|
||||
fun getTotalChapterMangaQuery()= """
|
||||
SELECT ${Manga.TABLE}.*
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER by COUNT(*)
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the categories for a manga.
|
||||
*/
|
||||
|
@ -1,24 +1,23 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.database.Cursor
|
||||
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.mappers.BaseMangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
class LibraryMangaGetResolver : MangaGetResolver() {
|
||||
class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGetResolver {
|
||||
|
||||
companion object {
|
||||
val INSTANCE = LibraryMangaGetResolver()
|
||||
}
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Manga {
|
||||
val manga = super.mapFromCursor(cursor)
|
||||
override fun mapFromCursor(cursor: Cursor): LibraryManga {
|
||||
val manga = LibraryManga()
|
||||
|
||||
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD)
|
||||
manga.unread = cursor.getInt(unreadColumn)
|
||||
|
||||
val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY)
|
||||
manga.category = cursor.getInt(categoryColumn)
|
||||
mapBaseFromCursor(manga, cursor)
|
||||
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
||||
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
|
||||
|
||||
return manga
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ object ChapterTable {
|
||||
|
||||
const val COL_READ = "read"
|
||||
|
||||
const val COL_SCANLATOR = "scanlator"
|
||||
|
||||
const val COL_BOOKMARK = "bookmark"
|
||||
|
||||
const val COL_DATE_FETCH = "date_fetch"
|
||||
@ -32,6 +34,7 @@ object ChapterTable {
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_URL TEXT NOT NULL,
|
||||
$COL_NAME TEXT NOT NULL,
|
||||
$COL_SCANLATOR TEXT,
|
||||
$COL_READ BOOLEAN NOT NULL,
|
||||
$COL_BOOKMARK BOOLEAN NOT NULL,
|
||||
$COL_LAST_PAGE_READ INT NOT NULL,
|
||||
@ -52,4 +55,7 @@ object ChapterTable {
|
||||
val bookmarkUpdateQuery: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
|
||||
|
||||
val addScanlator: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
||||
|
||||
}
|
||||
|
@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.data.download
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.Constants
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.chop
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
import java.util.regex.Pattern
|
||||
@ -23,7 +23,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* Notification builder.
|
||||
*/
|
||||
private val notification by lazy {
|
||||
NotificationCompat.Builder(context)
|
||||
NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
*
|
||||
* @param id the id of the notification.
|
||||
*/
|
||||
private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID) {
|
||||
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) {
|
||||
context.notificationManager.notify(id, build())
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* those can only be dismissed by the user.
|
||||
*/
|
||||
fun dismiss() {
|
||||
context.notificationManager.cancel(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID)
|
||||
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -262,7 +262,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
}
|
||||
notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID)
|
||||
notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
|
||||
// Reset download information
|
||||
errorThrown = true
|
||||
|
@ -122,7 +122,7 @@ class DownloadService : Service() {
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ state -> onNetworkStateChanged(state)
|
||||
}, { error ->
|
||||
}, { _ ->
|
||||
toast(R.string.download_queue_error)
|
||||
stopSelf()
|
||||
})
|
||||
|
@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||
import eu.kanade.tachiyomi.util.*
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
@ -90,12 +91,10 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
||||
@Volatile private var isRunning: Boolean = false
|
||||
|
||||
init {
|
||||
Observable.fromCallable { store.restore() }
|
||||
.map { downloads -> downloads.filter { isDownloadAllowed(it) } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ downloads -> queue.addAll(downloads)
|
||||
}, { error -> Timber.e(error) })
|
||||
launchNow {
|
||||
val chapters = async { store.restore() }
|
||||
queue.addAll(chapters.await())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -114,6 +113,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
||||
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
||||
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
||||
|
||||
// Show download notification when simultaneous download > 1.
|
||||
notifier.onProgressChange(queue)
|
||||
|
||||
downloadsRelay.call(pending)
|
||||
return !pending.isEmpty()
|
||||
}
|
||||
@ -210,61 +212,54 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a download object for every chapter and adds them to the downloads queue. This method
|
||||
* must be called in the main thread.
|
||||
* Creates a download object for every chapter and adds them to the downloads queue.
|
||||
*
|
||||
* @param manga the manga of the chapters to download.
|
||||
* @param chapters the list of chapters to download.
|
||||
*/
|
||||
fun queueChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return
|
||||
fun queueChapters(manga: Manga, chapters: List<Chapter>) = launchUI {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
|
||||
|
||||
val chaptersToQueue = chapters
|
||||
// Avoid downloading chapters with the same name.
|
||||
.distinctBy { it.name }
|
||||
// Add chapters to queue from the start.
|
||||
.sortedByDescending { it.source_order }
|
||||
// Create a downloader for each one.
|
||||
.map { Download(source, manga, it) }
|
||||
// Filter out those already queued or downloaded.
|
||||
.filter { isDownloadAllowed(it) }
|
||||
// Called in background thread, the operation can be slow with SAF.
|
||||
val chaptersWithoutDir = async {
|
||||
val mangaDir = provider.findMangaDir(source, manga)
|
||||
|
||||
// Return if there's nothing to queue.
|
||||
if (chaptersToQueue.isEmpty())
|
||||
return
|
||||
|
||||
queue.addAll(chaptersToQueue)
|
||||
|
||||
// Initialize queue size.
|
||||
notifier.initialQueueSize = queue.size
|
||||
|
||||
// Initial multi-thread
|
||||
notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1
|
||||
|
||||
if (isRunning) {
|
||||
// Send the list of downloads to the downloader.
|
||||
downloadsRelay.call(chaptersToQueue)
|
||||
} else {
|
||||
// Show initial notification.
|
||||
notifier.onProgressChange(queue)
|
||||
chapters
|
||||
// Avoid downloading chapters with the same name.
|
||||
.distinctBy { it.name }
|
||||
// Filter out those already downloaded.
|
||||
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
|
||||
// Add chapters to queue from the start.
|
||||
.sortedByDescending { it.source_order }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given download can be queued and downloaded.
|
||||
*
|
||||
* @param download the download to be checked.
|
||||
*/
|
||||
private fun isDownloadAllowed(download: Download): Boolean {
|
||||
// If the chapter is already queued, don't add it again
|
||||
if (queue.any { it.chapter.id == download.chapter.id })
|
||||
return false
|
||||
// Runs in main thread (synchronization needed).
|
||||
val chaptersToQueue = chaptersWithoutDir.await()
|
||||
// Filter out those already enqueued.
|
||||
.filter { chapter -> queue.none { it.chapter.id == chapter.id } }
|
||||
// Create a download for each one.
|
||||
.map { Download(source, manga, it) }
|
||||
|
||||
val dir = provider.findChapterDir(download.source, download.manga, download.chapter)
|
||||
if (dir != null && dir.exists())
|
||||
return false
|
||||
if (chaptersToQueue.isNotEmpty()) {
|
||||
queue.addAll(chaptersToQueue)
|
||||
|
||||
return true
|
||||
// Initialize queue size.
|
||||
notifier.initialQueueSize = queue.size
|
||||
|
||||
// Initial multi-thread
|
||||
notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1
|
||||
|
||||
if (isRunning) {
|
||||
// Send the list of downloads to the downloader.
|
||||
downloadsRelay.call(chaptersToQueue)
|
||||
} else {
|
||||
// Show initial notification.
|
||||
notifier.onProgressChange(queue)
|
||||
}
|
||||
|
||||
// Start downloader if needed
|
||||
DownloadService.start(this@Downloader.context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -292,7 +287,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
||||
}
|
||||
|
||||
return pageListObservable
|
||||
.doOnNext { pages ->
|
||||
.doOnNext { _ ->
|
||||
// Delete all temporary (unfinished) files
|
||||
tmpDir.listFiles()
|
||||
?.filter { it.name!!.endsWith(".tmp") }
|
||||
@ -308,7 +303,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
||||
// Do when page is downloaded.
|
||||
.doOnNext { notifier.onProgressChange(download, queue) }
|
||||
.toList()
|
||||
.map { pages -> download }
|
||||
.map { _ -> download }
|
||||
// Do after download completes
|
||||
.doOnNext { ensureSuccessfulDownload(download, tmpDir, chapterDirname) }
|
||||
// If the page list threw, it will resume here
|
||||
@ -380,7 +375,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
||||
.map { response ->
|
||||
val file = tmpDir.createFile("$filename.tmp")
|
||||
try {
|
||||
response.body().source().saveTo(file.openOutputStream())
|
||||
response.body()!!.source().saveTo(file.openOutputStream())
|
||||
val extension = getImageExtension(response, file)
|
||||
file.renameTo("$filename.$extension")
|
||||
} catch (e: Exception) {
|
||||
@ -403,7 +398,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
||||
*/
|
||||
private fun getImageExtension(response: Response, file: UniFile): String {
|
||||
// Read content type if available.
|
||||
val mime = response.body().contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
|
||||
val mime = response.body()?.contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
|
||||
// Else guess from the uri.
|
||||
?: context.contentResolver.getType(file.uri)
|
||||
// Else read magic numbers.
|
||||
|
@ -66,6 +66,7 @@ class DownloadQueue(
|
||||
val pageStatusSubject = PublishSubject.create<Int>()
|
||||
setPagesSubject(download.pages, pageStatusSubject)
|
||||
return@flatMap pageStatusSubject
|
||||
.onBackpressureBuffer()
|
||||
.filter { it == Page.READY }
|
||||
.map { download }
|
||||
|
||||
|
@ -1,35 +1,51 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||
|
||||
private var data: InputStream? = null
|
||||
|
||||
override fun loadData(priority: Priority): InputStream {
|
||||
data = file.inputStream()
|
||||
return data!!
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
data?.let { data ->
|
||||
try {
|
||||
data.close()
|
||||
} catch (e: IOException) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun getId(): String {
|
||||
return file.toString()
|
||||
}
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.ContentValues.TAG
|
||||
import android.util.Log
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import java.io.*
|
||||
|
||||
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||
|
||||
private var data: InputStream? = null
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
loadFromFile(callback)
|
||||
}
|
||||
|
||||
protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
try {
|
||||
data = FileInputStream(file)
|
||||
} catch (e: FileNotFoundException) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Failed to open file", e)
|
||||
}
|
||||
callback.onLoadFailed(e)
|
||||
return
|
||||
}
|
||||
|
||||
callback.onDataReady(data)
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
try {
|
||||
data?.close()
|
||||
} catch (e: IOException) {
|
||||
// Ignored.
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun getDataClass(): Class<InputStream> {
|
||||
return InputStream::class.java
|
||||
}
|
||||
|
||||
override fun getDataSource(): DataSource {
|
||||
return DataSource.LOCAL
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* A [DataFetcher] for loading a cover of a library manga.
|
||||
* It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network
|
||||
* and copies the result to the cache.
|
||||
*
|
||||
* @param networkFetcher the network fetcher for this cover.
|
||||
* @param manga the manga of the cover to load.
|
||||
* @param file the file where this cover should be. It may exists or not.
|
||||
*/
|
||||
class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
||||
private val manga: Manga,
|
||||
private val file: File)
|
||||
: FileFetcher(file) {
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
if (!file.exists()) {
|
||||
networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> {
|
||||
override fun onDataReady(data: InputStream?) {
|
||||
if (data != null) {
|
||||
val tmpFile = File(file.path + ".tmp")
|
||||
try {
|
||||
// Retrieve destination stream, create parent folders if needed.
|
||||
val output = try {
|
||||
tmpFile.outputStream()
|
||||
} catch (e: FileNotFoundException) {
|
||||
tmpFile.parentFile.mkdirs()
|
||||
tmpFile.outputStream()
|
||||
}
|
||||
|
||||
// Copy the file and rename to the original.
|
||||
data.use { output.use { data.copyTo(output) } }
|
||||
tmpFile.renameTo(file)
|
||||
loadFromFile(callback)
|
||||
} catch (e: Exception) {
|
||||
tmpFile.delete()
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
} else {
|
||||
callback.onLoadFailed(Exception("Null data"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadFailed(e: Exception) {
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
|
||||
})
|
||||
} else {
|
||||
loadFromFile(callback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
super.cleanup()
|
||||
networkFetcher.cleanup()
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
super.cancel()
|
||||
networkFetcher.cancel()
|
||||
}
|
||||
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
|
||||
open class MangaFileFetcher(private val file: File, private val manga: Manga) : FileFetcher(file) {
|
||||
|
||||
/**
|
||||
* Returns the id for this manga's cover.
|
||||
*
|
||||
* Appending the file's modified date to the url, we can force Glide to skip its memory and disk
|
||||
* lookup step and fetch from our custom cache. This allows us to invalidate Glide's cache when
|
||||
* the file has changed. If the file doesn't exist it will append a 0.
|
||||
*/
|
||||
override fun getId(): String {
|
||||
return manga.thumbnail_url + file.lastModified()
|
||||
}
|
||||
}
|
@ -1,23 +1,24 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.util.LruCache
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.model.*
|
||||
import com.bumptech.glide.load.model.stream.StreamModelLoader
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
/**
|
||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
||||
* Coupled with [MangaUrlFetcher], this class allows to implement the following flow:
|
||||
* Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
|
||||
*
|
||||
* - Check in RAM LRU.
|
||||
* - Check in disk LRU.
|
||||
@ -26,7 +27,7 @@ import java.io.InputStream
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
||||
|
||||
/**
|
||||
* Cover cache where persistent covers are stored.
|
||||
@ -39,16 +40,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Base network loader.
|
||||
* Default network client.
|
||||
*/
|
||||
private val baseUrlLoader = Glide.buildModelLoader(GlideUrl::class.java,
|
||||
InputStream::class.java, context)
|
||||
private val defaultClient = Injekt.get<NetworkHelper>().client
|
||||
|
||||
/**
|
||||
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
|
||||
* and the file where it should be stored in case the manga is a favorite.
|
||||
*/
|
||||
private val lruCache = LruCache<String, Pair<GlideUrl, File>>(100)
|
||||
private val lruCache = LruCache<GlideUrl, File>(100)
|
||||
|
||||
/**
|
||||
* Map where request headers are stored for a source.
|
||||
@ -60,12 +60,17 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
*/
|
||||
class Factory : ModelLoaderFactory<Manga, InputStream> {
|
||||
|
||||
override fun build(context: Context, factories: GenericLoaderFactory)
|
||||
= MangaModelLoader(context)
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> {
|
||||
return MangaModelLoader()
|
||||
}
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
|
||||
override fun handles(model: Manga): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fetcher for the given manga or null if the url is empty.
|
||||
*
|
||||
@ -73,10 +78,8 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
* @param width the width of the view where the resource will be loaded.
|
||||
* @param height the height of the view where the resource will be loaded.
|
||||
*/
|
||||
override fun getResourceFetcher(manga: Manga,
|
||||
width: Int,
|
||||
height: Int): DataFetcher<InputStream>? {
|
||||
|
||||
override fun buildLoadData(manga: Manga, width: Int, height: Int,
|
||||
options: Options?): ModelLoader.LoadData<InputStream>? {
|
||||
// Check thumbnail is not null or empty
|
||||
val url = manga.thumbnail_url
|
||||
if (url == null || url.isEmpty()) {
|
||||
@ -85,26 +88,28 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
|
||||
if (url.startsWith("http")) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
|
||||
// Obtain the request url and the file for this url from the LRU cache, or calculate it
|
||||
// and add them to the cache.
|
||||
val (glideUrl, file) = lruCache.get(url) ?:
|
||||
Pair(GlideUrl(url, getHeaders(manga, source)), coverCache.getCoverFile(url)).apply {
|
||||
lruCache.put(url, this)
|
||||
}
|
||||
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
||||
|
||||
// Get the resource fetcher for this request url.
|
||||
val networkFetcher = source?.let { OkHttpStreamFetcher(it.client, glideUrl) }
|
||||
?: baseUrlLoader.getResourceFetcher(glideUrl, width, height)
|
||||
val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
|
||||
|
||||
if (!manga.favorite) {
|
||||
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
||||
}
|
||||
|
||||
// Obtain the file for this url from the LRU cache, or retrieve and add it to the cache.
|
||||
val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) }
|
||||
|
||||
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file)
|
||||
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return MangaUrlFetcher(networkFetcher, file, manga)
|
||||
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
|
||||
} else {
|
||||
// Get the file from the url, removing the scheme if present.
|
||||
val file = File(url.substringAfter("file://"))
|
||||
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return MangaFileFetcher(file, manga)
|
||||
return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,4 +132,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <K, V> LruCache<K, V>.getOrPut(key: K, defaultValue: () -> V): V {
|
||||
val value = get(key)
|
||||
return if (value == null) {
|
||||
val answer = defaultValue()
|
||||
put(key, answer)
|
||||
answer
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.load.Key
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
class MangaSignature(manga: Manga, file: File) : Key {
|
||||
|
||||
private val key = manga.thumbnail_url + file.lastModified()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is MangaSignature) {
|
||||
key == other.key
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return key.hashCode()
|
||||
}
|
||||
|
||||
override fun updateDiskCacheKey(md: MessageDigest) {
|
||||
md.update(key.toByteArray(Key.CHARSET))
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* A [DataFetcher] for loading a cover of a manga depending on its favorite status.
|
||||
* If the manga is favorite, it tries to load the cover from our cache, and if it's not found, it
|
||||
* fallbacks to network and copies it to the cache.
|
||||
* If the manga is not favorite, it tries to delete the cover from our cache and always fallback
|
||||
* to network for fetching.
|
||||
*
|
||||
* @param networkFetcher the network fetcher for this cover.
|
||||
* @param file the file where this cover should be. It may exists or not.
|
||||
* @param manga the manga of the cover to load.
|
||||
*/
|
||||
class MangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
||||
private val file: File,
|
||||
private val manga: Manga)
|
||||
: MangaFileFetcher(file, manga) {
|
||||
|
||||
override fun loadData(priority: Priority): InputStream {
|
||||
if (manga.favorite) {
|
||||
synchronized(file) {
|
||||
if (!file.exists()) {
|
||||
val tmpFile = File(file.path + ".tmp")
|
||||
try {
|
||||
// Retrieve source stream.
|
||||
val input = networkFetcher.loadData(priority)
|
||||
?: throw Exception("Couldn't open source stream")
|
||||
|
||||
// Retrieve destination stream, create parent folders if needed.
|
||||
val output = try {
|
||||
tmpFile.outputStream()
|
||||
} catch (e: FileNotFoundException) {
|
||||
tmpFile.parentFile.mkdirs()
|
||||
tmpFile.outputStream()
|
||||
}
|
||||
|
||||
// Copy the file and rename to the original.
|
||||
input.use { output.use { input.copyTo(output) } }
|
||||
tmpFile.renameTo(file)
|
||||
} catch (e: Exception) {
|
||||
tmpFile.delete()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.loadData(priority)
|
||||
} else {
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
}
|
||||
return networkFetcher.loadData(priority)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
networkFetcher.cancel()
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
super.cleanup()
|
||||
networkFetcher.cleanup()
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,18 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.GlideModule
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@ -16,17 +22,20 @@ import java.io.InputStream
|
||||
/**
|
||||
* Class used to update Glide module settings
|
||||
*/
|
||||
class AppGlideModule : GlideModule {
|
||||
@GlideModule
|
||||
class TachiGlideModule : AppGlideModule() {
|
||||
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
// Set the cache size of Glide to 15 MiB
|
||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
|
||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
|
||||
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
|
||||
builder.setDefaultTransitionOptions(Drawable::class.java,
|
||||
DrawableTransitionOptions.withCrossFade())
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide) {
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
||||
|
||||
glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory)
|
||||
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
||||
registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
|
||||
registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
||||
}
|
||||
}
|
@ -34,7 +34,6 @@ class LibraryUpdateJob : Job() {
|
||||
.setRequiredNetworkType(wifiRestriction)
|
||||
.setRequiresCharging(acRestriction)
|
||||
.setRequirementsEnforced(true)
|
||||
.setPersisted(true)
|
||||
.setUpdateCurrent(true)
|
||||
.build()
|
||||
.schedule()
|
||||
|
@ -10,18 +10,20 @@ import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.Constants
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
@ -48,7 +50,8 @@ class LibraryUpdateService(
|
||||
val db: DatabaseHelper = Injekt.get(),
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
val preferences: PreferencesHelper = Injekt.get(),
|
||||
val downloadManager: DownloadManager = Injekt.get()
|
||||
val downloadManager: DownloadManager = Injekt.get(),
|
||||
val trackManager: TrackManager = Injekt.get()
|
||||
) : Service() {
|
||||
|
||||
/**
|
||||
@ -78,24 +81,35 @@ class LibraryUpdateService(
|
||||
/**
|
||||
* Cached progress notification to avoid creating a lot.
|
||||
*/
|
||||
private val progressNotification by lazy { NotificationCompat.Builder(this)
|
||||
private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||
.setLargeIcon(notificationBitmap)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines what should be updated within a service execution.
|
||||
*/
|
||||
enum class Target {
|
||||
CHAPTERS, // Manga chapters
|
||||
DETAILS, // Manga metadata
|
||||
TRACKING // Tracking metadata
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Key for category to update.
|
||||
*/
|
||||
const val UPDATE_CATEGORY = "category"
|
||||
const val KEY_CATEGORY = "category"
|
||||
|
||||
/**
|
||||
* Key for updating the details instead of the chapters.
|
||||
* Key that defines what should be updated.
|
||||
*/
|
||||
const val UPDATE_DETAILS = "details"
|
||||
const val KEY_TARGET = "target"
|
||||
|
||||
/**
|
||||
* Returns the status of the service.
|
||||
@ -104,7 +118,7 @@ class LibraryUpdateService(
|
||||
* @return true if the service is running, false otherwise.
|
||||
*/
|
||||
fun isRunning(context: Context): Boolean {
|
||||
return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java)
|
||||
return context.isServiceRunning(LibraryUpdateService::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,15 +127,19 @@ class LibraryUpdateService(
|
||||
*
|
||||
* @param context the application context.
|
||||
* @param category a specific category to update, or null for global update.
|
||||
* @param details whether to update the details instead of the list of chapters.
|
||||
* @param target defines what should be updated.
|
||||
*/
|
||||
fun start(context: Context, category: Category? = null, details: Boolean = false) {
|
||||
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS) {
|
||||
if (!isRunning(context)) {
|
||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||
putExtra(UPDATE_DETAILS, details)
|
||||
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
|
||||
putExtra(KEY_TARGET, target)
|
||||
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
context.startService(intent)
|
||||
} else {
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,6 +160,7 @@ class LibraryUpdateService(
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
|
||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
|
||||
wakeLock.acquire()
|
||||
@ -176,6 +195,8 @@ class LibraryUpdateService(
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent == null) return Service.START_NOT_STICKY
|
||||
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
||||
?: return Service.START_NOT_STICKY
|
||||
|
||||
// Unsubscribe from any previous subscription if needed.
|
||||
subscription?.unsubscribe()
|
||||
@ -183,13 +204,14 @@ class LibraryUpdateService(
|
||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||
subscription = Observable
|
||||
.defer {
|
||||
val mangaList = getMangaToUpdate(intent)
|
||||
val mangaList = getMangaToUpdate(intent, target)
|
||||
|
||||
// Update either chapter list or manga details.
|
||||
if (!intent.getBooleanExtra(UPDATE_DETAILS, false))
|
||||
updateChapterList(mangaList)
|
||||
else
|
||||
updateDetails(mangaList)
|
||||
when (target) {
|
||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||
Target.DETAILS -> updateDetails(mangaList)
|
||||
Target.TRACKING -> updateTrackings(mangaList)
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe({
|
||||
@ -207,10 +229,11 @@ class LibraryUpdateService(
|
||||
* Returns the list of manga to be updated.
|
||||
*
|
||||
* @param intent the update intent.
|
||||
* @param target the target to update.
|
||||
* @return a list of manga to update
|
||||
*/
|
||||
fun getMangaToUpdate(intent: Intent): List<Manga> {
|
||||
val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1)
|
||||
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||
|
||||
var listToUpdate = if (categoryId != -1)
|
||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||
@ -224,7 +247,7 @@ class LibraryUpdateService(
|
||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||
}
|
||||
|
||||
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
|
||||
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||
}
|
||||
|
||||
@ -240,7 +263,7 @@ class LibraryUpdateService(
|
||||
* @param mangaToUpdate the list to update
|
||||
* @return an observable delivering the progress of each update.
|
||||
*/
|
||||
fun updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> {
|
||||
fun updateChapterList(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||
// Initialize the variables holding the progress of the updates.
|
||||
val count = AtomicInteger(0)
|
||||
// List containing new updates
|
||||
@ -264,7 +287,7 @@ class LibraryUpdateService(
|
||||
// If there's any error, return empty update and continue.
|
||||
.onErrorReturn {
|
||||
failedUpdates.add(manga)
|
||||
Pair(emptyList<Chapter>(), emptyList<Chapter>())
|
||||
Pair(emptyList(), emptyList())
|
||||
}
|
||||
// Filter out mangas without new chapters (or failed).
|
||||
.filter { pair -> pair.first.isNotEmpty() }
|
||||
@ -328,13 +351,11 @@ class LibraryUpdateService(
|
||||
/**
|
||||
* Method that updates the details of the given list of manga. It's called in a background
|
||||
* thread, so it's safe to do heavy operations or network calls here.
|
||||
* For each manga it calls [updateManga] and updates the notification showing the current
|
||||
* progress.
|
||||
*
|
||||
* @param mangaToUpdate the list to update
|
||||
* @return an observable delivering the progress of each update.
|
||||
*/
|
||||
fun updateDetails(mangaToUpdate: List<Manga>): Observable<Manga> {
|
||||
fun updateDetails(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||
// Initialize the variables holding the progress of the updates.
|
||||
val count = AtomicInteger(0)
|
||||
|
||||
@ -345,7 +366,7 @@ class LibraryUpdateService(
|
||||
// Update the details of the manga.
|
||||
.concatMap { manga ->
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
?: return@concatMap Observable.empty<Manga>()
|
||||
?: return@concatMap Observable.empty<LibraryManga>()
|
||||
|
||||
source.fetchMangaDetails(manga)
|
||||
.map { networkManga ->
|
||||
@ -360,6 +381,42 @@ class LibraryUpdateService(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that updates the metadata of the connected tracking services. It's called in a
|
||||
* background thread, so it's safe to do heavy operations or network calls here.
|
||||
*/
|
||||
private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||
// Initialize the variables holding the progress of the updates.
|
||||
var count = 0
|
||||
|
||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||
|
||||
// Emit each manga and update it sequentially.
|
||||
return Observable.from(mangaToUpdate)
|
||||
// Notify manga that will update.
|
||||
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
|
||||
// Update the tracking details.
|
||||
.concatMap { manga ->
|
||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||
|
||||
Observable.from(tracks)
|
||||
.concatMap { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service in loggedServices) {
|
||||
service.refresh(track)
|
||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||
.onErrorReturn { track }
|
||||
} else {
|
||||
Observable.empty()
|
||||
}
|
||||
}
|
||||
.map { manga }
|
||||
}
|
||||
.doOnCompleted {
|
||||
cancelProgressNotification()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the notification containing the currently updating manga and the progress.
|
||||
*
|
||||
@ -368,7 +425,7 @@ class LibraryUpdateService(
|
||||
* @param total the total progress.
|
||||
*/
|
||||
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
||||
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID, progressNotification
|
||||
notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification
|
||||
.setContentTitle(manga.title)
|
||||
.setProgress(total, current, false)
|
||||
.build())
|
||||
@ -385,7 +442,7 @@ class LibraryUpdateService(
|
||||
// Append new chapters from a previous, existing notification
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val previousNotification = notificationManager.activeNotifications
|
||||
.find { it.id == Constants.NOTIFICATION_LIBRARY_RESULT_ID }
|
||||
.find { it.id == Notifications.ID_LIBRARY_RESULT }
|
||||
|
||||
if (previousNotification != null) {
|
||||
val oldUpdates = previousNotification.notification.extras
|
||||
@ -397,7 +454,7 @@ class LibraryUpdateService(
|
||||
}
|
||||
}
|
||||
|
||||
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_RESULT_ID, notification {
|
||||
notificationManager.notify(Notifications.ID_LIBRARY_RESULT, notification(Notifications.CHANNEL_LIBRARY) {
|
||||
setSmallIcon(R.drawable.ic_book_white_24dp)
|
||||
setLargeIcon(notificationBitmap)
|
||||
setContentTitle(getString(R.string.notification_new_chapters))
|
||||
@ -417,7 +474,7 @@ class LibraryUpdateService(
|
||||
* Cancels the progress notification.
|
||||
*/
|
||||
private fun cancelProgressNotification() {
|
||||
notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID)
|
||||
notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -426,6 +483,7 @@ class LibraryUpdateService(
|
||||
private fun getNotificationIntent(): PendingIntent {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED
|
||||
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadActivity
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.getUriCompat
|
||||
import java.io.File
|
||||
|
||||
@ -17,8 +17,9 @@ object NotificationHandler {
|
||||
* @param context context of application
|
||||
*/
|
||||
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
|
||||
val intent = Intent(context, DownloadActivity::class.java).apply {
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
||||
action = MainActivity.SHORTCUT_DOWNLOADS
|
||||
}
|
||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import eu.kanade.tachiyomi.Constants
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
@ -41,6 +40,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
ACTION_RESUME_DOWNLOADS -> DownloadService.start(context)
|
||||
// Clear the download queue
|
||||
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
|
||||
// Show message notification created
|
||||
ACTION_SHORTCUT_CREATED -> context.toast(R.string.shortcut_created)
|
||||
// Launch share activity and dismiss notification
|
||||
ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||
@ -48,7 +49,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||
// Cancel library update and dismiss notification
|
||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Constants.NOTIFICATION_LIBRARY_PROGRESS_ID)
|
||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
|
||||
// Open reader activity
|
||||
ACTION_OPEN_CHAPTER -> {
|
||||
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||
@ -161,6 +162,9 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
// Called to clear downloads.
|
||||
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
|
||||
|
||||
// Called to notify user shortcut is created.
|
||||
private const val ACTION_SHORTCUT_CREATED = "$ID.$NAME.ACTION_SHORTCUT_CREATED"
|
||||
|
||||
// Called to dismiss notification.
|
||||
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
|
||||
|
||||
@ -199,6 +203,13 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
internal fun shortcutCreatedBroadcast(context: Context) : PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_SHORTCUT_CREATED
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that starts a service which dismissed the notification
|
||||
*
|
||||
|
@ -0,0 +1,54 @@
|
||||
package eu.kanade.tachiyomi.data.notification
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
|
||||
/**
|
||||
* Class to manage the basic information of all the notifications used in the app.
|
||||
*/
|
||||
object Notifications {
|
||||
|
||||
/**
|
||||
* Common notification channel and ids used anywhere.
|
||||
*/
|
||||
const val CHANNEL_COMMON = "common_channel"
|
||||
const val ID_UPDATER = 1
|
||||
const val ID_DOWNLOAD_IMAGE = 2
|
||||
|
||||
/**
|
||||
* Notification channel and ids used by the library updater.
|
||||
*/
|
||||
const val CHANNEL_LIBRARY = "library_channel"
|
||||
const val ID_LIBRARY_PROGRESS = 101
|
||||
const val ID_LIBRARY_RESULT = 102
|
||||
|
||||
/**
|
||||
* Notification channel and ids used by the downloader.
|
||||
*/
|
||||
const val CHANNEL_DOWNLOADER = "downloader_channel"
|
||||
const val ID_DOWNLOAD_CHAPTER = 201
|
||||
const val ID_DOWNLOAD_CHAPTER_ERROR = 202
|
||||
|
||||
/**
|
||||
* Creates the notification channels introduced in Android Oreo.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
fun createChannels(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
val channels = listOf(
|
||||
NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common),
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library),
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
|
||||
NotificationManager.IMPORTANCE_LOW)
|
||||
)
|
||||
context.notificationManager.createNotificationChannels(channels)
|
||||
}
|
||||
}
|
@ -1,120 +1,120 @@
|
||||
package eu.kanade.tachiyomi.data.preference
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
/**
|
||||
* This class stores the keys for the preferences in the application. Most of them are defined
|
||||
* in the file "keys.xml". By using this class we can define preferences in one place and get them
|
||||
* referenced here.
|
||||
*/
|
||||
@Suppress("HasPlatformType")
|
||||
class PreferenceKeys(context: Context) {
|
||||
|
||||
val theme = context.getString(R.string.pref_theme_key)
|
||||
|
||||
val rotation = context.getString(R.string.pref_rotation_type_key)
|
||||
|
||||
val enableTransitions = context.getString(R.string.pref_enable_transitions_key)
|
||||
|
||||
val showPageNumber = context.getString(R.string.pref_show_page_number_key)
|
||||
|
||||
val fullscreen = context.getString(R.string.pref_fullscreen_key)
|
||||
|
||||
val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key)
|
||||
|
||||
val customBrightness = context.getString(R.string.pref_custom_brightness_key)
|
||||
|
||||
val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key)
|
||||
|
||||
val colorFilter = context.getString(R.string.pref_color_filter_key)
|
||||
|
||||
val colorFilterValue = context.getString(R.string.pref_color_filter_value_key)
|
||||
|
||||
val defaultViewer = context.getString(R.string.pref_default_viewer_key)
|
||||
|
||||
val imageScaleType = context.getString(R.string.pref_image_scale_type_key)
|
||||
|
||||
val imageDecoder = context.getString(R.string.pref_image_decoder_key)
|
||||
|
||||
val zoomStart = context.getString(R.string.pref_zoom_start_key)
|
||||
|
||||
val readerTheme = context.getString(R.string.pref_reader_theme_key)
|
||||
|
||||
val cropBorders = context.getString(R.string.pref_crop_borders_key)
|
||||
|
||||
val readWithTapping = context.getString(R.string.pref_read_with_tapping_key)
|
||||
|
||||
val readWithVolumeKeys = context.getString(R.string.pref_read_with_volume_keys_key)
|
||||
|
||||
val portraitColumns = context.getString(R.string.pref_library_columns_portrait_key)
|
||||
|
||||
val landscapeColumns = context.getString(R.string.pref_library_columns_landscape_key)
|
||||
|
||||
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
|
||||
|
||||
val autoUpdateTrack = context.getString(R.string.pref_auto_update_manga_sync_key)
|
||||
|
||||
val askUpdateTrack = context.getString(R.string.pref_ask_update_manga_sync_key)
|
||||
|
||||
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
|
||||
|
||||
val lastUsedCategory = context.getString(R.string.pref_last_used_category_key)
|
||||
|
||||
val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list)
|
||||
|
||||
val enabledLanguages = context.getString(R.string.pref_source_languages)
|
||||
|
||||
val backupDirectory = context.getString(R.string.pref_backup_directory_key)
|
||||
|
||||
val downloadsDirectory = context.getString(R.string.pref_download_directory_key)
|
||||
|
||||
val downloadThreads = context.getString(R.string.pref_download_slots_key)
|
||||
|
||||
val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key)
|
||||
|
||||
val numberOfBackups = context.getString(R.string.pref_backup_slots_key)
|
||||
|
||||
val backupInterval = context.getString(R.string.pref_backup_interval_key)
|
||||
|
||||
val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key)
|
||||
|
||||
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key)
|
||||
|
||||
val libraryUpdateInterval = context.getString(R.string.pref_library_update_interval_key)
|
||||
|
||||
val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key)
|
||||
|
||||
val libraryUpdateCategories = context.getString(R.string.pref_library_update_categories_key)
|
||||
|
||||
val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key)
|
||||
|
||||
val filterUnread = context.getString(R.string.pref_filter_unread_key)
|
||||
|
||||
val librarySortingMode = context.getString(R.string.pref_library_sorting_mode_key)
|
||||
|
||||
val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key)
|
||||
|
||||
val startScreen = context.getString(R.string.pref_start_screen_key)
|
||||
|
||||
val downloadNew = context.getString(R.string.pref_download_new_key)
|
||||
|
||||
val downloadNewCategories = context.getString(R.string.pref_download_new_categories_key)
|
||||
|
||||
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
||||
|
||||
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
||||
|
||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||
|
||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||
|
||||
fun trackToken(syncId: Int) = "track_token_$syncId"
|
||||
|
||||
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
|
||||
|
||||
val lang = context.getString(R.string.pref_language_key)
|
||||
|
||||
val defaultCategory = context.getString(R.string.default_category_key)
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.data.preference
|
||||
|
||||
/**
|
||||
* This class stores the keys for the preferences in the application.
|
||||
*/
|
||||
object PreferenceKeys {
|
||||
|
||||
const val theme = "pref_theme_key"
|
||||
|
||||
const val rotation = "pref_rotation_type_key"
|
||||
|
||||
const val enableTransitions = "pref_enable_transitions_key"
|
||||
|
||||
const val showPageNumber = "pref_show_page_number_key"
|
||||
|
||||
const val fullscreen = "fullscreen"
|
||||
|
||||
const val keepScreenOn = "pref_keep_screen_on_key"
|
||||
|
||||
const val customBrightness = "pref_custom_brightness_key"
|
||||
|
||||
const val customBrightnessValue = "custom_brightness_value"
|
||||
|
||||
const val colorFilter = "pref_color_filter_key"
|
||||
|
||||
const val colorFilterValue = "color_filter_value"
|
||||
|
||||
const val defaultViewer = "pref_default_viewer_key"
|
||||
|
||||
const val imageScaleType = "pref_image_scale_type_key"
|
||||
|
||||
const val imageDecoder = "image_decoder"
|
||||
|
||||
const val zoomStart = "pref_zoom_start_key"
|
||||
|
||||
const val readerTheme = "pref_reader_theme_key"
|
||||
|
||||
const val cropBorders = "crop_borders"
|
||||
|
||||
const val readWithTapping = "reader_tap"
|
||||
|
||||
const val readWithVolumeKeys = "reader_volume_keys"
|
||||
|
||||
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
|
||||
|
||||
const val portraitColumns = "pref_library_columns_portrait_key"
|
||||
|
||||
const val landscapeColumns = "pref_library_columns_landscape_key"
|
||||
|
||||
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
|
||||
|
||||
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||
|
||||
const val askUpdateTrack = "pref_ask_update_manga_sync_key"
|
||||
|
||||
const val lastUsedCatalogueSource = "last_catalogue_source"
|
||||
|
||||
const val lastUsedCategory = "last_used_category"
|
||||
|
||||
const val catalogueAsList = "pref_display_catalogue_as_list"
|
||||
|
||||
const val enabledLanguages = "source_languages"
|
||||
|
||||
const val backupDirectory = "backup_directory"
|
||||
|
||||
const val downloadsDirectory = "download_directory"
|
||||
|
||||
const val downloadThreads = "pref_download_slots_key"
|
||||
|
||||
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
||||
|
||||
const val numberOfBackups = "backup_slots"
|
||||
|
||||
const val backupInterval = "backup_interval"
|
||||
|
||||
const val removeAfterReadSlots = "remove_after_read_slots"
|
||||
|
||||
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
||||
|
||||
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
||||
|
||||
const val libraryUpdateRestriction = "library_update_restriction"
|
||||
|
||||
const val libraryUpdateCategories = "library_update_categories"
|
||||
|
||||
const val filterDownloaded = "pref_filter_downloaded_key"
|
||||
|
||||
const val filterUnread = "pref_filter_unread_key"
|
||||
|
||||
const val filterCompleted = "pref_filter_completed_key"
|
||||
|
||||
const val librarySortingMode = "library_sorting_mode"
|
||||
|
||||
const val automaticUpdates = "automatic_updates"
|
||||
|
||||
const val startScreen = "start_screen"
|
||||
|
||||
const val downloadNew = "download_new"
|
||||
|
||||
const val downloadNewCategories = "download_new_categories"
|
||||
|
||||
const val libraryAsList = "pref_display_library_as_list"
|
||||
|
||||
const val lang = "app_language"
|
||||
|
||||
const val defaultCategory = "default_category"
|
||||
|
||||
const val downloadBadge = "display_download_badge"
|
||||
|
||||
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
||||
|
||||
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
||||
|
||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||
|
||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||
|
||||
fun trackToken(syncId: Int) = "track_token_$syncId"
|
||||
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import java.io.File
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
|
||||
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
||||
|
||||
@ -17,8 +18,6 @@ fun Preference<Boolean>.invert(): Boolean = getOrDefault().let { set(!it); !it }
|
||||
|
||||
class PreferencesHelper(val context: Context) {
|
||||
|
||||
val keys = PreferenceKeys(context)
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val rxPrefs = RxSharedPreferences.create(prefs)
|
||||
|
||||
@ -30,134 +29,140 @@ class PreferencesHelper(val context: Context) {
|
||||
File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
context.getString(R.string.app_name), "backup"))
|
||||
|
||||
fun startScreen() = prefs.getInt(keys.startScreen, 1)
|
||||
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
|
||||
|
||||
fun clear() = prefs.edit().clear().apply()
|
||||
|
||||
fun theme() = prefs.getInt(keys.theme, 1)
|
||||
fun theme() = prefs.getInt(Keys.theme, 1)
|
||||
|
||||
fun rotation() = rxPrefs.getInteger(keys.rotation, 1)
|
||||
fun rotation() = rxPrefs.getInteger(Keys.rotation, 1)
|
||||
|
||||
fun pageTransitions() = rxPrefs.getBoolean(keys.enableTransitions, true)
|
||||
fun pageTransitions() = rxPrefs.getBoolean(Keys.enableTransitions, true)
|
||||
|
||||
fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true)
|
||||
fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true)
|
||||
|
||||
fun fullscreen() = rxPrefs.getBoolean(keys.fullscreen, true)
|
||||
fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true)
|
||||
|
||||
fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true)
|
||||
fun keepScreenOn() = rxPrefs.getBoolean(Keys.keepScreenOn, true)
|
||||
|
||||
fun customBrightness() = rxPrefs.getBoolean(keys.customBrightness, false)
|
||||
fun customBrightness() = rxPrefs.getBoolean(Keys.customBrightness, false)
|
||||
|
||||
fun customBrightnessValue() = rxPrefs.getInteger(keys.customBrightnessValue, 0)
|
||||
fun customBrightnessValue() = rxPrefs.getInteger(Keys.customBrightnessValue, 0)
|
||||
|
||||
fun colorFilter() = rxPrefs.getBoolean(keys.colorFilter, false)
|
||||
fun colorFilter() = rxPrefs.getBoolean(Keys.colorFilter, false)
|
||||
|
||||
fun colorFilterValue() = rxPrefs.getInteger(keys.colorFilterValue, 0)
|
||||
fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 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)
|
||||
|
||||
fun imageDecoder() = rxPrefs.getInteger(keys.imageDecoder, 0)
|
||||
fun imageDecoder() = rxPrefs.getInteger(Keys.imageDecoder, 0)
|
||||
|
||||
fun zoomStart() = rxPrefs.getInteger(keys.zoomStart, 1)
|
||||
fun zoomStart() = rxPrefs.getInteger(Keys.zoomStart, 1)
|
||||
|
||||
fun readerTheme() = rxPrefs.getInteger(keys.readerTheme, 0)
|
||||
fun readerTheme() = rxPrefs.getInteger(Keys.readerTheme, 0)
|
||||
|
||||
fun cropBorders() = rxPrefs.getBoolean(keys.cropBorders, false)
|
||||
fun cropBorders() = rxPrefs.getBoolean(Keys.cropBorders, false)
|
||||
|
||||
fun readWithTapping() = rxPrefs.getBoolean(keys.readWithTapping, true)
|
||||
fun readWithTapping() = rxPrefs.getBoolean(Keys.readWithTapping, true)
|
||||
|
||||
fun readWithVolumeKeys() = rxPrefs.getBoolean(keys.readWithVolumeKeys, false)
|
||||
fun readWithVolumeKeys() = rxPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
||||
|
||||
fun portraitColumns() = rxPrefs.getInteger(keys.portraitColumns, 0)
|
||||
fun readWithVolumeKeysInverted() = rxPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
|
||||
|
||||
fun landscapeColumns() = rxPrefs.getInteger(keys.landscapeColumns, 0)
|
||||
fun portraitColumns() = rxPrefs.getInteger(Keys.portraitColumns, 0)
|
||||
|
||||
fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false)
|
||||
fun landscapeColumns() = rxPrefs.getInteger(Keys.landscapeColumns, 0)
|
||||
|
||||
fun autoUpdateTrack() = prefs.getBoolean(keys.autoUpdateTrack, true)
|
||||
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
|
||||
|
||||
fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false)
|
||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||
|
||||
fun lastUsedCatalogueSource() = rxPrefs.getLong(keys.lastUsedCatalogueSource, -1)
|
||||
fun askUpdateTrack() = prefs.getBoolean(Keys.askUpdateTrack, false)
|
||||
|
||||
fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0)
|
||||
fun lastUsedCatalogueSource() = rxPrefs.getLong(Keys.lastUsedCatalogueSource, -1)
|
||||
|
||||
fun lastUsedCategory() = rxPrefs.getInteger(Keys.lastUsedCategory, 0)
|
||||
|
||||
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
|
||||
|
||||
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
|
||||
fun catalogueAsList() = rxPrefs.getBoolean(Keys.catalogueAsList, false)
|
||||
|
||||
fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("en"))
|
||||
fun enabledLanguages() = rxPrefs.getStringSet(Keys.enabledLanguages, setOf("en"))
|
||||
|
||||
fun sourceUsername(source: Source) = prefs.getString(keys.sourceUsername(source.id), "")
|
||||
fun sourceUsername(source: Source) = prefs.getString(Keys.sourceUsername(source.id), "")
|
||||
|
||||
fun sourcePassword(source: Source) = prefs.getString(keys.sourcePassword(source.id), "")
|
||||
fun sourcePassword(source: Source) = prefs.getString(Keys.sourcePassword(source.id), "")
|
||||
|
||||
fun setSourceCredentials(source: Source, username: String, password: String) {
|
||||
prefs.edit()
|
||||
.putString(keys.sourceUsername(source.id), username)
|
||||
.putString(keys.sourcePassword(source.id), password)
|
||||
.putString(Keys.sourceUsername(source.id), username)
|
||||
.putString(Keys.sourcePassword(source.id), password)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun trackUsername(sync: TrackService) = prefs.getString(keys.trackUsername(sync.id), "")
|
||||
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
|
||||
|
||||
fun trackPassword(sync: TrackService) = prefs.getString(keys.trackPassword(sync.id), "")
|
||||
fun trackPassword(sync: TrackService) = prefs.getString(Keys.trackPassword(sync.id), "")
|
||||
|
||||
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
||||
prefs.edit()
|
||||
.putString(keys.trackUsername(sync.id), username)
|
||||
.putString(keys.trackPassword(sync.id), password)
|
||||
.putString(Keys.trackUsername(sync.id), username)
|
||||
.putString(Keys.trackPassword(sync.id), password)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun trackToken(sync: TrackService) = rxPrefs.getString(keys.trackToken(sync.id), "")
|
||||
fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "")
|
||||
|
||||
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
|
||||
|
||||
fun backupsDirectory() = rxPrefs.getString(keys.backupDirectory, defaultBackupDir.toString())
|
||||
fun backupsDirectory() = rxPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
||||
|
||||
fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString())
|
||||
fun downloadsDirectory() = rxPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
|
||||
|
||||
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
|
||||
fun downloadThreads() = rxPrefs.getInteger(Keys.downloadThreads, 1)
|
||||
|
||||
fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
|
||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||
|
||||
fun numberOfBackups() = rxPrefs.getInteger(keys.numberOfBackups, 1)
|
||||
fun numberOfBackups() = rxPrefs.getInteger(Keys.numberOfBackups, 1)
|
||||
|
||||
fun backupInterval() = rxPrefs.getInteger(keys.backupInterval, 0)
|
||||
fun backupInterval() = rxPrefs.getInteger(Keys.backupInterval, 0)
|
||||
|
||||
fun removeAfterReadSlots() = prefs.getInt(keys.removeAfterReadSlots, -1)
|
||||
fun removeAfterReadSlots() = prefs.getInt(Keys.removeAfterReadSlots, -1)
|
||||
|
||||
fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
|
||||
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
|
||||
|
||||
fun libraryUpdateInterval() = rxPrefs.getInteger(keys.libraryUpdateInterval, 0)
|
||||
fun libraryUpdateInterval() = rxPrefs.getInteger(Keys.libraryUpdateInterval, 0)
|
||||
|
||||
fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet())
|
||||
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, emptySet())
|
||||
|
||||
fun libraryUpdateCategories() = rxPrefs.getStringSet(keys.libraryUpdateCategories, emptySet())
|
||||
fun libraryUpdateCategories() = rxPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
||||
|
||||
fun libraryAsList() = rxPrefs.getBoolean(keys.libraryAsList, false)
|
||||
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
|
||||
|
||||
fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false)
|
||||
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false)
|
||||
|
||||
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
|
||||
fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false)
|
||||
|
||||
fun librarySortingMode() = rxPrefs.getInteger(keys.librarySortingMode, 0)
|
||||
fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false)
|
||||
|
||||
fun filterCompleted() = rxPrefs.getBoolean(Keys.filterCompleted, false)
|
||||
|
||||
fun librarySortingMode() = rxPrefs.getInteger(Keys.librarySortingMode, 0)
|
||||
|
||||
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true)
|
||||
|
||||
fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false)
|
||||
fun automaticUpdates() = prefs.getBoolean(Keys.automaticUpdates, false)
|
||||
|
||||
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
|
||||
|
||||
fun downloadNew() = rxPrefs.getBoolean(keys.downloadNew, false)
|
||||
fun downloadNew() = rxPrefs.getBoolean(Keys.downloadNew, false)
|
||||
|
||||
fun downloadNewCategories() = rxPrefs.getStringSet(keys.downloadNewCategories, emptySet())
|
||||
fun downloadNewCategories() = rxPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
||||
|
||||
fun lang() = prefs.getString(keys.lang, "")
|
||||
fun lang() = prefs.getString(Keys.lang, "")
|
||||
|
||||
fun defaultCategory() = prefs.getInt(keys.defaultCategory, -1)
|
||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
|
||||
.map { response ->
|
||||
response.body().close()
|
||||
response.body()?.close()
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("Could not add manga")
|
||||
}
|
||||
@ -38,7 +38,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
|
||||
track.toAnilistScore())
|
||||
.map { response ->
|
||||
response.body().close()
|
||||
response.body()?.close()
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("Could not update manga")
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
||||
if (oauth == null || oauth!!.isExpired()) {
|
||||
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
||||
oauth = if (response.isSuccessful) {
|
||||
Gson().fromJson(response.body().string(), OAuth::class.java)
|
||||
Gson().fromJson(response.body()!!.string(), OAuth::class.java)
|
||||
} else {
|
||||
response.close()
|
||||
null
|
||||
|
@ -149,9 +149,8 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
|
||||
@GET("library-entries")
|
||||
fun findLibManga(
|
||||
@Query("filter[media_id]", encoded = true) remoteId: Int,
|
||||
@Query("filter[manga_id]", encoded = true) remoteId: Int,
|
||||
@Query("filter[user_id]", encoded = true) userId: String,
|
||||
@Query("page[limit]", encoded = true) limit: Int = 10000,
|
||||
@Query("include") includes: String = "manga"
|
||||
): Observable<JsonObject>
|
||||
|
||||
|
@ -22,7 +22,7 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
|
||||
if (currAuth.isExpired()) {
|
||||
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
||||
if (response.isSuccessful) {
|
||||
newAuth(gson.fromJson(response.body().string(), OAuth::class.java))
|
||||
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
|
||||
} else {
|
||||
response.close()
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
||||
} else {
|
||||
client.newCall(GET(getSearchUrl(query), headers))
|
||||
.asObservable()
|
||||
.map { Jsoup.parse(it.body().string()) }
|
||||
.map { Jsoup.parse(it.body()!!.string()) }
|
||||
.flatMap { Observable.from(it.select("entry")) }
|
||||
.filter { it.select("type").text() != "Novel" }
|
||||
.map {
|
||||
@ -64,7 +64,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
||||
return client
|
||||
.newCall(GET(getListUrl(username), headers))
|
||||
.asObservable()
|
||||
.map { Jsoup.parse(it.body().string()) }
|
||||
.map { Jsoup.parse(it.body()!!.string()) }
|
||||
.flatMap { Observable.from(it.select("manga")) }
|
||||
.map {
|
||||
Track.create(TrackManager.MYANIMELIST).apply {
|
||||
|
@ -6,8 +6,8 @@ import android.support.v4.app.NotificationCompat
|
||||
import com.evernote.android.job.Job
|
||||
import com.evernote.android.job.JobManager
|
||||
import com.evernote.android.job.JobRequest
|
||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
|
||||
class UpdateCheckerJob : Job() {
|
||||
@ -23,7 +23,7 @@ class UpdateCheckerJob : Job() {
|
||||
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
|
||||
}
|
||||
|
||||
NotificationCompat.Builder(context).update {
|
||||
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
setContentText(context.getString(R.string.update_check_notification_update_available))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
@ -43,7 +43,7 @@ class UpdateCheckerJob : Job() {
|
||||
|
||||
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
|
||||
block()
|
||||
context.notificationManager.notify(NOTIFICATION_UPDATER_ID, build())
|
||||
context.notificationManager.notify(Notifications.ID_UPDATER, build())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -54,7 +54,6 @@ class UpdateCheckerJob : Job() {
|
||||
.setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000)
|
||||
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
|
||||
.setRequirementsEnforced(true)
|
||||
.setPersisted(true)
|
||||
.setUpdateCurrent(true)
|
||||
.build()
|
||||
.schedule()
|
||||
|
@ -4,10 +4,10 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.Constants
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
import java.io.File
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
@ -49,7 +49,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
/**
|
||||
* Notification shown to user
|
||||
*/
|
||||
private val notification = NotificationCompat.Builder(context)
|
||||
private val notification = NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON)
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.getStringExtra(EXTRA_ACTION)) {
|
||||
@ -82,6 +82,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
private fun updateProgress(progress: Int) {
|
||||
with(notification) {
|
||||
setProgress(100, progress, false)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
notification.show()
|
||||
}
|
||||
@ -96,6 +97,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
with(notification) {
|
||||
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setOnlyAlertOnce(false)
|
||||
setProgress(0, 0, false)
|
||||
// Install action
|
||||
setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path)))
|
||||
@ -105,7 +107,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
// Cancel action
|
||||
addAction(R.drawable.ic_clear_grey_24dp_img,
|
||||
context.getString(R.string.action_cancel),
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID))
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
|
||||
}
|
||||
notification.show()
|
||||
}
|
||||
@ -120,6 +122,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
with(notification) {
|
||||
setContentText(context.getString(R.string.update_check_notification_download_error))
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
setOnlyAlertOnce(false)
|
||||
setProgress(0, 0, false)
|
||||
// Retry action
|
||||
addAction(R.drawable.ic_refresh_grey_24dp_img,
|
||||
@ -128,7 +131,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
// Cancel action
|
||||
addAction(R.drawable.ic_clear_grey_24dp_img,
|
||||
context.getString(R.string.action_cancel),
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID))
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
|
||||
}
|
||||
notification.show()
|
||||
}
|
||||
@ -138,7 +141,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
*
|
||||
* @param id the id of the notification.
|
||||
*/
|
||||
private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_UPDATER_ID) {
|
||||
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) {
|
||||
context.notificationManager.notify(id, build())
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
||||
val apkFile = File(externalCacheDir, "update.apk")
|
||||
|
||||
if (response.isSuccessful) {
|
||||
response.body().source().saveTo(apkFile)
|
||||
response.body()!!.source().saveTo(apkFile)
|
||||
} else {
|
||||
response.close()
|
||||
throw Exception("Unsuccessful response")
|
||||
|
@ -8,13 +8,10 @@ import okhttp3.Response
|
||||
|
||||
class CloudflareInterceptor : Interceptor {
|
||||
|
||||
//language=RegExp
|
||||
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
||||
|
||||
//language=RegExp
|
||||
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
||||
|
||||
//language=RegExp
|
||||
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
||||
|
||||
@Synchronized
|
||||
@ -34,7 +31,7 @@ class CloudflareInterceptor : Interceptor {
|
||||
val originalRequest = response.request()
|
||||
val url = originalRequest.url()
|
||||
val domain = url.host()
|
||||
val content = response.body().string()
|
||||
val content = response.body()!!.string()
|
||||
|
||||
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
||||
Thread.sleep(4000)
|
||||
@ -48,9 +45,7 @@ class CloudflareInterceptor : Interceptor {
|
||||
}
|
||||
|
||||
val js = operation
|
||||
//language=RegExp
|
||||
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
|
||||
//language=RegExp
|
||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
||||
.replace("\n", "")
|
||||
|
||||
@ -58,7 +53,7 @@ class CloudflareInterceptor : Interceptor {
|
||||
|
||||
val answer = "${result + domain.length}"
|
||||
|
||||
val cloudflareUrl = HttpUrl.parse("${url.scheme()}://$domain/cdn-cgi/l/chk_jschl")
|
||||
val cloudflareUrl = HttpUrl.parse("${url.scheme()}://$domain/cdn-cgi/l/chk_jschl")!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("jschl_vc", challenge)
|
||||
.addQueryParameter("pass", pass)
|
||||
|
@ -61,7 +61,7 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
.body(ProgressResponseBody(originalResponse.body(), listener))
|
||||
.body(ProgressResponseBody(originalResponse.body()!!, listener))
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
|
@ -17,8 +17,8 @@ class PersistentCookieStore(context: Context) {
|
||||
val cookies = value as? Set<String>
|
||||
if (cookies != null) {
|
||||
try {
|
||||
val url = HttpUrl.parse("http://$key")
|
||||
val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
|
||||
val url = HttpUrl.parse("http://$key") ?: continue
|
||||
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
|
||||
.filter { !it.hasExpired() }
|
||||
cookieMap.put(key, nonExpiredCookies)
|
||||
} catch (e: Exception) {
|
||||
|
@ -12,7 +12,7 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
|
||||
}
|
||||
|
||||
override fun contentType(): MediaType {
|
||||
return responseBody.contentType()
|
||||
return responseBody.contentType()!!
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
|
@ -56,8 +56,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
}
|
||||
|
||||
override val id = ID
|
||||
override val name = "LocalSource"
|
||||
override val lang = "en"
|
||||
override val name = context.getString(R.string.local_source)
|
||||
override val lang = ""
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun toString() = context.getString(R.string.local_source)
|
||||
@ -76,13 +76,13 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
||||
when (state?.index) {
|
||||
0 -> {
|
||||
if (state!!.ascending)
|
||||
if (state.ascending)
|
||||
mangaDirs = mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
|
||||
else
|
||||
mangaDirs = mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
|
||||
}
|
||||
1 -> {
|
||||
if (state!!.ascending)
|
||||
if (state.ascending)
|
||||
mangaDirs = mangaDirs.sortedBy(File::lastModified)
|
||||
else
|
||||
mangaDirs = mangaDirs.sortedByDescending(File::lastModified)
|
||||
@ -144,7 +144,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
} else {
|
||||
chapterFile.nameWithoutExtension
|
||||
}
|
||||
val chapNameCut = chapName.replace(manga.title, "", true).trim()
|
||||
val chapNameCut = chapName.replace(manga.title, "", true).trim(' ', '-', '_')
|
||||
name = if (chapNameCut.isEmpty()) chapName else chapNameCut
|
||||
date_upload = chapterFile.lastModified()
|
||||
ChapterRecognition.parseChapterNumber(this, manga)
|
||||
|
@ -0,0 +1,12 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
/**
|
||||
* A factory for creating sources at runtime.
|
||||
*/
|
||||
interface SourceFactory {
|
||||
/**
|
||||
* Create a new copy of the sources
|
||||
* @return The created sources
|
||||
*/
|
||||
fun createSources(): List<Source>
|
||||
}
|
@ -74,7 +74,7 @@ open class SourceManager(private val context: Context) {
|
||||
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
|
||||
sources.add(YamlHttpSource(map))
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Error loading source from file. Bad format?")
|
||||
Timber.e("Error loading source from file. Bad format?", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,25 +95,29 @@ open class SourceManager(private val context: Context) {
|
||||
val extName = pkgManager.getApplicationLabel(appInfo).toString()
|
||||
.substringAfter("Tachiyomi: ")
|
||||
val version = pkgInfo.versionName
|
||||
var sourceClass = appInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
||||
if (sourceClass.startsWith(".")) {
|
||||
sourceClass = pkgInfo.packageName + sourceClass
|
||||
}
|
||||
val sourceClasses = appInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
||||
.split(";")
|
||||
.map {
|
||||
val sourceClass = it.trim()
|
||||
if(sourceClass.startsWith("."))
|
||||
pkgInfo.packageName + sourceClass
|
||||
else
|
||||
sourceClass
|
||||
}
|
||||
|
||||
val extension = Extension(extName, appInfo, version, sourceClass)
|
||||
val extension = Extension(extName, appInfo, version, sourceClasses)
|
||||
try {
|
||||
val instance = loadExtension(extension)
|
||||
sources.add(instance)
|
||||
sources += loadExtension(extension)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Extension load error: $extName. Reason: ${e.message}")
|
||||
Timber.e("Extension load error: $extName.", e)
|
||||
} catch (e: LinkageError) {
|
||||
Timber.e("Extension load error: $extName. Reason: ${e.message}")
|
||||
Timber.e("Extension load error: $extName.", e)
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
private fun loadExtension(ext: Extension): Source {
|
||||
private fun loadExtension(ext: Extension): List<Source> {
|
||||
// Validate lib version
|
||||
val majorLibVersion = ext.version.substringBefore('.').toInt()
|
||||
if (majorLibVersion < LIB_VERSION_MIN || majorLibVersion > LIB_VERSION_MAX) {
|
||||
@ -122,13 +126,20 @@ open class SourceManager(private val context: Context) {
|
||||
}
|
||||
|
||||
val classLoader = PathClassLoader(ext.appInfo.sourceDir, null, context.classLoader)
|
||||
return Class.forName(ext.sourceClass, false, classLoader).newInstance() as Source
|
||||
return ext.sourceClasses.flatMap {
|
||||
val obj = Class.forName(it, false, classLoader).newInstance()
|
||||
when(obj) {
|
||||
is Source -> listOf(obj)
|
||||
is SourceFactory -> obj.createSources()
|
||||
else -> throw Exception("Unknown source class type!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Extension(val name: String,
|
||||
val appInfo: ApplicationInfo,
|
||||
val version: String,
|
||||
val sourceClass: String)
|
||||
val sourceClasses: List<String>)
|
||||
|
||||
private companion object {
|
||||
const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||
|
@ -28,7 +28,11 @@ class Page(
|
||||
@Transient private var statusSubject: Subject<Int, Int>? = null
|
||||
|
||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
progress = (100 * bytesRead / contentLength).toInt()
|
||||
progress = if (contentLength > 0) {
|
||||
(100 * bytesRead / contentLength).toInt()
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
fun setStatusSubject(subject: Subject<Int, Int>?) {
|
||||
|
@ -12,11 +12,14 @@ interface SChapter : Serializable {
|
||||
|
||||
var chapter_number: Float
|
||||
|
||||
var scanlator: String?
|
||||
|
||||
fun copyFrom(other: SChapter) {
|
||||
name = other.name
|
||||
url = other.url
|
||||
date_upload = other.date_upload
|
||||
chapter_number = other.chapter_number
|
||||
scanlator = other.scanlator
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -10,4 +10,6 @@ class SChapterImpl : SChapter {
|
||||
|
||||
override var chapter_number: Float = -1f
|
||||
|
||||
override var scanlator: String? = null
|
||||
|
||||
}
|
@ -13,6 +13,7 @@ import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.lang.Exception
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.security.MessageDigest
|
||||
@ -51,7 +52,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
override val id by lazy {
|
||||
val key = "${name.toLowerCase()}/$lang/$versionId"
|
||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||
(0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||
}
|
||||
|
||||
/**
|
||||
@ -197,16 +198,20 @@ abstract class HttpSource : CatalogueSource {
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
* override this method. If a manga is licensed an empty chapter list observable is returned
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response)
|
||||
}
|
||||
if (manga.status != SManga.LICENSED) {
|
||||
return client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response)
|
||||
}
|
||||
} else {
|
||||
return Observable.error(Exception("Licensed - No chapters to show"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -171,7 +171,7 @@ class YamlHttpSource(mappings: Map<*, *>) : HttpSource() {
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val body = response.body().string()
|
||||
val body = response.body()!!.string()
|
||||
val url = response.request().url().toString()
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
@ -216,7 +216,7 @@ class YamlHttpSource(mappings: Map<*, *>) : HttpSource() {
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
val body = response.body().string()
|
||||
val body = response.body()!!.string()
|
||||
val url = response.request().url().toString()
|
||||
|
||||
with(map.pages) {
|
||||
|
@ -28,7 +28,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
||||
|
||||
override val name = "Batoto"
|
||||
|
||||
override val baseUrl = "http://bato.to"
|
||||
override val baseUrl = "https://bato.to"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
@ -52,7 +52,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
||||
.add("Cookie", "lang_option=English")
|
||||
|
||||
private val pageHeaders = super.headersBuilder()
|
||||
.add("Referer", "http://bato.to/reader")
|
||||
.add("Referer", "$baseUrl/reader")
|
||||
.build()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
@ -69,7 +69,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a[href^=http://bato.to]").first().let {
|
||||
element.select("a[href*=bato.to]").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text().trim()
|
||||
}
|
||||
@ -85,7 +85,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
||||
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder()
|
||||
val url = HttpUrl.parse("$baseUrl/search_ajax")!!.newBuilder()
|
||||
if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
|
||||
var genres = ""
|
||||
filters.forEach { filter ->
|
||||
@ -161,8 +161,20 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
// Https is currently very slow. The replace also saves a redirection.
|
||||
var newUrl = "http://bato.to" + manga.url
|
||||
if ("/comic/_/comics/" !in newUrl) {
|
||||
newUrl = newUrl.replace("/comic/_/", "/comic/_/comics/")
|
||||
}
|
||||
|
||||
return super.chapterListRequest(manga).newBuilder()
|
||||
.url(newUrl)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val body = response.body().string()
|
||||
val body = response.body()!!.string()
|
||||
val matcher = staffNotice.matcher(body)
|
||||
if (matcher.find()) {
|
||||
@Suppress("DEPRECATION")
|
||||
@ -177,7 +189,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
||||
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a[href^=http://bato.to/reader").first()
|
||||
val urlElement = element.select("a[href*=bato.to/reader").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
@ -185,6 +197,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
||||
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
||||
parseDateFromElement(it)
|
||||
} ?: 0
|
||||
chapter.scanlator = element.select("td").getOrNull(2)?.text()
|
||||
return chapter
|
||||
}
|
||||
|
||||
@ -271,7 +284,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
||||
}
|
||||
|
||||
override fun isAuthenticationSuccessful(response: Response) =
|
||||
response.priorResponse() != null && response.priorResponse().code() == 302
|
||||
response.priorResponse() != null && response.priorResponse()!!.code() == 302
|
||||
|
||||
override fun isLogged(): Boolean {
|
||||
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
||||
|
@ -11,6 +11,7 @@ import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import timber.log.Timber
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@ -44,7 +45,33 @@ class Kissmanga : ParsedHttpSource() {
|
||||
val manga = SManga.create()
|
||||
element.select("td a:eq(0)").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
val title = it.text()
|
||||
//check if cloudfire email obfuscation is affecting title name
|
||||
if (title.contains("[email protected]", true)) {
|
||||
try {
|
||||
var str: String = it.html()
|
||||
//get the number
|
||||
str = str.substringAfter("data-cfemail=\"")
|
||||
str = str.substringBefore("\">[email")
|
||||
val sb = StringBuilder()
|
||||
//convert number to char
|
||||
val r = Integer.valueOf(str.substring(0, 2), 16)!!
|
||||
var i = 2
|
||||
while (i < str.length) {
|
||||
val c = (Integer.valueOf(str.substring(i, i + 2), 16) xor r).toChar()
|
||||
sb.append(c)
|
||||
i += 2
|
||||
}
|
||||
//replace the new word into the title
|
||||
manga.title = title.replace("[email protected]", sb.toString(), true)
|
||||
} catch (e: Exception) {
|
||||
//on error just default to obfuscated title
|
||||
Timber.e("error parsing [email protected]", e)
|
||||
manga.title = title
|
||||
}
|
||||
} else {
|
||||
manga.title = title
|
||||
}
|
||||
}
|
||||
return manga
|
||||
}
|
||||
@ -115,13 +142,13 @@ class Kissmanga : ParsedHttpSource() {
|
||||
override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val body = response.body().string()
|
||||
val body = response.body()!!.string()
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
// Kissmanga now encrypts the urls, so we need to execute these two scripts in JS.
|
||||
val ca = client.newCall(GET("$baseUrl/Scripts/ca.js", headers)).execute().body().string()
|
||||
val lo = client.newCall(GET("$baseUrl/Scripts/lo.js", headers)).execute().body().string()
|
||||
val ca = client.newCall(GET("$baseUrl/Scripts/ca.js", headers)).execute().body()!!.string()
|
||||
val lo = client.newCall(GET("$baseUrl/Scripts/lo.js", headers)).execute().body()!!.string()
|
||||
|
||||
Duktape.create().use {
|
||||
it.evaluate(ca)
|
||||
@ -199,6 +226,7 @@ class Kissmanga : ParsedHttpSource() {
|
||||
Genre("Mystery"),
|
||||
Genre("One shot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Reincarnation"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
@ -212,7 +240,9 @@ class Kissmanga : ParsedHttpSource() {
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Time Travel"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Transported"),
|
||||
Genre("Webtoon"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
|
@ -55,13 +55,13 @@ class Mangafox : ParsedHttpSource() {
|
||||
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
|
||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1")!!.newBuilder().addQueryParameter("name", query)
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> url.addQueryParameter(filter.id, filter.state.toString())
|
||||
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
|
||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||
is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString())
|
||||
is Type -> url.addQueryParameter("type", if (filter.state == 0) "" else filter.state.toString())
|
||||
is OrderBy -> {
|
||||
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
|
||||
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
|
||||
@ -89,13 +89,20 @@ class Mangafox : ParsedHttpSource() {
|
||||
val infoElement = document.select("div#title").first()
|
||||
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
|
||||
val sideInfoElement = document.select("#series_info").first()
|
||||
val licensedElement = document.select("div.warning").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = rowElement.select("td:eq(1)").first()?.text()
|
||||
manga.artist = rowElement.select("td:eq(2)").first()?.text()
|
||||
manga.genre = rowElement.select("td:eq(3)").first()?.text()
|
||||
manga.description = infoElement.select("p.summary").first()?.text()
|
||||
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
val isLicensed = licensedElement?.text()?.contains("licensed")
|
||||
if (isLicensed == true) {
|
||||
manga.status = SManga.LICENSED
|
||||
} else {
|
||||
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
}
|
||||
|
||||
manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
|
||||
return manga
|
||||
}
|
||||
@ -113,7 +120,7 @@ class Mangafox : ParsedHttpSource() {
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.name = element.select("span.title.nowrap").first()?.text()?.let { urlElement.text() + " - " + it } ?: urlElement.text()
|
||||
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
@ -169,6 +176,7 @@ class Mangafox : ParsedHttpSource() {
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||
Filter.Sort.Selection(2, false))
|
||||
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
|
@ -7,9 +7,13 @@ import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
class Mangahere : ParsedHttpSource() {
|
||||
|
||||
@ -23,6 +27,26 @@ class Mangahere : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val trustManager = object : X509TrustManager {
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> {
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
}
|
||||
}
|
||||
|
||||
private val sslContext = SSLContext.getInstance("SSL").apply {
|
||||
init(null, arrayOf(trustManager), SecureRandom())
|
||||
}
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
.sslSocketFactory(sslContext.socketFactory, trustManager)
|
||||
.build()
|
||||
|
||||
override fun popularMangaSelector() = "div.directory_list > ul > li"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
|
||||
@ -57,7 +81,7 @@ class Mangahere : ParsedHttpSource() {
|
||||
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
|
||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1")!!.newBuilder().addQueryParameter("name", query)
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
|
||||
@ -87,8 +111,8 @@ class Mangahere : ParsedHttpSource() {
|
||||
val infoElement = detailElement.select(".detail_topText").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text()
|
||||
manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text()
|
||||
manga.author = infoElement.select("a[href^=//www.mangahere.co/author/]").first()?.text()
|
||||
manga.artist = infoElement.select("a[href^=//www.mangahere.co/artist/]").first()?.text()
|
||||
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
|
||||
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
|
||||
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
@ -159,7 +183,9 @@ class Mangahere : ParsedHttpSource() {
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
|
||||
pages.add(Page(pages.size, it.attr("value")))
|
||||
if (!it.attr("value").contains("featured.html")) {
|
||||
pages.add(Page(pages.size, "http:" + it.attr("value")))
|
||||
}
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
return pages
|
||||
@ -174,6 +200,7 @@ class Mangahere : ParsedHttpSource() {
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||
Filter.Sort.Selection(2, false))
|
||||
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
|
@ -54,7 +54,7 @@ class Mangasee : ParsedHttpSource() {
|
||||
override fun searchMangaSelector() = "div.requested > div.row"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder()
|
||||
val url = HttpUrl.parse("$baseUrl/search/request.php")!!.newBuilder()
|
||||
if (!query.isEmpty()) url.addQueryParameter("keyword", query)
|
||||
val genres = mutableListOf<String>()
|
||||
val genresNo = mutableListOf<String>()
|
||||
@ -84,7 +84,7 @@ class Mangasee : ParsedHttpSource() {
|
||||
}
|
||||
|
||||
private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
|
||||
val url = HttpUrl.parse(url)
|
||||
val url = HttpUrl.parse(url)!!
|
||||
val body = FormBody.Builder().add("page", page.toString())
|
||||
for (i in 0..url.querySize() - 1) {
|
||||
body.add(url.queryParameterName(i), url.queryParameterValue(i))
|
||||
|
@ -17,7 +17,7 @@ class Readmangatoday : ParsedHttpSource() {
|
||||
|
||||
override val name = "ReadMangaToday"
|
||||
|
||||
override val baseUrl = "http://www.readmanga.today"
|
||||
override val baseUrl = "http://www.readmng.com/"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
@ -161,7 +161,7 @@ class Readmangatoday : ParsedHttpSource() {
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
|
||||
override fun imageUrlParse(document: Document) = document.select("#chapter_img").first().attr("src")
|
||||
|
||||
private class Status : Filter.TriState("Completed")
|
||||
private class Genre(name: String, val id: Int) : Filter.TriState(name)
|
||||
|
@ -23,9 +23,8 @@ class Mangachan : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
|
||||
}
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
var pageNum = 1
|
||||
@ -48,9 +47,7 @@ class Mangachan : ParsedHttpSource() {
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/newestch?page=$page")
|
||||
}
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/newestch?page=$page")
|
||||
|
||||
override fun popularMangaSelector() = "div.content_row"
|
||||
|
||||
@ -76,9 +73,7 @@ class Mangachan : ParsedHttpSource() {
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
||||
|
||||
@ -125,16 +120,14 @@ class Mangachan : ParsedHttpSource() {
|
||||
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
|
||||
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
|
||||
manga.description = descElement.textNodes().first().text()
|
||||
manga.thumbnail_url = baseUrl + imgElement.attr("src")
|
||||
manga.thumbnail_url = imgElement.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int {
|
||||
when {
|
||||
element.contains("перевод завершен") -> return SManga.COMPLETED
|
||||
element.contains("перевод продолжается") -> return SManga.ONGOING
|
||||
else -> return SManga.UNKNOWN
|
||||
}
|
||||
private fun parseStatus(element: String): Int = when {
|
||||
element.contains("перевод завершен") -> SManga.COMPLETED
|
||||
element.contains("перевод продолжается") -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
|
||||
@ -152,7 +145,7 @@ class Mangachan : ParsedHttpSource() {
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body().string()
|
||||
val html = response.body()!!.string()
|
||||
val beginIndex = html.indexOf("fullimg\":[") + 10
|
||||
val endIndex = html.indexOf(",]", beginIndex)
|
||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
|
||||
|
@ -23,13 +23,11 @@ class Mintmanga : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun popularMangaSelector() = "div.desc"
|
||||
|
||||
@ -44,24 +42,21 @@ class Mintmanga : ParsedHttpSource() {
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
|
||||
return GET("$baseUrl/search?q=$query&$genres", headers)
|
||||
val genres = filters.filterIsInstance<Genre>().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] }
|
||||
return GET("$baseUrl/search/advanced?q=$query&$genres", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
@ -78,13 +73,11 @@ class Mintmanga : ParsedHttpSource() {
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int {
|
||||
when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
|
||||
else -> return SManga.UNKNOWN
|
||||
}
|
||||
private fun parseStatus(element: String): Int = when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
||||
@ -120,7 +113,7 @@ class Mintmanga : ParsedHttpSource() {
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body().string()
|
||||
val html = response.body()!!.string()
|
||||
val beginIndex = html.indexOf("rm_h.init( [")
|
||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||
@ -149,7 +142,7 @@ class Mintmanga : ParsedHttpSource() {
|
||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
||||
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||
* on http://mintmanga.com/search
|
||||
* on http://mintmanga.com/search/advanced
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("арт", "el_2220"),
|
||||
@ -171,6 +164,7 @@ class Mintmanga : ParsedHttpSource() {
|
||||
Genre("меха", "el_1318"),
|
||||
Genre("мистика", "el_1324"),
|
||||
Genre("научная фантастика", "el_1325"),
|
||||
Genre("омегаверс", "el_5676"),
|
||||
Genre("повседневность", "el_1327"),
|
||||
Genre("постапокалиптика", "el_1342"),
|
||||
Genre("приключения", "el_1322"),
|
||||
|
@ -27,13 +27,11 @@ class Readmanga : ParsedHttpSource() {
|
||||
|
||||
override fun latestUpdatesSelector() = "div.desc"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
@ -44,24 +42,21 @@ class Readmanga : ParsedHttpSource() {
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
|
||||
return GET("$baseUrl/search?q=$query&$genres", headers)
|
||||
val genres = filters.filterIsInstance<Genre>().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] }
|
||||
return GET("$baseUrl/search/advanced?q=$query&$genres", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
@ -78,13 +73,11 @@ class Readmanga : ParsedHttpSource() {
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int {
|
||||
when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
|
||||
else -> return SManga.UNKNOWN
|
||||
}
|
||||
private fun parseStatus(element: String): Int = when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
||||
@ -120,7 +113,7 @@ class Readmanga : ParsedHttpSource() {
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body().string()
|
||||
val html = response.body()!!.string()
|
||||
val beginIndex = html.indexOf("rm_h.init( [")
|
||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||
@ -149,7 +142,7 @@ class Readmanga : ParsedHttpSource() {
|
||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
||||
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||
* on http://readmanga.me/search
|
||||
* on http://readmanga.me/search/advanced
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("арт", "el_5685"),
|
||||
|
@ -1,83 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.base.activity
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.ActionBar
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
interface ActivityMixin {
|
||||
|
||||
var resumed: Boolean
|
||||
|
||||
fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
|
||||
setSupportActionBar(toolbar)
|
||||
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
|
||||
if (backNavigation) {
|
||||
toolbar.setNavigationOnClickListener {
|
||||
if (resumed) {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAppTheme() {
|
||||
setTheme(when (Injekt.get<PreferencesHelper>().theme()) {
|
||||
2 -> R.style.Theme_Tachiyomi_Dark
|
||||
else -> R.style.Theme_Tachiyomi
|
||||
})
|
||||
}
|
||||
|
||||
fun setToolbarTitle(title: String) {
|
||||
getSupportActionBar()?.title = title
|
||||
}
|
||||
|
||||
fun setToolbarTitle(titleResource: Int) {
|
||||
getSupportActionBar()?.title = getString(titleResource)
|
||||
}
|
||||
|
||||
fun setToolbarSubtitle(title: String) {
|
||||
getSupportActionBar()?.subtitle = title
|
||||
}
|
||||
|
||||
fun setToolbarSubtitle(titleResource: Int) {
|
||||
getSupportActionBar()?.subtitle = getString(titleResource)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests read and write permissions on Android M and higher.
|
||||
*/
|
||||
fun requestPermissionsOnMarshmallow() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (ContextCompat.checkSelfPermission(getActivity(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
ActivityCompat.requestPermissions(getActivity(),
|
||||
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
|
||||
1)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getActivity(): AppCompatActivity
|
||||
|
||||
fun onBackPressed()
|
||||
|
||||
fun getSupportActionBar(): ActionBar?
|
||||
|
||||
fun setSupportActionBar(toolbar: Toolbar?)
|
||||
|
||||
fun setTheme(resource: Int)
|
||||
|
||||
fun getString(resource: Int): String
|
||||
|
||||
}
|
@ -3,24 +3,11 @@ package eu.kanade.tachiyomi.ui.base.activity
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
|
||||
|
||||
override var resumed = false
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
LocaleHelper.updateConfiguration(this)
|
||||
}
|
||||
|
||||
override fun getActivity() = this
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
resumed = true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
resumed = false
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,40 +1,14 @@
|
||||
package eu.kanade.tachiyomi.ui.base.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.App
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
import nucleus.view.NucleusAppCompatActivity
|
||||
|
||||
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>(), ActivityMixin {
|
||||
|
||||
override var resumed = false
|
||||
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>() {
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
LocaleHelper.updateConfiguration(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
val superFactory = presenterFactory
|
||||
setPresenterFactory {
|
||||
superFactory.createPresenter().apply {
|
||||
val app = application as App
|
||||
context = app.applicationContext
|
||||
}
|
||||
}
|
||||
super.onCreate(savedState)
|
||||
}
|
||||
|
||||
override fun getActivity() = this
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
resumed = true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
resumed = false
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.base.adapter
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
|
||||
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
||||
|
||||
abstract class FlexibleViewHolder(view: View,
|
||||
private val adapter: FlexibleAdapter<*, *>,
|
||||
private val itemClickListener: FlexibleViewHolder.OnListItemClickListener) :
|
||||
RecyclerView.ViewHolder(view), View.OnClickListener, View.OnLongClickListener {
|
||||
|
||||
init {
|
||||
view.setOnClickListener(this)
|
||||
view.setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
override fun onClick(view: View) {
|
||||
if (itemClickListener.onListItemClick(adapterPosition)) {
|
||||
toggleActivation()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(view: View): Boolean {
|
||||
itemClickListener.onListItemLongClick(adapterPosition)
|
||||
toggleActivation()
|
||||
return true
|
||||
}
|
||||
|
||||
fun toggleActivation() {
|
||||
itemView.isActivated = adapter.isSelected(adapterPosition)
|
||||
}
|
||||
|
||||
interface OnListItemClickListener {
|
||||
fun onListItemClick(position: Int): Boolean
|
||||
fun onListItemLongClick(position: Int)
|
||||
}
|
||||
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.base.adapter
|
||||
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v4.app.FragmentStatePagerAdapter
|
||||
import android.util.SparseArray
|
||||
import android.view.ViewGroup
|
||||
import java.util.*
|
||||
|
||||
abstract class SmartFragmentStatePagerAdapter(fragmentManager: FragmentManager) :
|
||||
FragmentStatePagerAdapter(fragmentManager) {
|
||||
// Sparse array to keep track of registered fragments in memory
|
||||
private val registeredFragments = SparseArray<Fragment>()
|
||||
|
||||
// Register the fragment when the item is instantiated
|
||||
override fun instantiateItem(container: ViewGroup, position: Int): Any {
|
||||
val fragment = super.instantiateItem(container, position) as Fragment
|
||||
registeredFragments.put(position, fragment)
|
||||
return fragment
|
||||
}
|
||||
|
||||
// Unregister when the item is inactive
|
||||
override fun destroyItem(container: ViewGroup?, position: Int, `object`: Any) {
|
||||
registeredFragments.remove(position)
|
||||
super.destroyItem(container, position, `object`)
|
||||
}
|
||||
|
||||
// Returns the fragment for the position (if instantiated)
|
||||
fun getRegisteredFragment(position: Int): Fragment {
|
||||
return registeredFragments.get(position)
|
||||
}
|
||||
|
||||
fun getRegisteredFragments(): List<Fragment> {
|
||||
val fragments = ArrayList<Fragment>()
|
||||
for (i in 0..registeredFragments.size() - 1) {
|
||||
fragments.add(registeredFragments.valueAt(i))
|
||||
}
|
||||
return fragments
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.RestoreViewOnCreateController
|
||||
|
||||
abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle) {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
|
||||
val view = inflateView(inflater, container)
|
||||
onViewCreated(view, savedViewState)
|
||||
return view
|
||||
}
|
||||
|
||||
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
|
||||
|
||||
open fun onViewCreated(view: View, savedViewState: Bundle?) { }
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
if (type.isEnter) {
|
||||
setTitle()
|
||||
}
|
||||
super.onChangeStarted(handler, type)
|
||||
}
|
||||
|
||||
open fun getTitle(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun setTitle() {
|
||||
var parentController = parentController
|
||||
while (parentController != null) {
|
||||
if (parentController is BaseController && parentController.getTitle() != null) {
|
||||
return
|
||||
}
|
||||
parentController = parentController.parentController
|
||||
}
|
||||
|
||||
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for disappearing menu items when collapsing an expandable item like a SearchView.
|
||||
* This method should be removed when fixed upstream.
|
||||
* Issue link: https://issuetracker.google.com/issues/37657375
|
||||
*/
|
||||
fun MenuItem.fixExpand() {
|
||||
setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
activity?.invalidateOptionsMenu()
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.os.Build
|
||||
import android.support.v4.content.ContextCompat
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.Router
|
||||
|
||||
fun Router.popControllerWithTag(tag: String): Boolean {
|
||||
val controller = getControllerWithTag(tag)
|
||||
if (controller != null) {
|
||||
popController(controller)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: Int) {
|
||||
val activity = activity ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
permissions.forEach { permission ->
|
||||
if (ContextCompat.checkSelfPermission(activity, permission) != PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(permission), requestCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
|
||||
import com.bluelinelabs.conductor.Router;
|
||||
import com.bluelinelabs.conductor.RouterTransaction;
|
||||
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
|
||||
|
||||
/**
|
||||
* A controller that displays a dialog window, floating on top of its activity's window.
|
||||
* This is a wrapper over {@link Dialog} object like {@link android.app.DialogFragment}.
|
||||
*
|
||||
* <p>Implementations should override this class and implement {@link #onCreateDialog(Bundle)} to create a custom dialog, such as an {@link android.app.AlertDialog}
|
||||
*/
|
||||
public abstract class DialogController extends RestoreViewOnCreateController {
|
||||
|
||||
private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState";
|
||||
|
||||
private Dialog dialog;
|
||||
private boolean dismissed;
|
||||
|
||||
/**
|
||||
* Convenience constructor for use when no arguments are needed.
|
||||
*/
|
||||
protected DialogController() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor that takes arguments that need to be retained across restarts.
|
||||
*
|
||||
* @param args Any arguments that need to be retained.
|
||||
*/
|
||||
protected DialogController(@Nullable Bundle args) {
|
||||
super(args);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
final protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
|
||||
dialog = onCreateDialog(savedViewState);
|
||||
//noinspection ConstantConditions
|
||||
dialog.setOwnerActivity(getActivity());
|
||||
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
dismissDialog();
|
||||
}
|
||||
});
|
||||
if (savedViewState != null) {
|
||||
Bundle dialogState = savedViewState.getBundle(SAVED_DIALOG_STATE_TAG);
|
||||
if (dialogState != null) {
|
||||
dialog.onRestoreInstanceState(dialogState);
|
||||
}
|
||||
}
|
||||
return new View(getActivity());//stub view
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) {
|
||||
super.onSaveViewState(view, outState);
|
||||
Bundle dialogState = dialog.onSaveInstanceState();
|
||||
outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttach(@NonNull View view) {
|
||||
super.onAttach(view);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetach(@NonNull View view) {
|
||||
super.onDetach(view);
|
||||
dialog.hide();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroyView(@NonNull View view) {
|
||||
super.onDestroyView(view);
|
||||
dialog.setOnDismissListener(null);
|
||||
dialog.dismiss();
|
||||
dialog = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the dialog, create a transaction and pushing the controller.
|
||||
* @param router The router on which the transaction will be applied
|
||||
*/
|
||||
public void showDialog(@NonNull Router router) {
|
||||
showDialog(router, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the dialog, create a transaction and pushing the controller.
|
||||
* @param router The router on which the transaction will be applied
|
||||
* @param tag The tag for this controller
|
||||
*/
|
||||
public void showDialog(@NonNull Router router, @Nullable String tag) {
|
||||
dismissed = false;
|
||||
router.pushController(RouterTransaction.with(this)
|
||||
.pushChangeHandler(new SimpleSwapChangeHandler(false))
|
||||
.popChangeHandler(new SimpleSwapChangeHandler(false))
|
||||
.tag(tag));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the dialog and pop this controller
|
||||
*/
|
||||
public void dismissDialog() {
|
||||
if (dismissed) {
|
||||
return;
|
||||
}
|
||||
getRouter().popController(this);
|
||||
dismissed = true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected Dialog getDialog() {
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build your own custom Dialog container such as an {@link android.app.AlertDialog}
|
||||
*
|
||||
* @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)} or {@code null} if no saved state exists.
|
||||
* @return Return a new Dialog instance to be displayed by the Controller
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract Dialog onCreateDialog(@Nullable Bundle savedViewState);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
interface NoToolbarElevationController
|
@ -0,0 +1,21 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorDelegate
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorLifecycleListener
|
||||
import nucleus.factory.PresenterFactory
|
||||
import nucleus.presenter.Presenter
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(bundle),
|
||||
PresenterFactory<P> {
|
||||
|
||||
private val delegate = NucleusConductorDelegate(this)
|
||||
|
||||
val presenter: P
|
||||
get() = delegate.presenter
|
||||
|
||||
init {
|
||||
addLifecycleListener(NucleusConductorLifecycleListener(delegate))
|
||||
}
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.view.PagerAdapter;
|
||||
import android.util.SparseArray;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bluelinelabs.conductor.Controller;
|
||||
import com.bluelinelabs.conductor.Router;
|
||||
import com.bluelinelabs.conductor.RouterTransaction;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An adapter for ViewPagers that uses Routers as pages
|
||||
*/
|
||||
public abstract class RouterPagerAdapter extends PagerAdapter {
|
||||
|
||||
private static final String KEY_SAVED_PAGES = "RouterPagerAdapter.savedStates";
|
||||
private static final String KEY_MAX_PAGES_TO_STATE_SAVE = "RouterPagerAdapter.maxPagesToStateSave";
|
||||
private static final String KEY_SAVE_PAGE_HISTORY = "RouterPagerAdapter.savedPageHistory";
|
||||
|
||||
private final Controller host;
|
||||
private int maxPagesToStateSave = Integer.MAX_VALUE;
|
||||
private SparseArray<Bundle> savedPages = new SparseArray<>();
|
||||
private SparseArray<Router> visibleRouters = new SparseArray<>();
|
||||
private ArrayList<Integer> savedPageHistory = new ArrayList<>();
|
||||
private Router primaryRouter;
|
||||
|
||||
/**
|
||||
* Creates a new RouterPagerAdapter using the passed host.
|
||||
*/
|
||||
public RouterPagerAdapter(@NonNull Controller host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a router is instantiated. Here the router's root should be set if needed.
|
||||
*
|
||||
* @param router The router used for the page
|
||||
* @param position The page position to be instantiated.
|
||||
*/
|
||||
public abstract void configureRouter(@NonNull Router router, int position);
|
||||
|
||||
/**
|
||||
* Sets the maximum number of pages that will have their states saved. When this number is exceeded,
|
||||
* the page that was state saved least recently will have its state removed from the save data.
|
||||
*/
|
||||
public void setMaxPagesToStateSave(int maxPagesToStateSave) {
|
||||
if (maxPagesToStateSave < 0) {
|
||||
throw new IllegalArgumentException("Only positive integers may be passed for maxPagesToStateSave.");
|
||||
}
|
||||
|
||||
this.maxPagesToStateSave = maxPagesToStateSave;
|
||||
|
||||
ensurePagesSaved();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object instantiateItem(ViewGroup container, int position) {
|
||||
final String name = makeRouterName(container.getId(), getItemId(position));
|
||||
|
||||
Router router = host.getChildRouter(container, name);
|
||||
if (!router.hasRootController()) {
|
||||
Bundle routerSavedState = savedPages.get(position);
|
||||
|
||||
if (routerSavedState != null) {
|
||||
router.restoreInstanceState(routerSavedState);
|
||||
savedPages.remove(position);
|
||||
}
|
||||
}
|
||||
|
||||
router.rebindIfNeeded();
|
||||
configureRouter(router, position);
|
||||
|
||||
if (router != primaryRouter) {
|
||||
for (RouterTransaction transaction : router.getBackstack()) {
|
||||
transaction.controller().setOptionsMenuHidden(true);
|
||||
}
|
||||
}
|
||||
|
||||
visibleRouters.put(position, router);
|
||||
return router;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
Router router = (Router)object;
|
||||
|
||||
Bundle savedState = new Bundle();
|
||||
router.saveInstanceState(savedState);
|
||||
savedPages.put(position, savedState);
|
||||
|
||||
savedPageHistory.remove((Integer)position);
|
||||
savedPageHistory.add(position);
|
||||
|
||||
ensurePagesSaved();
|
||||
|
||||
host.removeChildRouter(router);
|
||||
|
||||
visibleRouters.remove(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPrimaryItem(ViewGroup container, int position, Object object) {
|
||||
Router router = (Router)object;
|
||||
if (router != primaryRouter) {
|
||||
if (primaryRouter != null) {
|
||||
for (RouterTransaction transaction : primaryRouter.getBackstack()) {
|
||||
transaction.controller().setOptionsMenuHidden(true);
|
||||
}
|
||||
}
|
||||
if (router != null) {
|
||||
for (RouterTransaction transaction : router.getBackstack()) {
|
||||
transaction.controller().setOptionsMenuHidden(false);
|
||||
}
|
||||
}
|
||||
primaryRouter = router;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(View view, Object object) {
|
||||
Router router = (Router)object;
|
||||
final List<RouterTransaction> backstack = router.getBackstack();
|
||||
for (RouterTransaction transaction : backstack) {
|
||||
if (transaction.controller().getView() == view) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable saveState() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages);
|
||||
bundle.putInt(KEY_MAX_PAGES_TO_STATE_SAVE, maxPagesToStateSave);
|
||||
bundle.putIntegerArrayList(KEY_SAVE_PAGE_HISTORY, savedPageHistory);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreState(Parcelable state, ClassLoader loader) {
|
||||
Bundle bundle = (Bundle)state;
|
||||
if (state != null) {
|
||||
savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES);
|
||||
maxPagesToStateSave = bundle.getInt(KEY_MAX_PAGES_TO_STATE_SAVE);
|
||||
savedPageHistory = bundle.getIntegerArrayList(KEY_SAVE_PAGE_HISTORY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the already instantiated Router in the specified position or {@code null} if there
|
||||
* is no router associated with this position.
|
||||
*/
|
||||
@Nullable
|
||||
public Router getRouter(int position) {
|
||||
return visibleRouters.get(position);
|
||||
}
|
||||
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
SparseArray<Bundle> getSavedPages() {
|
||||
return savedPages;
|
||||
}
|
||||
|
||||
private void ensurePagesSaved() {
|
||||
while (savedPages.size() > maxPagesToStateSave) {
|
||||
int positionToRemove = savedPageHistory.remove(0);
|
||||
savedPages.remove(positionToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
private static String makeRouterName(int viewId, long id) {
|
||||
return viewId + ":" + id;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.CallSuper
|
||||
import android.view.View
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
|
||||
abstract class RxController(bundle: Bundle? = null) : BaseController(bundle) {
|
||||
|
||||
var untilDetachSubscriptions = CompositeSubscription()
|
||||
private set
|
||||
|
||||
var untilDestroySubscriptions = CompositeSubscription()
|
||||
private set
|
||||
|
||||
@CallSuper
|
||||
override fun onAttach(view: View) {
|
||||
super.onAttach(view)
|
||||
if (untilDetachSubscriptions.isUnsubscribed) {
|
||||
untilDetachSubscriptions = CompositeSubscription()
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onDetach(view: View) {
|
||||
super.onDetach(view)
|
||||
untilDetachSubscriptions.unsubscribe()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onViewCreated(view: View, savedViewState: Bundle?) {
|
||||
if (untilDestroySubscriptions.isUnsubscribed) {
|
||||
untilDestroySubscriptions = CompositeSubscription()
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
untilDestroySubscriptions.unsubscribe()
|
||||
}
|
||||
|
||||
|
||||
fun <T> Observable<T>.subscribeUntilDetach(): Subscription {
|
||||
|
||||
return subscribe().also { untilDetachSubscriptions.add(it) }
|
||||
}
|
||||
|
||||
fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit): Subscription {
|
||||
|
||||
return subscribe(onNext).also { untilDetachSubscriptions.add(it) }
|
||||
}
|
||||
|
||||
fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit,
|
||||
onError: (Throwable) -> Unit): Subscription {
|
||||
|
||||
return subscribe(onNext, onError).also { untilDetachSubscriptions.add(it) }
|
||||
}
|
||||
|
||||
fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit,
|
||||
onError: (Throwable) -> Unit,
|
||||
onCompleted: () -> Unit): Subscription {
|
||||
|
||||
return subscribe(onNext, onError, onCompleted).also { untilDetachSubscriptions.add(it) }
|
||||
}
|
||||
|
||||
fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {
|
||||
|
||||
return subscribe().also { untilDestroySubscriptions.add(it) }
|
||||
}
|
||||
|
||||
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
|
||||
|
||||
return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
|
||||
}
|
||||
|
||||
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit,
|
||||
onError: (Throwable) -> Unit): Subscription {
|
||||
|
||||
return subscribe(onNext, onError).also { untilDestroySubscriptions.add(it) }
|
||||
}
|
||||
|
||||
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit,
|
||||
onError: (Throwable) -> Unit,
|
||||
onCompleted: () -> Unit): Subscription {
|
||||
|
||||
return subscribe(onNext, onError, onCompleted).also { untilDestroySubscriptions.add(it) }
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import android.support.v4.widget.DrawerLayout
|
||||
import android.view.ViewGroup
|
||||
|
||||
interface SecondaryDrawerController {
|
||||
|
||||
fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup?
|
||||
|
||||
fun cleanupSecondaryDrawer(drawer: DrawerLayout)
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import android.support.design.widget.TabLayout
|
||||
|
||||
interface TabbedController {
|
||||
|
||||
fun configureTabs(tabs: TabLayout) {}
|
||||
|
||||
fun cleanupTabs(tabs: TabLayout) {}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.base.fragment
|
||||
|
||||
import android.support.v4.app.Fragment
|
||||
|
||||
abstract class BaseFragment : Fragment(), FragmentMixin {
|
||||
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.base.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.App
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import nucleus.view.NucleusSupportFragment
|
||||
|
||||
abstract class BaseRxFragment<P : BasePresenter<*>> : NucleusSupportFragment<P>(), FragmentMixin {
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
val superFactory = presenterFactory
|
||||
setPresenterFactory {
|
||||
superFactory.createPresenter().apply {
|
||||
val app = activity.application as App
|
||||
context = app.applicationContext
|
||||
}
|
||||
}
|
||||
super.onCreate(savedState)
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.base.fragment
|
||||
|
||||
import android.support.v4.app.FragmentActivity
|
||||
import eu.kanade.tachiyomi.ui.base.activity.ActivityMixin
|
||||
|
||||
interface FragmentMixin {
|
||||
|
||||
fun setToolbarTitle(title: String) {
|
||||
(getActivity() as ActivityMixin).setToolbarTitle(title)
|
||||
}
|
||||
|
||||
fun setToolbarTitle(resourceId: Int) {
|
||||
(getActivity() as ActivityMixin).setToolbarTitle(getString(resourceId))
|
||||
}
|
||||
|
||||
fun getActivity(): FragmentActivity
|
||||
|
||||
fun getString(resource: Int): String
|
||||
}
|
@ -1,13 +1,9 @@
|
||||
package eu.kanade.tachiyomi.ui.base.presenter
|
||||
|
||||
import android.content.Context
|
||||
import nucleus.presenter.RxPresenter
|
||||
import nucleus.view.ViewWithPresenter
|
||||
import rx.Observable
|
||||
|
||||
open class BasePresenter<V : ViewWithPresenter<*>> : RxPresenter<V>() {
|
||||
|
||||
lateinit var context: Context
|
||||
open class BasePresenter<V> : RxPresenter<V>() {
|
||||
|
||||
/**
|
||||
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
|
||||
|
@ -0,0 +1,61 @@
|
||||
package eu.kanade.tachiyomi.ui.base.presenter;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import nucleus.factory.PresenterFactory;
|
||||
import nucleus.presenter.Presenter;
|
||||
|
||||
public class NucleusConductorDelegate<P extends Presenter> {
|
||||
|
||||
@Nullable private P presenter;
|
||||
@Nullable private Bundle bundle;
|
||||
|
||||
private PresenterFactory<P> factory;
|
||||
|
||||
public NucleusConductorDelegate(PresenterFactory<P> creator) {
|
||||
this.factory = creator;
|
||||
}
|
||||
|
||||
public P getPresenter() {
|
||||
if (presenter == null) {
|
||||
presenter = factory.createPresenter();
|
||||
presenter.create(bundle);
|
||||
bundle = null;
|
||||
}
|
||||
return presenter;
|
||||
}
|
||||
|
||||
Bundle onSaveInstanceState() {
|
||||
Bundle bundle = new Bundle();
|
||||
getPresenter();
|
||||
if (presenter != null) {
|
||||
presenter.save(bundle);
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
void onRestoreInstanceState(Bundle presenterState) {
|
||||
bundle = presenterState;
|
||||
}
|
||||
|
||||
void onTakeView(Object view) {
|
||||
getPresenter();
|
||||
if (presenter != null) {
|
||||
//noinspection unchecked
|
||||
presenter.takeView(view);
|
||||
}
|
||||
}
|
||||
|
||||
void onDropView() {
|
||||
if (presenter != null) {
|
||||
presenter.dropView();
|
||||
}
|
||||
}
|
||||
|
||||
void onDestroy() {
|
||||
if (presenter != null) {
|
||||
presenter.destroy();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package eu.kanade.tachiyomi.ui.base.presenter;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
|
||||
import com.bluelinelabs.conductor.Controller;
|
||||
|
||||
public class NucleusConductorLifecycleListener extends Controller.LifecycleListener {
|
||||
|
||||
private static final String PRESENTER_STATE_KEY = "presenter_state";
|
||||
|
||||
private NucleusConductorDelegate delegate;
|
||||
|
||||
public NucleusConductorLifecycleListener(NucleusConductorDelegate delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
|
||||
delegate.onTakeView(controller);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
|
||||
delegate.onDropView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preDestroy(@NonNull Controller controller) {
|
||||
delegate.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
|
||||
outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
|
||||
delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY));
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user