mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
345 Commits
Author | SHA1 | Date | |
---|---|---|---|
b8a98ef5e4 | |||
c8fa90f473 | |||
b47ee8857b | |||
131dfa62c4 | |||
6a5af438dd | |||
ccc0a61158 | |||
e990ad25eb | |||
98a4d1e763 | |||
f762598c5c | |||
ec56c27071 | |||
eb0e0a1952 | |||
01a837fde6 | |||
b9488645d4 | |||
7a94b477cb | |||
99710b45d1 | |||
3813743e3d | |||
9bb2334b69 | |||
7e73ede47a | |||
b0106aa420 | |||
33e5fea96c | |||
f0a1dcd120 | |||
26d5a87bef | |||
52ae208df3 | |||
34aaa7fb0a | |||
a8c784355c | |||
0aed93becf | |||
1ea0804209 | |||
a52fbb012a | |||
2dc47352f8 | |||
e95a5be21d | |||
abd69d4f91 | |||
d749e309f8 | |||
e4d075fb91 | |||
71c6c71081 | |||
d2b14bcfc4 | |||
2c04c81bd1 | |||
dd66c83c50 | |||
9e51d82154 | |||
bdc441a5be | |||
2dcb73700b | |||
ee01686ae4 | |||
9a55cf880e | |||
6742cdeb8b | |||
c37377bffa | |||
76147a9be7 | |||
bf22e69250 | |||
49693934cf | |||
e6a63ee5b2 | |||
08dc57fd02 | |||
cede590696 | |||
c401915fb5 | |||
2a202bd510 | |||
dcd8ed08fc | |||
d3ebedeef2 | |||
d2e2ebbe45 | |||
0cef05dd89 | |||
a443dc3040 | |||
4e6cc013e5 | |||
0c65d54d89 | |||
ccd0e0cdfe | |||
9278ca3f5e | |||
d7a70b962b | |||
fff0f841fa | |||
8ba426350f | |||
8ef548032f | |||
5452e29840 | |||
148f8e6d11 | |||
13a5662a84 | |||
6713a7ae3c | |||
226ad13061 | |||
88ee86b7ef | |||
4bc2288806 | |||
a928d9fa0b | |||
d8f4e6b45f | |||
5ef5087406 | |||
135c371d88 | |||
966c196f4a | |||
dc43e41896 | |||
4809d06d04 | |||
9f7fda0bc5 | |||
1f67695713 | |||
66ef1a8206 | |||
beaffc3870 | |||
8536ecb611 | |||
d7a89b0f8c | |||
943081e80d | |||
3f007a1edd | |||
e33cacf6a4 | |||
23fe848a35 | |||
fa5d2276c0 | |||
d353a3457d | |||
b363b9fc1a | |||
1920568057 | |||
763da19c9d | |||
1813dbbf59 | |||
339169b624 | |||
93960315d9 | |||
479eb1ba71 | |||
962d8e5fd2 | |||
fefd4c0b26 | |||
40639c0933 | |||
7401673ac1 | |||
41f759dafe | |||
73dcc7bcb1 | |||
d8b1f60581 | |||
16fc58bd16 | |||
68df2f4ce7 | |||
367932de69 | |||
3da08cbcbf | |||
68efc0c42f | |||
f5b01b0ca2 | |||
5733429682 | |||
c6d29fc19b | |||
d9a12d79b0 | |||
963cf4c996 | |||
0ef073669a | |||
ed1123feb0 | |||
c2114bbd4f | |||
fedb1d2590 | |||
eef39b75a6 | |||
eca593ac36 | |||
a1917b8c81 | |||
ec6dba12bd | |||
da0671ad62 | |||
04d83e9a6a | |||
2cb7624953 | |||
e6ace844b6 | |||
f2f6628693 | |||
1c33032721 | |||
b3f5f13c39 | |||
a5339969c9 | |||
1681437206 | |||
2eaf083eee | |||
3645d19135 | |||
3b4b1185e2 | |||
406c5bde11 | |||
75d1913aaf | |||
eb254d9c56 | |||
4e633b8936 | |||
e99a27e382 | |||
c8a6a2653f | |||
0ea0eba4f0 | |||
7f88b56d8b | |||
789421c7a0 | |||
d44503cb19 | |||
0258422527 | |||
47327d840d | |||
d0f1a33744 | |||
c54b8e62d7 | |||
3dc738f28c | |||
ca75400467 | |||
65091c05c9 | |||
52e846f3b6 | |||
ce22b2c29a | |||
361b0284fa | |||
b8947a1c50 | |||
8d1effa0e8 | |||
096a9f4cbf | |||
18712b166f | |||
4605e14729 | |||
a768280d82 | |||
cbb8f25645 | |||
24ff7ff67c | |||
eee0bd6cf4 | |||
f176a5179a | |||
381ba86e3c | |||
f4be8e28ca | |||
e17605f8d9 | |||
e06a488af8 | |||
6dc8bfed8d | |||
f642f23366 | |||
c041410f61 | |||
ff5f13eafe | |||
749b240897 | |||
9207762ade | |||
0a22950ad3 | |||
0fcd404b4f | |||
150ea29a70 | |||
f9baff0e90 | |||
6ad3fcb91d | |||
395ca3630c | |||
e9fbdb660b | |||
526e029ebb | |||
763c4522b7 | |||
32e3ac63ed | |||
90f31aab38 | |||
3d44feaf2c | |||
f5a44245e9 | |||
7e7eb9f39f | |||
6c9b982104 | |||
a106104027 | |||
7753161332 | |||
ec9d592cf1 | |||
31015504f4 | |||
bd40ec527d | |||
3899938b25 | |||
ca7373c28b | |||
390bdfa93d | |||
ac2df87954 | |||
0b73f8b1ef | |||
812fcec9f0 | |||
ec7297f8c2 | |||
0fdb19c07d | |||
3bc07b4753 | |||
0b8b13d0bb | |||
4c5bf9bc8e | |||
9f53109414 | |||
bccb1229c8 | |||
40ab3fe0a6 | |||
4a99118cce | |||
58ba29fa16 | |||
54cfb2acdf | |||
0bf14fd31c | |||
7dd9a0211b | |||
3d43473bf8 | |||
2194c4ba28 | |||
744a4f8f47 | |||
67d91f7b69 | |||
bf5065d16b | |||
3e837f8781 | |||
77d378ccd1 | |||
1a542bae71 | |||
e3ed12b5d2 | |||
759795940b | |||
a23d5ab734 | |||
6ee69ef430 | |||
3edf17d322 | |||
8c2b2f99bc | |||
73dc51b3f6 | |||
9a082d4df1 | |||
f430b6f853 | |||
78a352541a | |||
a0f5633094 | |||
0af81c7d05 | |||
52e82b3548 | |||
f05b99ec1f | |||
194897bf3c | |||
7cf26363c8 | |||
3d1dec4c05 | |||
af1935d2e4 | |||
3c4bc17065 | |||
333d1c1ad9 | |||
4e027cec71 | |||
39ae84301a | |||
3bf14623ad | |||
ac8f2923e5 | |||
e9d3b75e2b | |||
e6bc181e7a | |||
a2ece82197 | |||
259946cf0a | |||
4fdb4f14a8 | |||
914f5e569b | |||
9b4ffd1cd5 | |||
ed87dd89a1 | |||
3b41a78e76 | |||
0fccbbc0ca | |||
067627b51a | |||
09816ed5b6 | |||
b457cdb0c2 | |||
5fd1dec347 | |||
647391ef73 | |||
ed029c52ae | |||
102a372df9 | |||
d4ffb09a8b | |||
6ba052d2af | |||
57b63f43f5 | |||
2fb0969c75 | |||
3357e878a5 | |||
471d5d62d5 | |||
e810b343cf | |||
620be2617a | |||
035038a0b6 | |||
b8ffb87f01 | |||
39e1e11f99 | |||
5f9df78ab0 | |||
a00d11701f | |||
1cf74a5396 | |||
8cd27a199d | |||
772929b5c6 | |||
c4ca3606ad | |||
9e830f1c55 | |||
ee8c71c14a | |||
39bd823651 | |||
a9d16fad34 | |||
1da169319d | |||
2bb1eea2be | |||
9cb45b92e1 | |||
4a8d5098da | |||
d875d5ef74 | |||
fc4e290c49 | |||
ccc198e081 | |||
9f0ed77423 | |||
bb3e616890 | |||
573f3a392a | |||
830a834ea6 | |||
e4ea5d0344 | |||
97aed045e6 | |||
46b01c6134 | |||
e208fa4020 | |||
6b71264482 | |||
5723c184b1 | |||
530daeaa3a | |||
dd1b5c7ea7 | |||
3cffc6890d | |||
04d44f19f5 | |||
d46a742a43 | |||
a94fd24fa9 | |||
8a4c4c346a | |||
dc54299e24 | |||
436253dd63 | |||
29feee0095 | |||
63f3180dff | |||
6b3b98cf57 | |||
1442e2b53e | |||
521ebf0678 | |||
a20874f6a1 | |||
40776bdc8d | |||
f853158e6b | |||
c9035b5df9 | |||
150132f4dd | |||
fb97ac47bc | |||
5b53b90495 | |||
c6513d4450 | |||
ce6848b3c0 | |||
d86d861e4b | |||
3b45fcdb21 | |||
3d1250f2f8 | |||
eaf1ef831a | |||
b4c7992726 | |||
03baa21185 | |||
694de99a3f | |||
8383f4fb7b | |||
eb3fff6c51 | |||
4ff3f0bcba | |||
788ea052fc | |||
08ba805bbd | |||
dbd14c6dac | |||
5977fca6b9 | |||
fcf596d36b | |||
dabca5f09e | |||
018dbce57e | |||
bd45cc2024 | |||
abf47deee3 | |||
ce0090f0ca | |||
6cd34614f6 |
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
@ -14,9 +14,9 @@
|
|||||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
||||||
|
|
||||||
# Bugs
|
# Bugs
|
||||||
* Include version (Setting > About > Version)
|
* Include version (More > About > Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* Dev version is equal to the number of commits as seen in the main page
|
* Preview version is equal to the number of commits as seen in the main page
|
||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.9.2)
|
- I have updated to the latest version of the app (stable is v0.10.0)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
|
||||||
|
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: "🐞 Bug report"
|
name: "🐞 Bug report"
|
||||||
about: Report a bug
|
about: Report a bug
|
||||||
title: "[Bug] Write short description here"
|
title: "[Bug] <Write short description here>"
|
||||||
labels: "bug"
|
labels: "bug"
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ labels: "bug"
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.9.2)
|
- I have updated to the latest version of the app (stable is v0.10.0)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
|
||||||
|
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: "🌟 Feature request"
|
name: "🌟 Feature request"
|
||||||
about: Suggest a feature to improve Tachiyomi
|
about: Suggest a feature to improve Tachiyomi
|
||||||
title: "[Feature Request] Write short description here"
|
title: "[Feature Request] <Write short description here>"
|
||||||
labels: "feature"
|
labels: "feature"
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ labels: "feature"
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.9.2)
|
- I have updated to the latest version of the app (stable is v0.10.0)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
|
||||||
|
BIN
.github/readme-images/screens.png
vendored
BIN
.github/readme-images/screens.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.2 MiB |
31
.github/workflows/issue_closer.yml
vendored
31
.github/workflows/issue_closer.yml
vendored
@ -4,10 +4,31 @@ jobs:
|
|||||||
autoclose:
|
autoclose:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Autoclose issue
|
- name: Autoclose when created in wrong repo
|
||||||
uses: arkon/issue-closer-action@v1.0
|
uses: arkon/issue-closer-action@v1.1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
issue-close-message: "@${issue.user.login} this issue was automatically closed because it was not filled in correctly or the acknowledgment section was not removed."
|
type: title
|
||||||
issue-title-pattern: ".*THIS ISSUE IS IN THE WRONG REPO.*"
|
regex: ".*THIS ISSUE IS IN THE WRONG REPO.*"
|
||||||
issue-body-pattern: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
|
message: "@${issue.user.login} this issue was automatically closed because it was not opened in the correct repo, as the template mentioned."
|
||||||
|
- name: Autoclose when no short description provided
|
||||||
|
uses: arkon/issue-closer-action@v1.1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
type: title
|
||||||
|
regex: ".*<Write short description here>*"
|
||||||
|
message: "@${issue.user.login} this issue was automatically closed because you did not fill out the description in the title."
|
||||||
|
- name: Autoclose when body acknowledgement section not removed
|
||||||
|
uses: arkon/issue-closer-action@v1.1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
type: body
|
||||||
|
regex: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
|
||||||
|
message: "@${issue.user.login} this issue was automatically closed because the acknowledgment section was not removed."
|
||||||
|
- name: Autoclose when body requested information not filled out
|
||||||
|
uses: arkon/issue-closer-action@v1.1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
type: body
|
||||||
|
regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*"
|
||||||
|
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
|
@ -62,6 +62,13 @@ deploy:
|
|||||||
branch: master
|
branch: master
|
||||||
condition: "-z $TRAVIS_TAG"
|
condition: "-z $TRAVIS_TAG"
|
||||||
repo: inorichi/tachiyomi
|
repo: inorichi/tachiyomi
|
||||||
|
- provider: script
|
||||||
|
script: ".travis/deploy.sh"
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
branch: dev
|
||||||
|
condition: "-z $TRAVIS_TAG"
|
||||||
|
repo: inorichi/tachiyomi
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
|
28
PREVIEW_RELEASE_NOTES.md
Normal file
28
PREVIEW_RELEASE_NOTES.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
### r1810
|
||||||
|
- Background jobs were migrated to a new system. You may need to toggle the settings to ensure they
|
||||||
|
run properly. This includes app updates, library updates, and automatic backups.
|
||||||
|
|
||||||
|
### r1340
|
||||||
|
- A new screen for managing extensions was added. If you previously installed extensions from FDroid,
|
||||||
|
you will have to uninstall all of them first (tap on the extension then uninstall), otherwise you won't be able
|
||||||
|
to update them due to signature mismatch. You won't lose anything in this process as the extensions themselves
|
||||||
|
don't store anything.
|
||||||
|
|
||||||
|
### r959
|
||||||
|
- The download manager has been rewritten and it's possible some of your downloads
|
||||||
|
aren't recognized anymore. You may have to check your downloads folder and manually delete those.
|
||||||
|
- You can now download to any folder in your SD card.
|
||||||
|
- The download directory setting has been reset.
|
||||||
|
|
||||||
|
### r857
|
||||||
|
- **Important!** Delete after read has been updated.
|
||||||
|
This means the value has been reset set to disabled.
|
||||||
|
This can be changed in Settings > Downloads
|
||||||
|
|
||||||
|
### r736
|
||||||
|
- **Important!** Now chapters follow the order of the sources. **It's required that you update your entire library
|
||||||
|
before reading in order for them to be synced.** Old behavior can be restored for a manga in the overflow menu of the chapters tab.
|
||||||
|
|
||||||
|
### r724
|
||||||
|
- Kissmanga covers may not load anymore. The only workaround is to update the details of the manga
|
||||||
|
from the info tab, or clearing the database (the latter won't fix covers from library manga).
|
@ -38,7 +38,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
|
|
||||||
<details><summary>Bugs</summary>
|
<details><summary>Bugs</summary>
|
||||||
|
|
||||||
* Include version (Setting > About > Version)
|
* Include version (More > About > Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* Preview version is equal to the number of commits as seen in the main page
|
* Preview version is equal to the number of commits as seen in the main page
|
||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
|
136
app/build.gradle
136
app/build.gradle
@ -1,7 +1,9 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
|
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
@ -30,16 +32,16 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion AndroidConfig.compileSdk
|
||||||
buildToolsVersion '29.0.3'
|
buildToolsVersion AndroidConfig.buildTools
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "eu.kanade.tachiyomi"
|
applicationId "eu.kanade.tachiyomi"
|
||||||
minSdkVersion 21
|
minSdkVersion AndroidConfig.minSdk
|
||||||
targetSdkVersion 29
|
targetSdkVersion AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 45
|
versionCode 46
|
||||||
versionName "0.9.2"
|
versionName "0.10.0"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@ -53,8 +55,8 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewBinding {
|
buildFeatures {
|
||||||
enabled = true
|
viewBinding = true
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@ -62,11 +64,15 @@ android {
|
|||||||
versionNameSuffix "-${getCommitCount()}"
|
versionNameSuffix "-${getCommitCount()}"
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
}
|
}
|
||||||
// release {
|
release {
|
||||||
// minifyEnabled true
|
postprocessing {
|
||||||
// shrinkResources true
|
obfuscate false
|
||||||
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
optimizeCode true
|
||||||
// }
|
removeUnusedCode false
|
||||||
|
removeUnusedResources true
|
||||||
|
proguardFiles 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "default"
|
flavorDimensions "default"
|
||||||
@ -93,18 +99,25 @@ android {
|
|||||||
exclude 'META-INF/NOTICE'
|
exclude 'META-INF/NOTICE'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependenciesInfo {
|
||||||
|
includeInApk = false
|
||||||
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
|
disable 'MissingTranslation'
|
||||||
|
disable 'ExtraTranslation'
|
||||||
|
|
||||||
abortOnError false
|
abortOnError false
|
||||||
checkReleaseBuilds false
|
checkReleaseBuilds false
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = 1.8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = 1.8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,30 +127,34 @@ androidExtensions {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Modified dependencies
|
// AndroidX libraries
|
||||||
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
|
|
||||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
|
||||||
|
|
||||||
// Android support library
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
|
||||||
implementation 'androidx.annotation:annotation:1.1.0'
|
implementation 'androidx.annotation:annotation:1.1.0'
|
||||||
implementation 'androidx.browser:browser:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
|
||||||
implementation 'androidx.biometric:biometric:1.0.1'
|
implementation 'androidx.biometric:biometric:1.0.1'
|
||||||
|
implementation 'androidx.browser:browser:1.2.0'
|
||||||
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
|
||||||
|
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||||
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
|
||||||
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||||
|
implementation 'androidx.webkit:webkit:1.3.0-rc01'
|
||||||
|
|
||||||
final lifecycle_version = '2.2.0'
|
final lifecycle_version = '2.3.0-alpha06'
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
|
|
||||||
// UI library
|
// Job scheduling
|
||||||
implementation 'com.google.android.material:material:1.1.0'
|
final work_version = '2.4.0'
|
||||||
|
implementation "androidx.work:work-runtime:$work_version"
|
||||||
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
|
|
||||||
standardImplementation 'com.google.firebase:firebase-core:17.4.0'
|
// UI library
|
||||||
|
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
||||||
|
|
||||||
|
standardImplementation 'com.google.firebase:firebase-core:17.4.4'
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
@ -146,13 +163,17 @@ dependencies {
|
|||||||
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
final okhttp_version = '4.5.0'
|
final okhttp_version = '4.8.0'
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||||
implementation 'com.squareup.okio:okio:2.6.0'
|
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
|
||||||
|
implementation 'com.squareup.okio:okio:2.7.0'
|
||||||
|
|
||||||
|
// TLS 1.3 support for Android < 10
|
||||||
|
implementation 'org.conscrypt:conscrypt-android:2.4.0'
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
final retrofit_version = '2.8.1'
|
final retrofit_version = '2.9.0'
|
||||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||||
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||||
@ -167,30 +188,22 @@ dependencies {
|
|||||||
// Disk
|
// Disk
|
||||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||||
implementation 'com.github.inorichi:unifile:e9ee588'
|
implementation 'com.github.inorichi:unifile:e9ee588'
|
||||||
|
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation 'org.jsoup:jsoup:1.13.1'
|
implementation 'org.jsoup:jsoup:1.13.1'
|
||||||
|
|
||||||
// Job scheduling
|
|
||||||
final work_version = '2.3.4'
|
|
||||||
implementation "androidx.work:work-runtime:$work_version"
|
|
||||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
|
||||||
|
|
||||||
// Changelog
|
|
||||||
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation 'androidx.sqlite:sqlite:2.1.0'
|
implementation 'androidx.sqlite:sqlite:2.1.0'
|
||||||
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
||||||
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
|
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
|
||||||
implementation 'io.requery:sqlite-android:3.31.0'
|
implementation 'io.requery:sqlite-android:3.32.2'
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
|
||||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1'
|
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1'
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
final nucleus_version = '6.0.0'
|
final nucleus_version = '3.0.0'
|
||||||
implementation "info.android15.nucleus:nucleus:$nucleus_version"
|
implementation "info.android15.nucleus:nucleus:$nucleus_version"
|
||||||
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||||
|
|
||||||
@ -203,12 +216,13 @@ dependencies {
|
|||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||||
|
|
||||||
|
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:bff2806'
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
final acra_version = '5.5.0'
|
implementation 'ch.acra:acra-http:5.7.0'
|
||||||
implementation "ch.acra:acra-http:$acra_version"
|
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||||
@ -235,16 +249,21 @@ dependencies {
|
|||||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
||||||
exclude group: "com.android.support"
|
exclude group: "com.android.support"
|
||||||
}
|
}
|
||||||
implementation 'com.github.inorichi:conductor-support-preference:a32c357'
|
implementation 'com.github.tachiyomiorg:conductor-support-preference:1.1.1'
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
final flowbinding_version = '0.11.1'
|
final flowbinding_version = '0.12.0'
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version"
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
||||||
|
|
||||||
|
// Licenses
|
||||||
|
final aboutlibraries_version = '8.3.0'
|
||||||
|
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
|
||||||
|
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13'
|
||||||
testImplementation 'org.assertj:assertj-core:3.12.2'
|
testImplementation 'org.assertj:assertj-core:3.12.2'
|
||||||
@ -257,14 +276,17 @@ dependencies {
|
|||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
|
||||||
final coroutines_version = '1.3.5'
|
// Do not update until we bump to Kotlin 1.4, see https://github.com/Kotlin/kotlinx.coroutines/issues/2049
|
||||||
|
final coroutines_version = '1.3.6'
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
|
|
||||||
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
|
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
||||||
|
|
||||||
|
// Debug tool; see https://fbflipper.com/
|
||||||
|
// debugImplementation 'com.facebook.flipper:flipper:0.50.0'
|
||||||
|
// debugImplementation 'com.facebook.soloader:soloader:0.9.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
@ -282,7 +304,7 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
||||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all {
|
tasks.withType(AbstractKotlinCompile).all {
|
||||||
kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.Experimental"]
|
kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.Experimental"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
@ -119,13 +118,6 @@
|
|||||||
android:name=".extension.util.ExtensionInstallActivity"
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
|
||||||
android:theme="@style/Theme.MaterialComponents" />
|
|
||||||
<activity
|
|
||||||
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
|
|
||||||
android:theme="@style/Theme.MaterialComponents" />
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.os.Build
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleObserver
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
@ -12,10 +13,12 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import java.security.Security
|
||||||
import org.acra.ACRA
|
import org.acra.ACRA
|
||||||
import org.acra.annotation.AcraCore
|
import org.acra.annotation.AcraCore
|
||||||
import org.acra.annotation.AcraHttpSender
|
import org.acra.annotation.AcraHttpSender
|
||||||
import org.acra.sender.HttpSender
|
import org.acra.sender.HttpSender
|
||||||
|
import org.conscrypt.Conscrypt
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.InjektScope
|
import uy.kohesive.injekt.api.InjektScope
|
||||||
@ -36,6 +39,20 @@ open class App : Application(), LifecycleObserver {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
|
// Debug tool; see https://fbflipper.com/
|
||||||
|
// SoLoader.init(this, false)
|
||||||
|
// if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) {
|
||||||
|
// val client = AndroidFlipperClient.getInstance(this)
|
||||||
|
// client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
|
||||||
|
// client.addPlugin(DatabasesFlipperPlugin(this))
|
||||||
|
// client.start()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TLS 1.3 support for Android < 10
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
Injekt = InjektScope(DefaultRegistrar())
|
Injekt = InjektScope(DefaultRegistrar())
|
||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
|
@ -7,8 +7,10 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,14 +71,11 @@ class BackupCreateService : Service() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
notifier = BackupNotifier(this)
|
notifier = BackupNotifier(this)
|
||||||
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
|
|
||||||
startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
|
startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
|
||||||
|
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, "${javaClass.name}:WakeLock"
|
|
||||||
)
|
|
||||||
wakeLock.acquire()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopService(name: Intent?): Boolean {
|
override fun stopService(name: Intent?): Boolean {
|
||||||
@ -108,7 +107,7 @@ class BackupCreateService : Service() {
|
|||||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||||
backupManager = BackupManager(this)
|
backupManager = BackupManager(this)
|
||||||
|
|
||||||
val backupFileUri = Uri.parse(backupManager.createBackup(uri, backupFlags, false))
|
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
||||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||||
notifier.showBackupComplete(unifile)
|
notifier.showBackupComplete(unifile)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
@ -18,7 +18,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val backupManager = BackupManager(context)
|
val backupManager = BackupManager(context)
|
||||||
val uri = Uri.parse(preferences.backupsDirectory().get())
|
val uri = preferences.backupsDirectory().get().toUri()
|
||||||
val flags = BackupCreateService.BACKUP_ALL
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
return try {
|
return try {
|
||||||
backupManager.createBackup(uri, flags, true)
|
backupManager.createBackup(uri, flags, true)
|
||||||
|
@ -46,6 +46,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
@ -131,8 +132,10 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
mangaEntries.add(backupMangaObject(manga, flags))
|
mangaEntries.add(backupMangaObject(manga, flags))
|
||||||
|
|
||||||
// Maintain set of extensions/sources used (excludes local source)
|
// Maintain set of extensions/sources used (excludes local source)
|
||||||
if (manga.source != 0L && sourceManager.get(manga.source) != null) {
|
if (manga.source != LocalSource.ID) {
|
||||||
extensions.add("${manga.source}:${sourceManager.get(manga.source)!!.name}")
|
sourceManager.get(manga.source)?.let {
|
||||||
|
extensions.add("${manga.source}:${it.name}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,7 +325,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
for (dbCategory in dbCategories) {
|
for (dbCategory in dbCategories) {
|
||||||
// If the category is already in the db, assign the id to the file's category
|
// If the category is already in the db, assign the id to the file's category
|
||||||
// and do nothing
|
// and do nothing
|
||||||
if (category.nameLower == dbCategory.nameLower) {
|
if (category.name == dbCategory.name) {
|
||||||
category.id = dbCategory.id
|
category.id = dbCategory.id
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@ -347,10 +350,10 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*/
|
*/
|
||||||
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
|
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
|
||||||
for (backupCategoryStr in categories) {
|
for (backupCategoryStr in categories) {
|
||||||
for (dbCategory in dbCategories) {
|
for (dbCategory in dbCategories) {
|
||||||
if (backupCategoryStr.toLowerCase() == dbCategory.nameLower) {
|
if (backupCategoryStr == dbCategory.name) {
|
||||||
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
|
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -359,9 +362,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
|
|
||||||
// Update database
|
// Update database
|
||||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||||
val mangaAsList = ArrayList<Manga>()
|
databaseHelper.deleteOldMangasCategories(listOf(manga)).executeAsBlocking()
|
||||||
mangaAsList.add(manga)
|
|
||||||
databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
|
||||||
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -373,7 +374,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*/
|
*/
|
||||||
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
||||||
// List containing history to be updated
|
// List containing history to be updated
|
||||||
val historyToBeUpdated = ArrayList<History>()
|
val historyToBeUpdated = mutableListOf<History>()
|
||||||
for ((url, lastRead) in history) {
|
for ((url, lastRead) in history) {
|
||||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||||
// Check if history already in database and update
|
// Check if history already in database and update
|
||||||
@ -407,9 +408,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
|
|
||||||
// Get tracks from database
|
// Get tracks from database
|
||||||
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
val trackToUpdate = ArrayList<Track>()
|
val trackToUpdate = mutableListOf<Track>()
|
||||||
|
|
||||||
for (track in tracks) {
|
tracks.forEach { track ->
|
||||||
val service = trackManager.getService(track.sync_id)
|
val service = trackManager.getService(track.sync_id)
|
||||||
if (service != null && service.isLogged) {
|
if (service != null && service.isLogged) {
|
||||||
var isInDatabase = false
|
var isInDatabase = false
|
||||||
|
@ -41,6 +41,7 @@ internal class BackupNotifier(private val context: Context) {
|
|||||||
setContentTitle(context.getString(R.string.creating_backup))
|
setContentTitle(context.getString(R.string.creating_backup))
|
||||||
|
|
||||||
setProgress(0, 0, true)
|
setProgress(0, 0, true)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
||||||
@ -93,6 +94,7 @@ internal class BackupNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setProgress(maxAmount, progress, false)
|
setProgress(maxAmount, progress, false)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
|
||||||
// Clear old actions if they exist
|
// Clear old actions if they exist
|
||||||
if (mActions.isNotEmpty()) {
|
if (mActions.isNotEmpty()) {
|
||||||
@ -135,7 +137,7 @@ internal class BackupNotifier(private val context: Context) {
|
|||||||
|
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.restore_completed))
|
setContentTitle(context.getString(R.string.restore_completed))
|
||||||
setContentText(context.getString(R.string.restore_completed_content, timeString, errorCount))
|
setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount))
|
||||||
|
|
||||||
// Clear old actions if they exist
|
// Clear old actions if they exist
|
||||||
if (mActions.isNotEmpty()) {
|
if (mActions.isNotEmpty()) {
|
||||||
|
@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -109,6 +110,11 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private var restoreAmount = 0
|
private var restoreAmount = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of source ID to source name from backup data
|
||||||
|
*/
|
||||||
|
private var sourceMapping: Map<Long, String> = emptyMap()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List containing errors
|
* List containing errors
|
||||||
*/
|
*/
|
||||||
@ -123,14 +129,11 @@ class BackupRestoreService : Service() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
notifier = BackupNotifier(this)
|
notifier = BackupNotifier(this)
|
||||||
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
|
|
||||||
startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build())
|
startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build())
|
||||||
|
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, "${javaClass.name}:WakeLock"
|
|
||||||
)
|
|
||||||
wakeLock.acquire()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopService(name: Intent?): Boolean {
|
override fun stopService(name: Intent?): Boolean {
|
||||||
@ -177,7 +180,9 @@ class BackupRestoreService : Service() {
|
|||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
job = GlobalScope.launch(handler) {
|
job = GlobalScope.launch(handler) {
|
||||||
restoreBackup(uri)
|
if (!restoreBackup(uri)) {
|
||||||
|
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
job?.invokeOnCompletion {
|
job?.invokeOnCompletion {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
@ -191,7 +196,7 @@ class BackupRestoreService : Service() {
|
|||||||
*
|
*
|
||||||
* @param uri backup file to restore
|
* @param uri backup file to restore
|
||||||
*/
|
*/
|
||||||
private fun restoreBackup(uri: Uri) {
|
private fun restoreBackup(uri: Uri): Boolean {
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
@ -210,10 +215,17 @@ class BackupRestoreService : Service() {
|
|||||||
errors.clear()
|
errors.clear()
|
||||||
|
|
||||||
// Restore categories
|
// Restore categories
|
||||||
restoreCategories(json.get(CATEGORIES))
|
json.get(CATEGORIES)?.let { restoreCategories(it) }
|
||||||
|
|
||||||
|
// Store source mapping for error messages
|
||||||
|
sourceMapping = BackupRestoreValidator.getSourceMapping(json)
|
||||||
|
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
mangasJson.forEach {
|
mangasJson.forEach {
|
||||||
|
if (job?.isActive != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
restoreManga(it.asJsonObject)
|
restoreManga(it.asJsonObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,56 +235,58 @@ class BackupRestoreService : Service() {
|
|||||||
val logFile = writeErrorLog()
|
val logFile = writeErrorLog()
|
||||||
|
|
||||||
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreCategories(categoriesJson: JsonElement) {
|
private fun restoreCategories(categoriesJson: JsonElement) {
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
||||||
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreManga(mangaJson: JsonObject) {
|
private fun restoreManga(mangaJson: JsonObject) {
|
||||||
db.inTransaction {
|
val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
|
||||||
val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
|
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
||||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
mangaJson.get(CHAPTERS)
|
||||||
mangaJson.get(CHAPTERS)
|
?: JsonArray()
|
||||||
?: JsonArray()
|
)
|
||||||
)
|
val categories = backupManager.parser.fromJson<List<String>>(
|
||||||
val categories = backupManager.parser.fromJson<List<String>>(
|
mangaJson.get(CATEGORIES)
|
||||||
mangaJson.get(CATEGORIES)
|
?: JsonArray()
|
||||||
?: JsonArray()
|
)
|
||||||
)
|
val history = backupManager.parser.fromJson<List<DHistory>>(
|
||||||
val history = backupManager.parser.fromJson<List<DHistory>>(
|
mangaJson.get(HISTORY)
|
||||||
mangaJson.get(HISTORY)
|
?: JsonArray()
|
||||||
?: JsonArray()
|
)
|
||||||
)
|
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
||||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
mangaJson.get(TRACK)
|
||||||
mangaJson.get(TRACK)
|
?: JsonArray()
|
||||||
?: JsonArray()
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if (job?.isActive != true) {
|
try {
|
||||||
throw Exception(getString(R.string.restoring_backup_canceled))
|
val source = backupManager.sourceManager.get(manga.source)
|
||||||
|
if (source != null) {
|
||||||
|
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||||
|
} else {
|
||||||
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found_name, sourceName)}")
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
try {
|
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||||
restoreMangaData(manga, chapters, categories, history, tracks)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}")
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a manga restore observable
|
* Returns a manga restore observable
|
||||||
*
|
*
|
||||||
* @param manga manga data from json
|
* @param manga manga data from json
|
||||||
|
* @param source source to get manga data from
|
||||||
* @param chapters chapters data from json
|
* @param chapters chapters data from json
|
||||||
* @param categories categories data from json
|
* @param categories categories data from json
|
||||||
* @param history history data from json
|
* @param history history data from json
|
||||||
@ -280,23 +294,24 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private fun restoreMangaData(
|
private fun restoreMangaData(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
|
source: Source,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<String>,
|
categories: List<String>,
|
||||||
history: List<DHistory>,
|
history: List<DHistory>,
|
||||||
tracks: List<Track>
|
tracks: List<Track>
|
||||||
) {
|
) {
|
||||||
// Get source
|
|
||||||
val source = backupManager.sourceManager.getOrStub(manga.source)
|
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
if (dbManga == null) {
|
db.inTransaction {
|
||||||
// Manga not in database
|
if (dbManga == null) {
|
||||||
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
|
// Manga not in database
|
||||||
} else { // Manga in database
|
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
|
||||||
// Copy information from manga already in database
|
} else { // Manga in database
|
||||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
// Copy information from manga already in database
|
||||||
// Fetch rest of manga information
|
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks)
|
// Fetch rest of manga information
|
||||||
|
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,7 +411,7 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
||||||
return Observable.from(tracks)
|
return Observable.from(tracks)
|
||||||
.concatMap { track ->
|
.flatMap { track ->
|
||||||
val service = trackManager.getService(track.sync_id)
|
val service = trackManager.getService(track.sync_id)
|
||||||
if (service != null && service.isLogged) {
|
if (service != null && service.isLogged) {
|
||||||
service.refresh(track)
|
service.refresh(track)
|
||||||
@ -406,7 +421,7 @@ class BackupRestoreService : Service() {
|
|||||||
track
|
track
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errors.add(Date() to "${manga.title} - ${service?.name} not logged in")
|
errors.add(Date() to "${manga.title} - ${getString(R.string.tracker_not_logged_in, service?.name)}")
|
||||||
Observable.empty()
|
Observable.empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
|
||||||
|
object BackupRestoreValidator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for critical backup file data.
|
||||||
|
*
|
||||||
|
* @throws Exception if version or manga cannot be found.
|
||||||
|
* @return List of required sources.
|
||||||
|
*/
|
||||||
|
fun validate(context: Context, uri: Uri): Map<Long, String> {
|
||||||
|
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
|
val version = json.get(Backup.VERSION)
|
||||||
|
val mangasJson = json.get(Backup.MANGAS)
|
||||||
|
if (version == null || mangasJson == null) {
|
||||||
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mangasJson.asJsonArray.size() == 0) {
|
||||||
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
|
}
|
||||||
|
|
||||||
|
return getSourceMapping(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||||
|
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
||||||
|
|
||||||
|
return extensionsMapping.asJsonArray
|
||||||
|
.map {
|
||||||
|
val items = it.asString.split(":")
|
||||||
|
items[0].toLong() to items[1]
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -17,51 +18,89 @@ import java.io.InputStream
|
|||||||
*/
|
*/
|
||||||
class CoverCache(private val context: Context) {
|
class CoverCache(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val COVERS_DIR = "covers"
|
||||||
|
private const val CUSTOM_COVERS_DIR = "covers/custom"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache directory used for cache management.
|
* Cache directory used for cache management.
|
||||||
*/
|
*/
|
||||||
private val cacheDir = context.getExternalFilesDir("covers")
|
private val cacheDir = getCacheDir(COVERS_DIR)
|
||||||
?: File(context.filesDir, "covers").also { it.mkdirs() }
|
|
||||||
|
private val customCoverCacheDir = getCacheDir(CUSTOM_COVERS_DIR)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cover from cache.
|
* Returns the cover from cache.
|
||||||
*
|
*
|
||||||
* @param thumbnailUrl the thumbnail url.
|
* @param manga the manga.
|
||||||
* @return cover image.
|
* @return cover image.
|
||||||
*/
|
*/
|
||||||
fun getCoverFile(thumbnailUrl: String): File {
|
fun getCoverFile(manga: Manga): File? {
|
||||||
return File(cacheDir, DiskUtil.hashKeyForDisk(thumbnailUrl))
|
return manga.thumbnail_url?.let {
|
||||||
|
File(cacheDir, DiskUtil.hashKeyForDisk(it))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy the given stream to this cache.
|
* Returns the custom cover from cache.
|
||||||
*
|
*
|
||||||
* @param thumbnailUrl url of the thumbnail.
|
* @param manga the manga.
|
||||||
|
* @return cover image.
|
||||||
|
*/
|
||||||
|
fun getCustomCoverFile(manga: Manga): File {
|
||||||
|
return File(customCoverCacheDir, DiskUtil.hashKeyForDisk(manga.id.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the given stream as the manga's custom cover to cache.
|
||||||
|
*
|
||||||
|
* @param manga the manga.
|
||||||
* @param inputStream the stream to copy.
|
* @param inputStream the stream to copy.
|
||||||
* @throws IOException if there's any error.
|
* @throws IOException if there's any error.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
|
fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) {
|
||||||
// Get destination file.
|
getCustomCoverFile(manga).outputStream().use {
|
||||||
val destFile = getCoverFile(thumbnailUrl)
|
inputStream.copyTo(it)
|
||||||
|
}
|
||||||
destFile.outputStream().use { inputStream.copyTo(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the cover file from the cache.
|
* Delete the cover files of the manga from the cache.
|
||||||
*
|
*
|
||||||
* @param thumbnailUrl the thumbnail url.
|
* @param manga the manga.
|
||||||
* @return status of deletion.
|
* @param deleteCustomCover whether the custom cover should be deleted.
|
||||||
|
* @return number of files that were deleted.
|
||||||
*/
|
*/
|
||||||
fun deleteFromCache(thumbnailUrl: String?): Boolean {
|
fun deleteFromCache(manga: Manga, deleteCustomCover: Boolean = false): Int {
|
||||||
// Check if url is empty.
|
var deleted = 0
|
||||||
if (thumbnailUrl.isNullOrEmpty()) {
|
|
||||||
return false
|
getCoverFile(manga)?.let {
|
||||||
|
if (it.exists() && it.delete()) ++deleted
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove file.
|
if (deleteCustomCover) {
|
||||||
val file = getCoverFile(thumbnailUrl)
|
if (deleteCustomCover(manga)) ++deleted
|
||||||
return file.exists() && file.delete()
|
}
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete custom cover of the manga from the cache
|
||||||
|
*
|
||||||
|
* @param manga the manga.
|
||||||
|
* @return whether the cover was deleted.
|
||||||
|
*/
|
||||||
|
fun deleteCustomCover(manga: Manga): Boolean {
|
||||||
|
return getCustomCoverFile(manga).let {
|
||||||
|
it.exists() && it.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCacheDir(dir: String): File {
|
||||||
|
return context.getExternalFilesDir(dir)
|
||||||
|
?: File(context.filesDir, dir).also { it.mkdirs() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 9
|
const val DATABASE_VERSION = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@ -75,6 +75,13 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
db.execSQL(TrackTable.addStartDate)
|
db.execSQL(TrackTable.addStartDate)
|
||||||
db.execSQL(TrackTable.addFinishDate)
|
db.execSQL(TrackTable.addFinishDate)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 10) {
|
||||||
|
db.execSQL(MangaTable.addCoverLastModified)
|
||||||
|
}
|
||||||
|
if (oldVersion < 11) {
|
||||||
|
db.execSQL(MangaTable.addDateAdded)
|
||||||
|
db.execSQL(MangaTable.backfillDateAdded)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
@ -14,6 +14,8 @@ import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFIED
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||||
@ -46,7 +48,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
|
override fun mapToContentValues(obj: Manga) = ContentValues(17).apply {
|
||||||
put(COL_ID, obj.id)
|
put(COL_ID, obj.id)
|
||||||
put(COL_SOURCE, obj.source)
|
put(COL_SOURCE, obj.source)
|
||||||
put(COL_URL, obj.url)
|
put(COL_URL, obj.url)
|
||||||
@ -62,6 +64,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
put(COL_INITIALIZED, obj.initialized)
|
put(COL_INITIALIZED, obj.initialized)
|
||||||
put(COL_VIEWER, obj.viewer)
|
put(COL_VIEWER, obj.viewer)
|
||||||
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
||||||
|
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
|
||||||
|
put(COL_DATE_ADDED, obj.date_added)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +86,8 @@ interface BaseMangaGetResolver {
|
|||||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||||
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||||
|
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
||||||
|
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,9 +12,6 @@ interface Category : Serializable {
|
|||||||
|
|
||||||
var flags: Int
|
var flags: Int
|
||||||
|
|
||||||
val nameLower: String
|
|
||||||
get() = name.toLowerCase()
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun create(name: String): Category = CategoryImpl().apply {
|
fun create(name: String): Category = CategoryImpl().apply {
|
||||||
|
@ -15,7 +15,6 @@ class CategoryImpl : Category {
|
|||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
val category = other as Category
|
val category = other as Category
|
||||||
|
|
||||||
return name == category.name
|
return name == category.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,10 +31,11 @@ class ChapterImpl : Chapter {
|
|||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
val chapter = other as Chapter
|
val chapter = other as Chapter
|
||||||
return url == chapter.url
|
if (url != chapter.url) return false
|
||||||
|
return id == chapter.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return url.hashCode()
|
return url.hashCode() + id.hashCode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,18 +12,18 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
var last_update: Long
|
var last_update: Long
|
||||||
|
|
||||||
|
var date_added: Long
|
||||||
|
|
||||||
var viewer: Int
|
var viewer: Int
|
||||||
|
|
||||||
var chapter_flags: Int
|
var chapter_flags: Int
|
||||||
|
|
||||||
|
var cover_last_modified: Long
|
||||||
|
|
||||||
fun setChapterOrder(order: Int) {
|
fun setChapterOrder(order: Int) {
|
||||||
setFlags(order, SORT_MASK)
|
setFlags(order, SORT_MASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setFlags(flag: Int, mask: Int) {
|
|
||||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sortDescending(): Boolean {
|
fun sortDescending(): Boolean {
|
||||||
return chapter_flags and SORT_MASK == SORT_DESC
|
return chapter_flags and SORT_MASK == SORT_DESC
|
||||||
}
|
}
|
||||||
@ -32,6 +32,10 @@ interface Manga : SManga {
|
|||||||
return genre?.split(", ")?.map { it.trim() }
|
return genre?.split(", ")?.map { it.trim() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setFlags(flag: Int, mask: Int) {
|
||||||
|
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||||
|
}
|
||||||
|
|
||||||
// Used to display the chapter's title one way or another
|
// Used to display the chapter's title one way or another
|
||||||
var displayMode: Int
|
var displayMode: Int
|
||||||
get() = chapter_flags and DISPLAY_MASK
|
get() = chapter_flags and DISPLAY_MASK
|
||||||
@ -76,7 +80,8 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
const val SORTING_SOURCE = 0x00000000
|
const val SORTING_SOURCE = 0x00000000
|
||||||
const val SORTING_NUMBER = 0x00000100
|
const val SORTING_NUMBER = 0x00000100
|
||||||
const val SORTING_MASK = 0x00000100
|
const val SORTING_UPLOAD_DATE = 0x00000200
|
||||||
|
const val SORTING_MASK = 0x00000300
|
||||||
|
|
||||||
const val DISPLAY_NAME = 0x00000000
|
const val DISPLAY_NAME = 0x00000000
|
||||||
const val DISPLAY_NUMBER = 0x00100000
|
const val DISPLAY_NUMBER = 0x00100000
|
||||||
|
@ -26,22 +26,26 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var last_update: Long = 0
|
override var last_update: Long = 0
|
||||||
|
|
||||||
|
override var date_added: Long = 0
|
||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
|
|
||||||
override var viewer: Int = 0
|
override var viewer: Int = 0
|
||||||
|
|
||||||
override var chapter_flags: Int = 0
|
override var chapter_flags: Int = 0
|
||||||
|
|
||||||
|
override var cover_last_modified: Long = 0
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
val manga = other as Manga
|
val manga = other as Manga
|
||||||
|
if (url != manga.url) return false
|
||||||
return url == manga.url
|
return id == manga.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return url.hashCode()
|
return url.hashCode() + id.hashCode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.DbProvider
|
|||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||||
@ -102,6 +103,11 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaTitlePutResolver())
|
.withPutResolver(MangaTitlePutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateMangaCoverLastModified(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaCoverLastModifiedPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||||
|
|
||||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||||
|
@ -38,7 +38,9 @@ fun getRecentsQuery() =
|
|||||||
"""
|
"""
|
||||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
|
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
|
WHERE ${Manga.COL_FAVORITE} = 1
|
||||||
|
AND ${Chapter.COL_DATE_UPLOAD} > ?
|
||||||
|
AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED}
|
||||||
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
class MangaCoverLastModifiedPutResolver : PutResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
|
val contentValues = mapToContentValues(manga)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
|
put(MangaTable.COL_COVER_LAST_MODIFIED, manga.cover_last_modified)
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,8 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_LAST_UPDATE = "last_update"
|
const val COL_LAST_UPDATE = "last_update"
|
||||||
|
|
||||||
|
const val COL_DATE_ADDED = "date_added"
|
||||||
|
|
||||||
const val COL_INITIALIZED = "initialized"
|
const val COL_INITIALIZED = "initialized"
|
||||||
|
|
||||||
const val COL_VIEWER = "viewer"
|
const val COL_VIEWER = "viewer"
|
||||||
@ -38,6 +40,8 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_CATEGORY = "category"
|
const val COL_CATEGORY = "category"
|
||||||
|
|
||||||
|
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
||||||
|
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() =
|
get() =
|
||||||
"""CREATE TABLE $TABLE(
|
"""CREATE TABLE $TABLE(
|
||||||
@ -55,7 +59,9 @@ object MangaTable {
|
|||||||
$COL_LAST_UPDATE LONG,
|
$COL_LAST_UPDATE LONG,
|
||||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||||
$COL_VIEWER INTEGER NOT NULL,
|
$COL_VIEWER INTEGER NOT NULL,
|
||||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL
|
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||||
|
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
|
||||||
|
$COL_DATE_ADDED LONG NOT NULL
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
val createUrlIndexQuery: String
|
val createUrlIndexQuery: String
|
||||||
@ -64,4 +70,20 @@ object MangaTable {
|
|||||||
val createLibraryIndexQuery: String
|
val createLibraryIndexQuery: String
|
||||||
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
|
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
|
||||||
"WHERE $COL_FAVORITE = 1"
|
"WHERE $COL_FAVORITE = 1"
|
||||||
|
|
||||||
|
val addCoverLastModified: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0"
|
||||||
|
|
||||||
|
val addDateAdded: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_DATE_ADDED LONG NOT NULL DEFAULT 0"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used with addDateAdded to populate it with the oldest chapter fetch date.
|
||||||
|
*/
|
||||||
|
val backfillDateAdded: String
|
||||||
|
get() = "UPDATE $TABLE SET $COL_DATE_ADDED = " +
|
||||||
|
"(SELECT MIN(${ChapterTable.COL_DATE_FETCH}) " +
|
||||||
|
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
||||||
|
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
||||||
|
"GROUP BY $TABLE.$COL_ID)"
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -59,7 +59,7 @@ class DownloadCache(
|
|||||||
*/
|
*/
|
||||||
private fun getDirectoryFromPreference(): UniFile {
|
private fun getDirectoryFromPreference(): UniFile {
|
||||||
val dir = preferences.downloadsDirectory().get()
|
val dir = preferences.downloadsDirectory().get()
|
||||||
return UniFile.fromUri(context, Uri.parse(dir))
|
return UniFile.fromUri(context, dir.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,7 +81,7 @@ class DownloadCache(
|
|||||||
if (sourceDir != null) {
|
if (sourceDir != null) {
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
||||||
if (mangaDir != null) {
|
if (mangaDir != null) {
|
||||||
return provider.getChapterDirName(chapter) in mangaDir.files
|
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -128,7 +128,7 @@ class DownloadCache(
|
|||||||
.orEmpty()
|
.orEmpty()
|
||||||
.associate { it.name to SourceDirectory(it) }
|
.associate { it.name to SourceDirectory(it) }
|
||||||
.mapNotNullKeys { entry ->
|
.mapNotNullKeys { entry ->
|
||||||
onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id
|
onlineSources.find { provider.getSourceDirName(it).toLowerCase() == entry.key?.toLowerCase() }?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
rootDir.files = sourceDirs
|
rootDir.files = sourceDirs
|
||||||
@ -191,9 +191,10 @@ class DownloadCache(
|
|||||||
fun removeChapter(chapter: Chapter, manga: Manga) {
|
fun removeChapter(chapter: Chapter, manga: Manga) {
|
||||||
val sourceDir = rootDir.files[manga.source] ?: return
|
val sourceDir = rootDir.files[manga.source] ?: return
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
||||||
val chapterDirName = provider.getChapterDirName(chapter)
|
provider.getValidChapterDirNames(chapter).forEach {
|
||||||
if (chapterDirName in mangaDir.files) {
|
if (it in mangaDir.files) {
|
||||||
mangaDir.files -= chapterDirName
|
mangaDir.files -= it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,10 +208,11 @@ class DownloadCache(
|
|||||||
fun removeChapters(chapters: List<Chapter>, manga: Manga) {
|
fun removeChapters(chapters: List<Chapter>, manga: Manga) {
|
||||||
val sourceDir = rootDir.files[manga.source] ?: return
|
val sourceDir = rootDir.files[manga.source] ?: return
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
||||||
for (chapter in chapters) {
|
chapters.forEach { chapter ->
|
||||||
val chapterDirName = provider.getChapterDirName(chapter)
|
provider.getValidChapterDirNames(chapter).forEach {
|
||||||
if (chapterDirName in mangaDir.files) {
|
if (it in mangaDir.files) {
|
||||||
mangaDir.files -= chapterDirName
|
mangaDir.files -= it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.download
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
@ -11,6 +12,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -151,7 +153,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
.filter { "image" in it.type.orEmpty() }
|
.filter { "image" in it.type.orEmpty() }
|
||||||
|
|
||||||
if (files.isEmpty()) {
|
if (files.isEmpty()) {
|
||||||
throw Exception("Page list is empty")
|
throw Exception(context.getString(R.string.page_list_empty_error))
|
||||||
}
|
}
|
||||||
|
|
||||||
files.sortedBy { it.name }
|
files.sortedBy { it.name }
|
||||||
@ -239,4 +241,30 @@ class DownloadManager(private val context: Context) {
|
|||||||
deleteChapters(chapters, manga, source)
|
deleteChapters(chapters, manga, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames an already downloaded chapter
|
||||||
|
*
|
||||||
|
* @param source the source of the manga.
|
||||||
|
* @param manga the manga of the chapter.
|
||||||
|
* @param oldChapter the existing chapter with the old name.
|
||||||
|
* @param newChapter the target chapter with the new name.
|
||||||
|
*/
|
||||||
|
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||||
|
val oldNames = provider.getValidChapterDirNames(oldChapter)
|
||||||
|
val newName = provider.getChapterDirName(newChapter)
|
||||||
|
val mangaDir = provider.getMangaDir(manga, source)
|
||||||
|
|
||||||
|
// Assume there's only 1 version of the chapter name formats present
|
||||||
|
val oldFolder = oldNames.asSequence()
|
||||||
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
|
.firstOrNull()
|
||||||
|
|
||||||
|
if (oldFolder?.renameTo(newName) == true) {
|
||||||
|
cache.removeChapter(oldChapter, manga)
|
||||||
|
cache.addChapter(newName, mangaDir, manga)
|
||||||
|
} else {
|
||||||
|
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ import eu.kanade.tachiyomi.util.lang.chop
|
|||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
||||||
@ -23,11 +23,15 @@ import uy.kohesive.injekt.api.get
|
|||||||
*/
|
*/
|
||||||
internal class DownloadNotifier(private val context: Context) {
|
internal class DownloadNotifier(private val context: Context) {
|
||||||
|
|
||||||
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val preferences by lazy { Injekt.get<PreferencesHelper>() }
|
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_COMPLETE) {
|
||||||
|
setAutoCancel(false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of download. Used for correct notification icon.
|
* Status of download. Used for correct notification icon.
|
||||||
@ -56,7 +60,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Clear old actions if they exist.
|
* Clear old actions if they exist.
|
||||||
*/
|
*/
|
||||||
private fun clearActions() = with(notificationBuilder) {
|
private fun NotificationCompat.Builder.clearActions() {
|
||||||
if (mActions.isNotEmpty()) {
|
if (mActions.isNotEmpty()) {
|
||||||
mActions.clear()
|
mActions.clear()
|
||||||
}
|
}
|
||||||
@ -76,8 +80,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* @param download download object containing download information.
|
* @param download download object containing download information.
|
||||||
*/
|
*/
|
||||||
fun onProgressChange(download: Download) {
|
fun onProgressChange(download: Download) {
|
||||||
// Create notification
|
with(progressNotificationBuilder) {
|
||||||
with(notificationBuilder) {
|
|
||||||
// Check if first call.
|
// Check if first call.
|
||||||
if (!isDownloading) {
|
if (!isDownloading) {
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
@ -94,8 +97,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadingProgressText = context.getString(R.string.chapter_downloading_progress)
|
val downloadingProgressText = context.getString(
|
||||||
.format(download.downloadedImages, download.pages!!.size)
|
R.string.chapter_downloading_progress, download.downloadedImages, download.pages!!.size
|
||||||
|
)
|
||||||
|
|
||||||
if (preferences.hideNotificationContent()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
setContentTitle(downloadingProgressText)
|
setContentTitle(downloadingProgressText)
|
||||||
@ -109,16 +113,14 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
|
|
||||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||||
}
|
}
|
||||||
|
progressNotificationBuilder.show()
|
||||||
// Displays the progress bar on notification
|
|
||||||
notificationBuilder.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show notification when download is paused.
|
* Show notification when download is paused.
|
||||||
*/
|
*/
|
||||||
fun onDownloadPaused() {
|
fun onDownloadPaused() {
|
||||||
with(notificationBuilder) {
|
with(progressNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.chapter_paused))
|
setContentTitle(context.getString(R.string.chapter_paused))
|
||||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||||
setSmallIcon(R.drawable.ic_pause_24dp)
|
setSmallIcon(R.drawable.ic_pause_24dp)
|
||||||
@ -140,21 +142,40 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
progressNotificationBuilder.show()
|
||||||
// Show notification.
|
|
||||||
notificationBuilder.show()
|
|
||||||
|
|
||||||
// Reset initial values
|
// Reset initial values
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function shows a notification to inform download tasks are done.
|
||||||
|
*/
|
||||||
|
fun downloadFinished() {
|
||||||
|
// Create notification
|
||||||
|
with(completeNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
|
setContentText(context.getString(R.string.download_notifier_download_finish))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
clearActions()
|
||||||
|
setAutoCancel(true)
|
||||||
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
}
|
||||||
|
completeNotificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_COMPLETE)
|
||||||
|
|
||||||
|
// Reset states to default
|
||||||
|
errorThrown = false
|
||||||
|
isDownloading = false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the downloader receives a warning.
|
* Called when the downloader receives a warning.
|
||||||
*
|
*
|
||||||
* @param reason the text to show.
|
* @param reason the text to show.
|
||||||
*/
|
*/
|
||||||
fun onWarning(reason: String) {
|
fun onWarning(reason: String) {
|
||||||
with(notificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(reason)
|
setContentText(reason)
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
@ -163,7 +184,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
notificationBuilder.show()
|
completeNotificationBuilder.show()
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
@ -178,7 +199,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun onError(error: String? = null, chapter: String? = null) {
|
fun onError(error: String? = null, chapter: String? = null) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(notificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
setContentTitle(
|
setContentTitle(
|
||||||
chapter
|
chapter
|
||||||
?: context.getString(R.string.download_notifier_downloader_title)
|
?: context.getString(R.string.download_notifier_downloader_title)
|
||||||
@ -190,7 +211,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
notificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
completeNotificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
errorThrown = true
|
errorThrown = true
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
@ -22,7 +23,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Preferences used to store the list of chapters to delete.
|
* Preferences used to store the list of chapters to delete.
|
||||||
*/
|
*/
|
||||||
private val prefs = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE)
|
private val preferences = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last added chapter, used to avoid decoding from the preference too often.
|
* Last added chapter, used to avoid decoding from the preference too often.
|
||||||
@ -49,7 +50,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
// Last entry matches the manga, reuse it to avoid decoding json from preferences
|
// Last entry matches the manga, reuse it to avoid decoding json from preferences
|
||||||
lastEntry.copy(chapters = newChapters)
|
lastEntry.copy(chapters = newChapters)
|
||||||
} else {
|
} else {
|
||||||
val existingEntry = prefs.getString(manga.id!!.toString(), null)
|
val existingEntry = preferences.getString(manga.id!!.toString(), null)
|
||||||
if (existingEntry != null) {
|
if (existingEntry != null) {
|
||||||
// Existing entry found on preferences, decode json and add the new chapter
|
// Existing entry found on preferences, decode json and add the new chapter
|
||||||
val savedEntry = gson.fromJson<Entry>(existingEntry)
|
val savedEntry = gson.fromJson<Entry>(existingEntry)
|
||||||
@ -69,7 +70,9 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
|
|
||||||
// Save current state
|
// Save current state
|
||||||
val json = gson.toJson(newEntry)
|
val json = gson.toJson(newEntry)
|
||||||
prefs.edit().putString(newEntry.manga.id.toString(), json).apply()
|
preferences.edit {
|
||||||
|
putString(newEntry.manga.id.toString(), json)
|
||||||
|
}
|
||||||
lastAddedEntry = newEntry
|
lastAddedEntry = newEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +85,9 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
fun getPendingChapters(): Map<Manga, List<Chapter>> {
|
fun getPendingChapters(): Map<Manga, List<Chapter>> {
|
||||||
val entries = decodeAll()
|
val entries = decodeAll()
|
||||||
prefs.edit().clear().apply()
|
preferences.edit {
|
||||||
|
clear()
|
||||||
|
}
|
||||||
lastAddedEntry = null
|
lastAddedEntry = null
|
||||||
|
|
||||||
return entries.associate { entry ->
|
return entries.associate { entry ->
|
||||||
@ -94,7 +99,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
* Decodes all the chapters from preferences.
|
* Decodes all the chapters from preferences.
|
||||||
*/
|
*/
|
||||||
private fun decodeAll(): List<Entry> {
|
private fun decodeAll(): List<Entry> {
|
||||||
return prefs.all.values.mapNotNull { rawEntry ->
|
return preferences.all.values.mapNotNull { rawEntry ->
|
||||||
try {
|
try {
|
||||||
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
|
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -130,7 +135,8 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
private data class ChapterEntry(
|
private data class ChapterEntry(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
val name: String
|
val name: String,
|
||||||
|
val scanlator: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -154,7 +160,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
* Returns a chapter entry from a chapter model.
|
* Returns a chapter entry from a chapter model.
|
||||||
*/
|
*/
|
||||||
private fun Chapter.toEntry(): ChapterEntry {
|
private fun Chapter.toEntry(): ChapterEntry {
|
||||||
return ChapterEntry(id!!, url, name)
|
return ChapterEntry(id!!, url, name, scanlator)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -174,6 +180,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
it.id = id
|
it.id = id
|
||||||
it.url = url
|
it.url = url
|
||||||
it.name = name
|
it.name = name
|
||||||
|
it.scanlator = scanlator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
@ -32,14 +32,14 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* The root directory for downloads.
|
* The root directory for downloads.
|
||||||
*/
|
*/
|
||||||
private var downloadsDir = preferences.downloadsDirectory().get().let {
|
private var downloadsDir = preferences.downloadsDirectory().get().let {
|
||||||
val dir = UniFile.fromUri(context, Uri.parse(it))
|
val dir = UniFile.fromUri(context, it.toUri())
|
||||||
DiskUtil.createNoMediaFile(dir, context)
|
DiskUtil.createNoMediaFile(dir, context)
|
||||||
dir
|
dir
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
preferences.downloadsDirectory().asFlow()
|
preferences.downloadsDirectory().asFlow()
|
||||||
.onEach { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
.onEach { downloadsDir = UniFile.fromUri(context, it.toUri()) }
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +88,9 @@ class DownloadProvider(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||||
val mangaDir = findMangaDir(manga, source)
|
val mangaDir = findMangaDir(manga, source)
|
||||||
return mangaDir?.findFile(getChapterDirName(chapter))
|
return getValidChapterDirNames(chapter).asSequence()
|
||||||
|
.mapNotNull { mangaDir?.findFile(it) }
|
||||||
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,7 +102,11 @@ class DownloadProvider(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
|
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
|
||||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||||
return chapters.mapNotNull { mangaDir.findFile(getChapterDirName(it)) }
|
return chapters.mapNotNull { chapter ->
|
||||||
|
getValidChapterDirNames(chapter).asSequence()
|
||||||
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,6 +133,25 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param chapter the chapter to query.
|
* @param chapter the chapter to query.
|
||||||
*/
|
*/
|
||||||
fun getChapterDirName(chapter: Chapter): String {
|
fun getChapterDirName(chapter: Chapter): String {
|
||||||
return DiskUtil.buildValidFilename(chapter.name)
|
return DiskUtil.buildValidFilename(
|
||||||
|
when {
|
||||||
|
chapter.scanlator != null -> "${chapter.scanlator}_${chapter.name}"
|
||||||
|
else -> chapter.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns valid downloaded chapter directory names.
|
||||||
|
*
|
||||||
|
* @param chapter the chapter to query.
|
||||||
|
*/
|
||||||
|
fun getValidChapterDirNames(chapter: Chapter): List<String> {
|
||||||
|
return listOf(
|
||||||
|
getChapterDirName(chapter),
|
||||||
|
|
||||||
|
// Legacy chapter directory name used in v0.9.2 and before
|
||||||
|
DiskUtil.buildValidFilename(chapter.name)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.app.Notification
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkInfo.State.CONNECTED
|
import android.net.NetworkInfo.State.CONNECTED
|
||||||
import android.net.NetworkInfo.State.DISCONNECTED
|
import android.net.NetworkInfo.State.DISCONNECTED
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@ -16,9 +17,9 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.connectivityManager
|
import eu.kanade.tachiyomi.util.system.connectivityManager
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import eu.kanade.tachiyomi.util.system.powerManager
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
@ -70,9 +71,7 @@ class DownloadService : Service() {
|
|||||||
/**
|
/**
|
||||||
* Wake lock to prevent the device to enter sleep mode.
|
* Wake lock to prevent the device to enter sleep mode.
|
||||||
*/
|
*/
|
||||||
private val wakeLock by lazy {
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "DownloadService:WakeLock")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscriptions to store while the service is running.
|
* Subscriptions to store while the service is running.
|
||||||
@ -85,6 +84,7 @@ class DownloadService : Service() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
|
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
|
||||||
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
runningRelay.call(true)
|
runningRelay.call(true)
|
||||||
subscriptions = CompositeSubscription()
|
subscriptions = CompositeSubscription()
|
||||||
listenDownloaderState()
|
listenDownloaderState()
|
||||||
@ -144,7 +144,7 @@ class DownloadService : Service() {
|
|||||||
private fun onNetworkStateChanged(connectivity: Connectivity) {
|
private fun onNetworkStateChanged(connectivity: Connectivity) {
|
||||||
when (connectivity.state) {
|
when (connectivity.state) {
|
||||||
CONNECTED -> {
|
CONNECTED -> {
|
||||||
if (preferences.downloadOnlyOverWifi() && connectivityManager.isActiveNetworkMetered) {
|
if (preferences.downloadOnlyOverWifi() && connectivityManager.activeNetworkInfo?.type != ConnectivityManager.TYPE_WIFI) {
|
||||||
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
||||||
} else {
|
} else {
|
||||||
val started = downloadManager.startDownloads()
|
val started = downloadManager.startDownloads()
|
||||||
@ -176,19 +176,19 @@ class DownloadService : Service() {
|
|||||||
/**
|
/**
|
||||||
* Releases the wake lock if it's held.
|
* Releases the wake lock if it's held.
|
||||||
*/
|
*/
|
||||||
fun PowerManager.WakeLock.releaseIfNeeded() {
|
private fun PowerManager.WakeLock.releaseIfNeeded() {
|
||||||
if (isHeld) release()
|
if (isHeld) release()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Acquires the wake lock if it's not held.
|
* Acquires the wake lock if it's not held.
|
||||||
*/
|
*/
|
||||||
fun PowerManager.WakeLock.acquireIfNeeded() {
|
private fun PowerManager.WakeLock.acquireIfNeeded() {
|
||||||
if (!isHeld) acquire()
|
if (!isHeld) acquire()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPlaceholderNotification(): Notification {
|
private fun getPlaceholderNotification(): Notification {
|
||||||
return notification(Notifications.CHANNEL_DOWNLOADER) {
|
return notification(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||||
setContentTitle(getString(R.string.download_notifier_downloader_title))
|
setContentTitle(getString(R.string.download_notifier_downloader_title))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -42,9 +43,9 @@ class DownloadStore(
|
|||||||
* @param downloads the list of downloads to add.
|
* @param downloads the list of downloads to add.
|
||||||
*/
|
*/
|
||||||
fun addAll(downloads: List<Download>) {
|
fun addAll(downloads: List<Download>) {
|
||||||
val editor = preferences.edit()
|
preferences.edit {
|
||||||
downloads.forEach { editor.putString(getKey(it), serialize(it)) }
|
downloads.forEach { putString(getKey(it), serialize(it)) }
|
||||||
editor.apply()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,14 +54,18 @@ class DownloadStore(
|
|||||||
* @param download the download to remove.
|
* @param download the download to remove.
|
||||||
*/
|
*/
|
||||||
fun remove(download: Download) {
|
fun remove(download: Download) {
|
||||||
preferences.edit().remove(getKey(download)).apply()
|
preferences.edit {
|
||||||
|
remove(getKey(download))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all the downloads from the store.
|
* Removes all the downloads from the store.
|
||||||
*/
|
*/
|
||||||
fun clear() {
|
fun clear() {
|
||||||
preferences.edit().clear().apply()
|
preferences.edit {
|
||||||
|
clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,6 +5,7 @@ import android.webkit.MimeTypeMap
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -116,6 +117,8 @@ class Downloader(
|
|||||||
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
||||||
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
||||||
|
|
||||||
|
notifier.paused = false
|
||||||
|
|
||||||
downloadsRelay.call(pending)
|
downloadsRelay.call(pending)
|
||||||
return pending.isNotEmpty()
|
return pending.isNotEmpty()
|
||||||
}
|
}
|
||||||
@ -136,7 +139,7 @@ class Downloader(
|
|||||||
notifier.paused = false
|
notifier.paused = false
|
||||||
notifier.onDownloadPaused()
|
notifier.onDownloadPaused()
|
||||||
} else {
|
} else {
|
||||||
notifier.dismiss()
|
notifier.downloadFinished()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,8 +184,17 @@ class Downloader(
|
|||||||
subscriptions.clear()
|
subscriptions.clear()
|
||||||
|
|
||||||
subscriptions += downloadsRelay.concatMapIterable { it }
|
subscriptions += downloadsRelay.concatMapIterable { it }
|
||||||
.concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) }
|
// Concurrently download from 5 different sources
|
||||||
.onBackpressureBuffer()
|
.groupBy { it.source }
|
||||||
|
.flatMap(
|
||||||
|
{ bySource ->
|
||||||
|
bySource.concatMap { download ->
|
||||||
|
downloadChapter(download).subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
5
|
||||||
|
)
|
||||||
|
.onBackpressureLatest()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{
|
{
|
||||||
@ -219,13 +231,9 @@ class Downloader(
|
|||||||
val wasEmpty = queue.isEmpty()
|
val wasEmpty = queue.isEmpty()
|
||||||
// Called in background thread, the operation can be slow with SAF.
|
// Called in background thread, the operation can be slow with SAF.
|
||||||
val chaptersWithoutDir = async {
|
val chaptersWithoutDir = async {
|
||||||
val mangaDir = provider.findMangaDir(manga, source)
|
|
||||||
|
|
||||||
chapters
|
chapters
|
||||||
// Avoid downloading chapters with the same name.
|
|
||||||
.distinctBy { it.name }
|
|
||||||
// Filter out those already downloaded.
|
// Filter out those already downloaded.
|
||||||
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
|
.filter { provider.findChapterDir(it, manga, source) == null }
|
||||||
// Add chapters to queue from the start.
|
// Add chapters to queue from the start.
|
||||||
.sortedByDescending { it.source_order }
|
.sortedByDescending { it.source_order }
|
||||||
}
|
}
|
||||||
@ -260,6 +268,13 @@ class Downloader(
|
|||||||
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
||||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||||
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
||||||
|
|
||||||
|
if (DiskUtil.getAvailableStorageSpace(mangaDir) < MIN_DISK_SPACE) {
|
||||||
|
download.status = Download.ERROR
|
||||||
|
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
||||||
|
return@defer Observable.just(download)
|
||||||
|
}
|
||||||
|
|
||||||
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
||||||
|
|
||||||
val pageListObservable = if (download.pages == null) {
|
val pageListObservable = if (download.pages == null) {
|
||||||
@ -267,7 +282,7 @@ class Downloader(
|
|||||||
download.source.fetchPageList(download.chapter)
|
download.source.fetchPageList(download.chapter)
|
||||||
.doOnNext { pages ->
|
.doOnNext { pages ->
|
||||||
if (pages.isEmpty()) {
|
if (pages.isEmpty()) {
|
||||||
throw Exception("Page list is empty")
|
throw Exception(context.getString(R.string.page_list_empty_error))
|
||||||
}
|
}
|
||||||
download.pages = pages
|
download.pages = pages
|
||||||
}
|
}
|
||||||
@ -289,7 +304,9 @@ class Downloader(
|
|||||||
// Get all the URLs to the source images, fetch pages if necessary
|
// Get all the URLs to the source images, fetch pages if necessary
|
||||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||||
// Start downloading images, consider we can have downloaded images already
|
// Start downloading images, consider we can have downloaded images already
|
||||||
.concatMap { page -> getOrDownloadImage(page, download, tmpDir) }
|
// Concurrently do 5 pages at a time
|
||||||
|
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir) }, 5)
|
||||||
|
.onBackpressureLatest()
|
||||||
// Do when page is downloaded.
|
// Do when page is downloaded.
|
||||||
.doOnNext { notifier.onProgressChange(download) }
|
.doOnNext { notifier.onProgressChange(download) }
|
||||||
.toList()
|
.toList()
|
||||||
@ -475,5 +492,8 @@ class Downloader(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TMP_DIR_SUFFIX = "_tmp"
|
const val TMP_DIR_SUFFIX = "_tmp"
|
||||||
|
|
||||||
|
// Arbitrary minimum required space to start a download: 50 MB
|
||||||
|
const val MIN_DISK_SPACE = 50 * 1024 * 1024
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import java.io.IOException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
|
||||||
|
|
||||||
private var data: InputStream? = null
|
private var data: InputStream? = null
|
||||||
|
|
||||||
@ -20,7 +20,11 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
|||||||
loadFromFile(callback)
|
loadFromFile(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
private fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
|
loadFromFile(File(filePath), callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun loadFromFile(file: File, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
try {
|
try {
|
||||||
data = FileInputStream(file)
|
data = FileInputStream(file)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import com.bumptech.glide.Priority
|
||||||
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
open class LibraryMangaCustomCoverFetcher(
|
||||||
|
private val manga: Manga,
|
||||||
|
private val coverCache: CoverCache
|
||||||
|
) : FileFetcher() {
|
||||||
|
|
||||||
|
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
|
getCustomCoverFile()?.let {
|
||||||
|
loadFromFile(it, callback)
|
||||||
|
} ?: callback.onLoadFailed(Exception("Custom cover file not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getCustomCoverFile(): File? {
|
||||||
|
return coverCache.getCustomCoverFile(manga).takeIf { it.exists() }
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.glide
|
|||||||
|
|
||||||
import com.bumptech.glide.Priority
|
import com.bumptech.glide.Priority
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
@ -19,31 +20,41 @@ import java.io.InputStream
|
|||||||
class LibraryMangaUrlFetcher(
|
class LibraryMangaUrlFetcher(
|
||||||
private val networkFetcher: DataFetcher<InputStream>,
|
private val networkFetcher: DataFetcher<InputStream>,
|
||||||
private val manga: Manga,
|
private val manga: Manga,
|
||||||
private val file: File
|
private val coverCache: CoverCache
|
||||||
) :
|
) : LibraryMangaCustomCoverFetcher(manga, coverCache) {
|
||||||
FileFetcher(file) {
|
|
||||||
|
|
||||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
if (!file.exists()) {
|
getCustomCoverFile()?.let {
|
||||||
|
loadFromFile(it, callback)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val cover = coverCache.getCoverFile(manga)
|
||||||
|
if (cover == null) {
|
||||||
|
callback.onLoadFailed(Exception("Null thumbnail url"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cover.exists()) {
|
||||||
networkFetcher.loadData(
|
networkFetcher.loadData(
|
||||||
priority,
|
priority,
|
||||||
object : DataFetcher.DataCallback<InputStream> {
|
object : DataFetcher.DataCallback<InputStream> {
|
||||||
override fun onDataReady(data: InputStream?) {
|
override fun onDataReady(data: InputStream?) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
val tmpFile = File(file.path + ".tmp")
|
val tmpFile = File(cover.path + ".tmp")
|
||||||
try {
|
try {
|
||||||
// Retrieve destination stream, create parent folders if needed.
|
// Retrieve destination stream, create parent folders if needed.
|
||||||
val output = try {
|
val output = try {
|
||||||
tmpFile.outputStream()
|
tmpFile.outputStream()
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
tmpFile.parentFile.mkdirs()
|
tmpFile.parentFile!!.mkdirs()
|
||||||
tmpFile.outputStream()
|
tmpFile.outputStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the file and rename to the original.
|
// Copy the file and rename to the original.
|
||||||
data.use { output.use { data.copyTo(output) } }
|
data.use { output.use { data.copyTo(output) } }
|
||||||
tmpFile.renameTo(file)
|
tmpFile.renameTo(cover)
|
||||||
loadFromFile(callback)
|
loadFromFile(cover, callback)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
tmpFile.delete()
|
tmpFile.delete()
|
||||||
callback.onLoadFailed(e)
|
callback.onLoadFailed(e)
|
||||||
@ -59,7 +70,7 @@ class LibraryMangaUrlFetcher(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
loadFromFile(callback)
|
loadFromFile(cover, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
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,7 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import com.bumptech.glide.load.Key
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
data class MangaThumbnail(val manga: Manga, val url: String?)
|
data class MangaThumbnail(val manga: Manga, val coverLastModified: Long) : Key {
|
||||||
|
val key = manga.url + coverLastModified
|
||||||
|
|
||||||
fun Manga.toMangaThumbnail() = MangaThumbnail(this, this.thumbnail_url)
|
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||||
|
messageDigest.update(key.toByteArray(Key.CHARSET))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Manga.toMangaThumbnail() = MangaThumbnail(this, cover_last_modified)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
import android.util.LruCache
|
|
||||||
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
||||||
import com.bumptech.glide.load.Options
|
import com.bumptech.glide.load.Options
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
@ -14,7 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import java.io.File
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -48,12 +47,6 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
|||||||
*/
|
*/
|
||||||
private val defaultClient = Injekt.get<NetworkHelper>().client
|
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<GlideUrl, File>(100)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map where request headers are stored for a source.
|
* Map where request headers are stored for a source.
|
||||||
*/
|
*/
|
||||||
@ -78,7 +71,7 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
|||||||
/**
|
/**
|
||||||
* Returns a fetcher for the given manga or null if the url is empty.
|
* Returns a fetcher for the given manga or null if the url is empty.
|
||||||
*
|
*
|
||||||
* @param manga the model.
|
* @param mangaThumbnail the model.
|
||||||
* @param width the width of the view where the resource will be loaded.
|
* @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.
|
* @param height the height of the view where the resource will be loaded.
|
||||||
*/
|
*/
|
||||||
@ -88,13 +81,16 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
|||||||
height: Int,
|
height: Int,
|
||||||
options: Options
|
options: Options
|
||||||
): ModelLoader.LoadData<InputStream>? {
|
): ModelLoader.LoadData<InputStream>? {
|
||||||
// Check thumbnail is not null or empty
|
|
||||||
val url = mangaThumbnail.url
|
|
||||||
if (url == null || url.isEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val manga = mangaThumbnail.manga
|
val manga = mangaThumbnail.manga
|
||||||
|
val url = manga.thumbnail_url
|
||||||
|
|
||||||
|
if (url.isNullOrEmpty()) {
|
||||||
|
return if (!manga.favorite || manga.isLocal()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
ModelLoader.LoadData(mangaThumbnail, LibraryMangaCustomCoverFetcher(manga, coverCache))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (url.startsWith("http", true)) {
|
if (url.startsWith("http", true)) {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
@ -107,19 +103,13 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
|||||||
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain the file for this url from the LRU cache, or retrieve and add it to the cache.
|
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, coverCache)
|
||||||
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 an instance of the fetcher providing the needed elements.
|
||||||
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
|
return ModelLoader.LoadData(mangaThumbnail, libraryFetcher)
|
||||||
} else {
|
} 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 an instance of the fetcher providing the needed elements.
|
||||||
return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
|
return ModelLoader.LoadData(mangaThumbnail, FileFetcher(url.removePrefix("file://")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,15 +131,4 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
|||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -36,12 +36,20 @@ class TachiGlideModule : AppGlideModule() {
|
|||||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||||
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
||||||
|
|
||||||
registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
|
registry.replace(
|
||||||
registry.append(MangaThumbnail::class.java, InputStream::class.java, MangaThumbnailModelLoader.Factory())
|
GlideUrl::class.java,
|
||||||
|
InputStream::class.java,
|
||||||
|
networkFactory
|
||||||
|
)
|
||||||
registry.append(
|
registry.append(
|
||||||
InputStream::class.java, InputStream::class.java,
|
MangaThumbnail::class.java,
|
||||||
PassthroughModelLoader
|
InputStream::class.java,
|
||||||
.Factory()
|
MangaThumbnailModelLoader.Factory()
|
||||||
|
)
|
||||||
|
registry.append(
|
||||||
|
InputStream::class.java,
|
||||||
|
InputStream::class.java,
|
||||||
|
PassthroughModelLoader.Factory()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
Worker(context, workerParams) {
|
Worker(context, workerParams) {
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
LibraryUpdateService.start(context)
|
return if (LibraryUpdateService.start(context)) {
|
||||||
return Result.success()
|
Result.success()
|
||||||
|
} else {
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -0,0 +1,306 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||||
|
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.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import java.text.DecimalFormatSymbols
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class LibraryUpdateNotifier(private val context: Context) {
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pending intent of action that cancels the library update
|
||||||
|
*/
|
||||||
|
private val cancelIntent by lazy {
|
||||||
|
NotificationReceiver.cancelLibraryUpdatePendingBroadcast(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bitmap of the app for notifications.
|
||||||
|
*/
|
||||||
|
private val notificationBitmap by lazy {
|
||||||
|
BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached progress notification to avoid creating a lot.
|
||||||
|
*/
|
||||||
|
val progressNotificationBuilder by lazy {
|
||||||
|
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
|
||||||
|
setContentTitle(context.getString(R.string.app_name))
|
||||||
|
setSmallIcon(R.drawable.ic_refresh_24dp)
|
||||||
|
setLargeIcon(notificationBitmap)
|
||||||
|
setOngoing(true)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
addAction(R.drawable.ic_close_24dp, context.getString(android.R.string.cancel), cancelIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the notification containing the currently updating manga and the progress.
|
||||||
|
*
|
||||||
|
* @param manga the manga that's being updated.
|
||||||
|
* @param current the current progress.
|
||||||
|
* @param total the total progress.
|
||||||
|
*/
|
||||||
|
fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
||||||
|
val title = if (preferences.hideNotificationContent()) {
|
||||||
|
context.getString(R.string.notification_check_updates)
|
||||||
|
} else {
|
||||||
|
manga.title
|
||||||
|
}
|
||||||
|
|
||||||
|
context.notificationManager.notify(
|
||||||
|
Notifications.ID_LIBRARY_PROGRESS,
|
||||||
|
progressNotificationBuilder
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setProgress(total, current, false)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows notification containing update entries that failed with action to open full log.
|
||||||
|
*
|
||||||
|
* @param errors List of entry titles that failed to update.
|
||||||
|
* @param uri Uri for error log file containing all titles that failed.
|
||||||
|
*/
|
||||||
|
fun showUpdateErrorNotification(errors: List<String>, uri: Uri) {
|
||||||
|
if (errors.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
context.notificationManager.notify(
|
||||||
|
Notifications.ID_LIBRARY_ERROR,
|
||||||
|
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
|
||||||
|
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
|
||||||
|
setStyle(
|
||||||
|
NotificationCompat.BigTextStyle().bigText(
|
||||||
|
errors.joinToString("\n") {
|
||||||
|
it.chop(NOTIF_TITLE_MAX_LEN)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
|
||||||
|
val errorLogIntent = NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||||
|
|
||||||
|
setContentIntent(errorLogIntent)
|
||||||
|
addAction(
|
||||||
|
R.drawable.nnf_ic_file_folder,
|
||||||
|
context.getString(R.string.action_open_log),
|
||||||
|
errorLogIntent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the notification containing the result of the update done by the service.
|
||||||
|
*
|
||||||
|
* @param updates a list of manga with new updates.
|
||||||
|
*/
|
||||||
|
fun showUpdateNotifications(updates: List<Pair<Manga, Array<Chapter>>>) {
|
||||||
|
if (updates.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).apply {
|
||||||
|
// Parent group notification
|
||||||
|
notify(
|
||||||
|
Notifications.ID_NEW_CHAPTERS,
|
||||||
|
context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||||
|
setContentTitle(context.getString(R.string.notification_new_chapters))
|
||||||
|
if (updates.size == 1 && !preferences.hideNotificationContent()) {
|
||||||
|
setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN))
|
||||||
|
} else {
|
||||||
|
setContentText(context.resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size))
|
||||||
|
|
||||||
|
if (!preferences.hideNotificationContent()) {
|
||||||
|
setStyle(
|
||||||
|
NotificationCompat.BigTextStyle().bigText(
|
||||||
|
updates.joinToString("\n") {
|
||||||
|
it.first.title.chop(NOTIF_TITLE_MAX_LEN)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
setLargeIcon(notificationBitmap)
|
||||||
|
|
||||||
|
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
||||||
|
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
||||||
|
setGroupSummary(true)
|
||||||
|
priority = NotificationCompat.PRIORITY_HIGH
|
||||||
|
|
||||||
|
setContentIntent(getNotificationIntent())
|
||||||
|
setAutoCancel(true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Per-manga notification
|
||||||
|
if (!preferences.hideNotificationContent()) {
|
||||||
|
updates.forEach {
|
||||||
|
val (manga, chapters) = it
|
||||||
|
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
|
||||||
|
return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||||
|
setContentTitle(manga.title)
|
||||||
|
|
||||||
|
val description = getNewChaptersDescription(chapters)
|
||||||
|
setContentText(description)
|
||||||
|
setStyle(NotificationCompat.BigTextStyle().bigText(description))
|
||||||
|
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
|
||||||
|
val icon = getMangaIcon(manga)
|
||||||
|
if (icon != null) {
|
||||||
|
setLargeIcon(icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
||||||
|
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
||||||
|
priority = NotificationCompat.PRIORITY_HIGH
|
||||||
|
|
||||||
|
// Open first chapter on tap
|
||||||
|
setContentIntent(NotificationReceiver.openChapterPendingActivity(context, manga, chapters.first()))
|
||||||
|
setAutoCancel(true)
|
||||||
|
|
||||||
|
// Mark chapters as read action
|
||||||
|
addAction(
|
||||||
|
R.drawable.ic_glasses_black_24dp, context.getString(R.string.action_mark_as_read),
|
||||||
|
NotificationReceiver.markAsReadPendingBroadcast(
|
||||||
|
context,
|
||||||
|
manga, chapters, Notifications.ID_NEW_CHAPTERS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// View chapters action
|
||||||
|
addAction(
|
||||||
|
R.drawable.ic_book_24dp, context.getString(R.string.action_view_chapters),
|
||||||
|
NotificationReceiver.openChapterPendingActivity(
|
||||||
|
context,
|
||||||
|
manga, Notifications.ID_NEW_CHAPTERS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the progress notification.
|
||||||
|
*/
|
||||||
|
fun cancelProgressNotification() {
|
||||||
|
context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMangaIcon(manga: Manga): Bitmap? {
|
||||||
|
return try {
|
||||||
|
Glide.with(context)
|
||||||
|
.asBitmap()
|
||||||
|
.load(manga.toMangaThumbnail())
|
||||||
|
.dontTransform()
|
||||||
|
.centerCrop()
|
||||||
|
.circleCrop()
|
||||||
|
.override(
|
||||||
|
NOTIF_ICON_SIZE,
|
||||||
|
NOTIF_ICON_SIZE
|
||||||
|
)
|
||||||
|
.submit()
|
||||||
|
.get()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
|
||||||
|
val formatter = DecimalFormat(
|
||||||
|
"#.###",
|
||||||
|
DecimalFormatSymbols()
|
||||||
|
.apply { decimalSeparator = '.' }
|
||||||
|
)
|
||||||
|
|
||||||
|
val displayableChapterNumbers = chapters
|
||||||
|
.filter { it.isRecognizedNumber }
|
||||||
|
.sortedBy { it.chapter_number }
|
||||||
|
.map { formatter.format(it.chapter_number) }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
return when (displayableChapterNumbers.size) {
|
||||||
|
// No sensible chapter numbers to show (i.e. no chapters have parsed chapter number)
|
||||||
|
0 -> {
|
||||||
|
// "1 new chapter" or "5 new chapters"
|
||||||
|
context.resources.getQuantityString(R.plurals.notification_chapters_generic, chapters.size, chapters.size)
|
||||||
|
}
|
||||||
|
// Only 1 chapter has a parsed chapter number
|
||||||
|
1 -> {
|
||||||
|
val remaining = chapters.size - displayableChapterNumbers.size
|
||||||
|
if (remaining == 0) {
|
||||||
|
// "Chapter 2.5"
|
||||||
|
context.resources.getString(R.string.notification_chapters_single, displayableChapterNumbers.first())
|
||||||
|
} else {
|
||||||
|
// "Chapter 2.5 and 10 more"
|
||||||
|
context.resources.getString(R.string.notification_chapters_single_and_more, displayableChapterNumbers.first(), remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Everything else (i.e. multiple parsed chapter numbers)
|
||||||
|
else -> {
|
||||||
|
val shouldTruncate = displayableChapterNumbers.size > NOTIF_MAX_CHAPTERS
|
||||||
|
if (shouldTruncate) {
|
||||||
|
// "Chapters 1, 2.5, 3, 4, 5 and 10 more"
|
||||||
|
val remaining = displayableChapterNumbers.size - NOTIF_MAX_CHAPTERS
|
||||||
|
val joinedChapterNumbers = displayableChapterNumbers.take(NOTIF_MAX_CHAPTERS).joinToString(", ")
|
||||||
|
context.resources.getQuantityString(R.plurals.notification_chapters_multiple_and_more, remaining, joinedChapterNumbers, remaining)
|
||||||
|
} else {
|
||||||
|
// "Chapters 1, 2.5, 3"
|
||||||
|
context.resources.getString(R.string.notification_chapters_multiple, displayableChapterNumbers.joinToString(", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an intent to open the main activity.
|
||||||
|
*/
|
||||||
|
private fun getNotificationIntent(): PendingIntent {
|
||||||
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
|
action = MainActivity.SHORTCUT_RECENTLY_UPDATED
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NOTIF_MAX_CHAPTERS = 5
|
||||||
|
private const val NOTIF_TITLE_MAX_LEN = 45
|
||||||
|
private const val NOTIF_ICON_SIZE = 192
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.library
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class will provide various functions to Rank mangas to efficiently schedule mangas to update.
|
* This class will provide various functions to rank manga to efficiently schedule manga to update.
|
||||||
*/
|
*/
|
||||||
object LibraryUpdateRanker {
|
object LibraryUpdateRanker {
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ object LibraryUpdateRanker {
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a total ordering over all the Mangas.
|
* Provides a total ordering over all the [Manga]s.
|
||||||
*
|
*
|
||||||
* Assumption: An active [Manga] mActive is expected to have been last updated after an
|
* Assumption: An active [Manga] mActive is expected to have been last updated after an
|
||||||
* inactive [Manga] mInactive.
|
* inactive [Manga] mInactive.
|
||||||
@ -21,23 +21,19 @@ object LibraryUpdateRanker {
|
|||||||
* Using this insight, function returns a Comparator for which mActive appears before mInactive.
|
* Using this insight, function returns a Comparator for which mActive appears before mInactive.
|
||||||
* @return a Comparator that ranks manga based on relevance.
|
* @return a Comparator that ranks manga based on relevance.
|
||||||
*/
|
*/
|
||||||
fun latestFirstRanking(): Comparator<Manga> {
|
private fun latestFirstRanking(): Comparator<Manga> =
|
||||||
return Comparator { mangaFirst: Manga,
|
Comparator { first: Manga, second: Manga ->
|
||||||
mangaSecond: Manga ->
|
compareValues(second.last_update, first.last_update)
|
||||||
compareValues(mangaSecond.last_update, mangaFirst.last_update)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a total ordering over all the Mangas.
|
* Provides a total ordering over all the [Manga]s.
|
||||||
*
|
*
|
||||||
* Order the manga lexicographically.
|
* Order the manga lexicographically.
|
||||||
* @return a Comparator that ranks manga lexicographically based on the title.
|
* @return a Comparator that ranks manga lexicographically based on the title.
|
||||||
*/
|
*/
|
||||||
fun lexicographicRanking(): Comparator<Manga> {
|
private fun lexicographicRanking(): Comparator<Manga> =
|
||||||
return Comparator { mangaFirst: Manga,
|
Comparator { first: Manga, second: Manga ->
|
||||||
mangaSecond: Manga ->
|
compareValues(first.title, second.title)
|
||||||
compareValues(mangaFirst.title, mangaSecond.title)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.app.NotificationCompat
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
@ -22,26 +14,20 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.lang.chop
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import java.io.File
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
import java.text.DecimalFormatSymbols
|
|
||||||
import java.util.ArrayList
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
@ -63,7 +49,8 @@ class LibraryUpdateService(
|
|||||||
val sourceManager: SourceManager = Injekt.get(),
|
val sourceManager: SourceManager = Injekt.get(),
|
||||||
val preferences: PreferencesHelper = Injekt.get(),
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
val downloadManager: DownloadManager = Injekt.get(),
|
val downloadManager: DownloadManager = Injekt.get(),
|
||||||
val trackManager: TrackManager = Injekt.get()
|
val trackManager: TrackManager = Injekt.get(),
|
||||||
|
val coverCache: CoverCache = Injekt.get()
|
||||||
) : Service() {
|
) : Service() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,45 +58,19 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
|
private lateinit var notifier: LibraryUpdateNotifier
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription where the update is done.
|
* Subscription where the update is done.
|
||||||
*/
|
*/
|
||||||
private var subscription: Subscription? = null
|
private var subscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Pending intent of action that cancels the library update
|
|
||||||
*/
|
|
||||||
private val cancelIntent by lazy {
|
|
||||||
NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bitmap of the app for notifications.
|
|
||||||
*/
|
|
||||||
private val notificationBitmap by lazy {
|
|
||||||
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cached progress notification to avoid creating a lot.
|
|
||||||
*/
|
|
||||||
private val progressNotificationBuilder by lazy {
|
|
||||||
notificationBuilder(Notifications.CHANNEL_LIBRARY) {
|
|
||||||
setContentTitle(getString(R.string.app_name))
|
|
||||||
setSmallIcon(R.drawable.ic_refresh_24dp)
|
|
||||||
setLargeIcon(notificationBitmap)
|
|
||||||
setOngoing(true)
|
|
||||||
setOnlyAlertOnce(true)
|
|
||||||
addAction(R.drawable.ic_close_24dp, getString(android.R.string.cancel), cancelIntent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines what should be updated within a service execution.
|
* Defines what should be updated within a service execution.
|
||||||
*/
|
*/
|
||||||
enum class Target {
|
enum class Target {
|
||||||
CHAPTERS, // Manga chapters
|
CHAPTERS, // Manga chapters
|
||||||
DETAILS, // Manga metadata
|
COVERS, // Manga covers
|
||||||
TRACKING // Tracking metadata
|
TRACKING // Tracking metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,10 +86,6 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
const val KEY_TARGET = "target"
|
const val KEY_TARGET = "target"
|
||||||
|
|
||||||
private const val NOTIF_MAX_CHAPTERS = 5
|
|
||||||
private const val NOTIF_TITLE_MAX_LEN = 45
|
|
||||||
private const val NOTIF_ICON_SIZE = 192
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the status of the service.
|
* Returns the status of the service.
|
||||||
*
|
*
|
||||||
@ -182,11 +139,11 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotificationBuilder.build())
|
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
notifier = LibraryUpdateNotifier(this)
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock"
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
)
|
|
||||||
wakeLock.acquire()
|
startForeground(Notifications.ID_LIBRARY_PROGRESS, notifier.progressNotificationBuilder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -234,7 +191,7 @@ class LibraryUpdateService(
|
|||||||
// Update either chapter list or manga details.
|
// Update either chapter list or manga details.
|
||||||
when (target) {
|
when (target) {
|
||||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||||
Target.DETAILS -> updateDetails(mangaList)
|
Target.COVERS -> updateCovers(mangaList)
|
||||||
Target.TRACKING -> updateTrackings(mangaList)
|
Target.TRACKING -> updateTrackings(mangaList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -296,36 +253,28 @@ class LibraryUpdateService(
|
|||||||
// Initialize the variables holding the progress of the updates.
|
// Initialize the variables holding the progress of the updates.
|
||||||
val count = AtomicInteger(0)
|
val count = AtomicInteger(0)
|
||||||
// List containing new updates
|
// List containing new updates
|
||||||
val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
||||||
// List containing failed updates
|
// List containing failed updates
|
||||||
val failedUpdates = ArrayList<Manga>()
|
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
||||||
// List containing categories that get included in downloads.
|
|
||||||
val categoriesToDownload = preferences.downloadNewCategories().get().map(String::toInt)
|
|
||||||
// Boolean to determine if user wants to automatically download new chapters.
|
|
||||||
val downloadNew = preferences.downloadNew().get()
|
|
||||||
// Boolean to determine if DownloadManager has downloads
|
// Boolean to determine if DownloadManager has downloads
|
||||||
var hasDownloads = false
|
var hasDownloads = false
|
||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
.doOnNext { notifier.showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
||||||
// Update the chapters of the manga.
|
// Update the chapters of the manga
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
updateManga(manga)
|
updateManga(manga)
|
||||||
// If there's any error, return empty update and continue.
|
// If there's any error, return empty update and continue.
|
||||||
.onErrorReturn {
|
.onErrorReturn {
|
||||||
failedUpdates.add(manga)
|
failedUpdates.add(Pair(manga, it.message))
|
||||||
Pair(emptyList(), emptyList())
|
Pair(emptyList(), emptyList())
|
||||||
}
|
}
|
||||||
// Filter out mangas without new chapters (or failed).
|
// Filter out mangas without new chapters (or failed).
|
||||||
.filter { pair -> pair.first.isNotEmpty() }
|
.filter { pair -> pair.first.isNotEmpty() }
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
if (downloadNew && (
|
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||||
categoriesToDownload.isEmpty() ||
|
|
||||||
manga.category in categoriesToDownload
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
downloadChapters(manga, it.first)
|
downloadChapters(manga, it.first)
|
||||||
hasDownloads = true
|
hasDownloads = true
|
||||||
}
|
}
|
||||||
@ -334,7 +283,10 @@ class LibraryUpdateService(
|
|||||||
.map {
|
.map {
|
||||||
Pair(
|
Pair(
|
||||||
manga,
|
manga,
|
||||||
(it.first.sortedByDescending { ch -> ch.source_order }.toTypedArray())
|
(
|
||||||
|
it.first.sortedByDescending { ch -> ch.source_order }
|
||||||
|
.toTypedArray()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -345,31 +297,30 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
// Notify result of the overall update.
|
// Notify result of the overall update.
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
|
notifier.cancelProgressNotification()
|
||||||
|
|
||||||
if (newUpdates.isNotEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
showUpdateNotifications(newUpdates)
|
notifier.showUpdateNotifications(newUpdates)
|
||||||
if (downloadNew && hasDownloads) {
|
if (hasDownloads) {
|
||||||
DownloadService.start(this)
|
DownloadService.start(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failedUpdates.isNotEmpty()) {
|
if (preferences.showLibraryUpdateErrors() && failedUpdates.isNotEmpty()) {
|
||||||
Timber.e("Failed updating: ${failedUpdates.map { it.title }}")
|
val errorFile = writeErrorFile(failedUpdates)
|
||||||
|
notifier.showUpdateErrorNotification(
|
||||||
|
failedUpdates.map { it.first.title },
|
||||||
|
errorFile.getUriCompat(this)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelProgressNotification()
|
|
||||||
}
|
}
|
||||||
.map { manga -> manga.first }
|
.map { manga -> manga.first }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
// we need to get the chapters from the db so we have chapter ids
|
|
||||||
val mangaChapters = db.getChapters(manga).executeAsBlocking()
|
|
||||||
val dbChapters = chapters.map {
|
|
||||||
mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!!
|
|
||||||
}
|
|
||||||
// We don't want to start downloading while the library is updating, because websites
|
// We don't want to start downloading while the library is updating, because websites
|
||||||
// may don't like it and they could ban the user.
|
// may don't like it and they could ban the user.
|
||||||
downloadManager.downloadChapters(manga, dbChapters, false)
|
downloadManager.downloadChapters(manga, chapters, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -379,41 +330,56 @@ class LibraryUpdateService(
|
|||||||
* @return a pair of the inserted and removed chapters.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
|
||||||
|
// Update manga details metadata in the background
|
||||||
|
if (preferences.autoUpdateMetadata()) {
|
||||||
|
source.fetchMangaDetails(manga)
|
||||||
|
.map { updatedManga ->
|
||||||
|
// Avoid "losing" existing cover
|
||||||
|
if (!updatedManga.thumbnail_url.isNullOrEmpty()) {
|
||||||
|
manga.prepUpdateCover(coverCache, updatedManga, false)
|
||||||
|
} else {
|
||||||
|
updatedManga.thumbnail_url = manga.thumbnail_url
|
||||||
|
}
|
||||||
|
|
||||||
|
manga.copyFrom(updatedManga)
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
.onErrorResumeNext { Observable.just(manga) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
return source.fetchChapterList(manga)
|
return source.fetchChapterList(manga)
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun updateCovers(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||||
* Method that updates the details of the given list of manga. It's called in a background
|
var count = 0
|
||||||
* thread, so it's safe to do heavy operations or network calls here.
|
|
||||||
*
|
|
||||||
* @param mangaToUpdate the list to update
|
|
||||||
* @return an observable delivering the progress of each update.
|
|
||||||
*/
|
|
||||||
fun updateDetails(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
|
||||||
// Initialize the variables holding the progress of the updates.
|
|
||||||
val count = AtomicInteger(0)
|
|
||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
.doOnNext {
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
notifier.showProgressNotification(it, count++, mangaToUpdate.size)
|
||||||
// Update the details of the manga.
|
}
|
||||||
.concatMap { manga ->
|
.flatMap { manga ->
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source)
|
||||||
?: return@concatMap Observable.empty<LibraryManga>()
|
?: return@flatMap Observable.empty<LibraryManga>()
|
||||||
|
|
||||||
source.fetchMangaDetails(manga)
|
source.fetchMangaDetails(manga)
|
||||||
.map { networkManga ->
|
.map { networkManga ->
|
||||||
manga.copyFrom(networkManga)
|
manga.prepUpdateCover(coverCache, networkManga, true)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
networkManga.thumbnail_url?.let {
|
||||||
|
manga.thumbnail_url = it
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
manga
|
manga
|
||||||
}
|
}
|
||||||
.onErrorReturn { manga }
|
.onErrorReturn { manga }
|
||||||
}
|
}
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
cancelProgressNotification()
|
notifier.cancelProgressNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -430,7 +396,7 @@ class LibraryUpdateService(
|
|||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
|
.doOnNext { notifier.showProgressNotification(it, count++, mangaToUpdate.size) }
|
||||||
// Update the tracking details.
|
// Update the tracking details.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||||
@ -449,207 +415,29 @@ class LibraryUpdateService(
|
|||||||
.map { manga }
|
.map { manga }
|
||||||
}
|
}
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
cancelProgressNotification()
|
notifier.cancelProgressNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the notification containing the currently updating manga and the progress.
|
* Writes basic file of update errors to cache dir.
|
||||||
*
|
|
||||||
* @param manga the manga that's being updated.
|
|
||||||
* @param current the current progress.
|
|
||||||
* @param total the total progress.
|
|
||||||
*/
|
*/
|
||||||
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
private fun writeErrorFile(errors: List<Pair<Manga, String?>>): File {
|
||||||
val title = if (preferences.hideNotificationContent()) {
|
try {
|
||||||
getString(R.string.notification_check_updates)
|
if (errors.isNotEmpty()) {
|
||||||
} else {
|
val destFile = File(externalCacheDir, "tachiyomi_update_errors.txt")
|
||||||
manga.title
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationManager.notify(
|
destFile.bufferedWriter().use { out ->
|
||||||
Notifications.ID_LIBRARY_PROGRESS,
|
errors.forEach { (manga, error) ->
|
||||||
progressNotificationBuilder
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
.setContentTitle(title)
|
out.write("${manga.title} ($source): $error\n")
|
||||||
.setProgress(total, current, false)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the notification containing the result of the update done by the service.
|
|
||||||
*
|
|
||||||
* @param updates a list of manga with new updates.
|
|
||||||
*/
|
|
||||||
private fun showUpdateNotifications(updates: List<Pair<Manga, Array<Chapter>>>) {
|
|
||||||
if (updates.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationManagerCompat.from(this).apply {
|
|
||||||
// Parent group notification
|
|
||||||
notify(
|
|
||||||
Notifications.ID_NEW_CHAPTERS,
|
|
||||||
notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
|
||||||
setContentTitle(getString(R.string.notification_new_chapters))
|
|
||||||
if (updates.size == 1 && !preferences.hideNotificationContent()) {
|
|
||||||
setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN))
|
|
||||||
} else {
|
|
||||||
setContentText(resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size))
|
|
||||||
|
|
||||||
if (!preferences.hideNotificationContent()) {
|
|
||||||
setStyle(
|
|
||||||
NotificationCompat.BigTextStyle().bigText(
|
|
||||||
updates.joinToString("\n") {
|
|
||||||
it.first.title.chop(NOTIF_TITLE_MAX_LEN)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
|
||||||
setLargeIcon(notificationBitmap)
|
|
||||||
|
|
||||||
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
|
||||||
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
|
||||||
setGroupSummary(true)
|
|
||||||
priority = NotificationCompat.PRIORITY_HIGH
|
|
||||||
|
|
||||||
setContentIntent(getNotificationIntent())
|
|
||||||
setAutoCancel(true)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Per-manga notification
|
|
||||||
if (!preferences.hideNotificationContent()) {
|
|
||||||
updates.forEach {
|
|
||||||
val (manga, chapters) = it
|
|
||||||
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
|
|
||||||
}
|
}
|
||||||
|
return destFile
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
|
|
||||||
return notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
|
||||||
setContentTitle(manga.title)
|
|
||||||
|
|
||||||
val description = getNewChaptersDescription(chapters)
|
|
||||||
setContentText(description)
|
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(description))
|
|
||||||
|
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
|
||||||
|
|
||||||
val icon = getMangaIcon(manga)
|
|
||||||
if (icon != null) {
|
|
||||||
setLargeIcon(icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
|
||||||
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
|
||||||
priority = NotificationCompat.PRIORITY_HIGH
|
|
||||||
|
|
||||||
// Open first chapter on tap
|
|
||||||
setContentIntent(NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService, manga, chapters.first()))
|
|
||||||
setAutoCancel(true)
|
|
||||||
|
|
||||||
// Mark chapters as read action
|
|
||||||
addAction(
|
|
||||||
R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
|
|
||||||
NotificationReceiver.markAsReadPendingBroadcast(
|
|
||||||
this@LibraryUpdateService,
|
|
||||||
manga, chapters, Notifications.ID_NEW_CHAPTERS
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// View chapters action
|
|
||||||
addAction(
|
|
||||||
R.drawable.ic_book_24dp, getString(R.string.action_view_chapters),
|
|
||||||
NotificationReceiver.openChapterPendingActivity(
|
|
||||||
this@LibraryUpdateService,
|
|
||||||
manga, Notifications.ID_NEW_CHAPTERS
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels the progress notification.
|
|
||||||
*/
|
|
||||||
private fun cancelProgressNotification() {
|
|
||||||
notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMangaIcon(manga: Manga): Bitmap? {
|
|
||||||
return try {
|
|
||||||
Glide.with(this)
|
|
||||||
.asBitmap()
|
|
||||||
.load(manga.toMangaThumbnail())
|
|
||||||
.dontTransform()
|
|
||||||
.centerCrop()
|
|
||||||
.circleCrop()
|
|
||||||
.override(NOTIF_ICON_SIZE, NOTIF_ICON_SIZE)
|
|
||||||
.submit()
|
|
||||||
.get()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
// Empty
|
||||||
}
|
}
|
||||||
}
|
return File("")
|
||||||
|
|
||||||
private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
|
|
||||||
val formatter = DecimalFormat(
|
|
||||||
"#.###",
|
|
||||||
DecimalFormatSymbols()
|
|
||||||
.apply { decimalSeparator = '.' }
|
|
||||||
)
|
|
||||||
|
|
||||||
val displayableChapterNumbers = chapters
|
|
||||||
.filter { it.isRecognizedNumber }
|
|
||||||
.sortedBy { it.chapter_number }
|
|
||||||
.map { formatter.format(it.chapter_number) }
|
|
||||||
.toSet()
|
|
||||||
|
|
||||||
return when (displayableChapterNumbers.size) {
|
|
||||||
// No sensible chapter numbers to show (i.e. no chapters have parsed chapter number)
|
|
||||||
0 -> {
|
|
||||||
// "1 new chapter" or "5 new chapters"
|
|
||||||
resources.getQuantityString(R.plurals.notification_chapters_generic, chapters.size, chapters.size)
|
|
||||||
}
|
|
||||||
// Only 1 chapter has a parsed chapter number
|
|
||||||
1 -> {
|
|
||||||
val remaining = chapters.size - displayableChapterNumbers.size
|
|
||||||
if (remaining == 0) {
|
|
||||||
// "Chapter 2.5"
|
|
||||||
resources.getString(R.string.notification_chapters_single, displayableChapterNumbers.first())
|
|
||||||
} else {
|
|
||||||
// "Chapter 2.5 and 10 more"
|
|
||||||
resources.getString(R.string.notification_chapters_single_and_more, displayableChapterNumbers.first(), remaining)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Everything else (i.e. multiple parsed chapter numbers)
|
|
||||||
else -> {
|
|
||||||
val shouldTruncate = displayableChapterNumbers.size > NOTIF_MAX_CHAPTERS
|
|
||||||
if (shouldTruncate) {
|
|
||||||
// "Chapters 1, 2.5, 3, 4, 5 and 10 more"
|
|
||||||
val remaining = displayableChapterNumbers.size - NOTIF_MAX_CHAPTERS
|
|
||||||
val joinedChapterNumbers = displayableChapterNumbers.take(NOTIF_MAX_CHAPTERS).joinToString(", ")
|
|
||||||
resources.getQuantityString(R.plurals.notification_chapters_multiple_and_more, remaining, joinedChapterNumbers, remaining)
|
|
||||||
} else {
|
|
||||||
// "Chapters 1, 2.5, 3"
|
|
||||||
resources.getString(R.string.notification_chapters_multiple, displayableChapterNumbers.joinToString(", "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an intent to open the main activity.
|
|
||||||
*/
|
|
||||||
private fun getNotificationIntent(): PendingIntent {
|
|
||||||
val intent = Intent(this, MainActivity::class.java).apply {
|
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
||||||
action = MainActivity.SHORTCUT_RECENTLY_UPDATED
|
|
||||||
}
|
|
||||||
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,13 +25,17 @@ object Notifications {
|
|||||||
*/
|
*/
|
||||||
const val CHANNEL_LIBRARY = "library_channel"
|
const val CHANNEL_LIBRARY = "library_channel"
|
||||||
const val ID_LIBRARY_PROGRESS = -101
|
const val ID_LIBRARY_PROGRESS = -101
|
||||||
|
const val ID_LIBRARY_ERROR = -102
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the downloader.
|
* Notification channel and ids used by the downloader.
|
||||||
*/
|
*/
|
||||||
const val CHANNEL_DOWNLOADER = "downloader_channel"
|
private const val GROUP_DOWNLOADER = "group_downloader"
|
||||||
|
const val CHANNEL_DOWNLOADER_PROGRESS = "downloader_progress_channel"
|
||||||
const val ID_DOWNLOAD_CHAPTER = -201
|
const val ID_DOWNLOAD_CHAPTER = -201
|
||||||
|
const val CHANNEL_DOWNLOADER_COMPLETE = "downloader_complete_channel"
|
||||||
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
|
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
|
||||||
|
const val ID_DOWNLOAD_CHAPTER_COMPLETE = -203
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the library updater.
|
* Notification channel and ids used by the library updater.
|
||||||
@ -49,7 +53,7 @@ object Notifications {
|
|||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the backup/restore system.
|
* Notification channel and ids used by the backup/restore system.
|
||||||
*/
|
*/
|
||||||
private const val GROUP_BACK_RESTORE = "group_backup_restore"
|
private const val GROUP_BACKUP_RESTORE = "group_backup_restore"
|
||||||
const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
|
const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
|
||||||
const val ID_BACKUP_PROGRESS = -501
|
const val ID_BACKUP_PROGRESS = -501
|
||||||
const val ID_RESTORE_PROGRESS = -503
|
const val ID_RESTORE_PROGRESS = -503
|
||||||
@ -58,6 +62,7 @@ object Notifications {
|
|||||||
const val ID_RESTORE_COMPLETE = -504
|
const val ID_RESTORE_COMPLETE = -504
|
||||||
|
|
||||||
private val deprecatedChannels = listOf(
|
private val deprecatedChannels = listOf(
|
||||||
|
"downloader_channel",
|
||||||
"backup_restore_complete_channel"
|
"backup_restore_complete_channel"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -69,10 +74,12 @@ object Notifications {
|
|||||||
fun createChannels(context: Context) {
|
fun createChannels(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
|
||||||
val backupRestoreGroup = NotificationChannelGroup(GROUP_BACK_RESTORE, context.getString(R.string.channel_backup_restore))
|
listOf(
|
||||||
context.notificationManager.createNotificationChannelGroup(backupRestoreGroup)
|
NotificationChannelGroup(GROUP_BACKUP_RESTORE, context.getString(R.string.group_backup_restore)),
|
||||||
|
NotificationChannelGroup(GROUP_DOWNLOADER, context.getString(R.string.group_downloader))
|
||||||
|
).forEach(context.notificationManager::createNotificationChannelGroup)
|
||||||
|
|
||||||
val channels = listOf(
|
listOf(
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_COMMON, context.getString(R.string.channel_common),
|
CHANNEL_COMMON, context.getString(R.string.channel_common),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
@ -84,9 +91,17 @@ object Notifications {
|
|||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
|
CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
|
group = GROUP_DOWNLOADER
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_DOWNLOADER_COMPLETE, context.getString(R.string.channel_complete),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
).apply {
|
||||||
|
group = GROUP_DOWNLOADER
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
@ -98,26 +113,23 @@ object Notifications {
|
|||||||
NotificationManager.IMPORTANCE_DEFAULT
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
),
|
),
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_backup_restore_progress),
|
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_progress),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_BACK_RESTORE
|
group = GROUP_BACKUP_RESTORE
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_backup_restore_complete),
|
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_complete),
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_BACK_RESTORE
|
group = GROUP_BACKUP_RESTORE
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
setSound(null, null)
|
setSound(null, null)
|
||||||
}
|
}
|
||||||
)
|
).forEach(context.notificationManager::createNotificationChannel)
|
||||||
context.notificationManager.createNotificationChannels(channels)
|
|
||||||
|
|
||||||
// Delete old notification channels
|
// Delete old notification channels
|
||||||
deprecatedChannels.forEach {
|
deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel)
|
||||||
context.notificationManager.deleteNotificationChannel(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val showPageNumber = "pref_show_page_number_key"
|
const val showPageNumber = "pref_show_page_number_key"
|
||||||
|
|
||||||
|
const val showReadingMode = "pref_show_reading_mode"
|
||||||
|
|
||||||
const val trueColor = "pref_true_color_key"
|
const val trueColor = "pref_true_color_key"
|
||||||
|
|
||||||
const val fullscreen = "fullscreen"
|
const val fullscreen = "fullscreen"
|
||||||
@ -53,6 +55,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val readWithTapping = "reader_tap"
|
const val readWithTapping = "reader_tap"
|
||||||
|
|
||||||
|
const val readWithTappingInverted = "reader_tapping_inverted"
|
||||||
|
|
||||||
const val readWithLongTap = "reader_long_tap"
|
const val readWithLongTap = "reader_long_tap"
|
||||||
|
|
||||||
const val readWithVolumeKeys = "reader_volume_keys"
|
const val readWithVolumeKeys = "reader_volume_keys"
|
||||||
@ -65,15 +69,17 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val landscapeColumns = "pref_library_columns_landscape_key"
|
const val landscapeColumns = "pref_library_columns_landscape_key"
|
||||||
|
|
||||||
|
const val jumpToChapters = "jump_to_chapters"
|
||||||
|
|
||||||
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
|
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
|
||||||
|
|
||||||
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||||
|
|
||||||
const val lastUsedCatalogueSource = "last_catalogue_source"
|
const val lastUsedSource = "last_catalogue_source"
|
||||||
|
|
||||||
const val lastUsedCategory = "last_used_category"
|
const val lastUsedCategory = "last_used_category"
|
||||||
|
|
||||||
const val catalogueAsList = "pref_display_catalogue_as_list"
|
const val sourceDisplayMode = "pref_display_mode_catalogue"
|
||||||
|
|
||||||
const val enabledLanguages = "source_languages"
|
const val enabledLanguages = "source_languages"
|
||||||
|
|
||||||
@ -123,11 +129,15 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val hideNotificationContent = "hide_notification_content"
|
const val hideNotificationContent = "hide_notification_content"
|
||||||
|
|
||||||
|
const val autoUpdateMetadata = "auto_update_metadata"
|
||||||
|
|
||||||
|
const val showLibraryUpdateErrors = "show_library_update_errors"
|
||||||
|
|
||||||
const val downloadNew = "download_new"
|
const val downloadNew = "download_new"
|
||||||
|
|
||||||
const val downloadNewCategories = "download_new_categories"
|
const val downloadNewCategories = "download_new_categories"
|
||||||
|
|
||||||
const val libraryAsList = "pref_display_library_as_list"
|
const val libraryDisplayMode = "pref_display_mode_library"
|
||||||
|
|
||||||
const val lang = "app_language"
|
const val lang = "app_language"
|
||||||
|
|
||||||
@ -141,10 +151,16 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val downloadBadge = "display_download_badge"
|
const val downloadBadge = "display_download_badge"
|
||||||
|
|
||||||
|
const val unreadBadge = "display_unread_badge"
|
||||||
|
|
||||||
|
const val categoryTabs = "display_category_tabs"
|
||||||
|
|
||||||
const val alwaysShowChapterTransition = "always_show_chapter_transition"
|
const val alwaysShowChapterTransition = "always_show_chapter_transition"
|
||||||
|
|
||||||
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
||||||
|
|
||||||
|
const val enableDoh = "enable_doh"
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
|
@ -5,14 +5,36 @@ package eu.kanade.tachiyomi.data.preference
|
|||||||
*/
|
*/
|
||||||
object PreferenceValues {
|
object PreferenceValues {
|
||||||
|
|
||||||
const val THEME_MODE_LIGHT = "light"
|
// Keys are lowercase to match legacy string values
|
||||||
const val THEME_MODE_DARK = "dark"
|
enum class ThemeMode {
|
||||||
const val THEME_MODE_SYSTEM = "system"
|
light,
|
||||||
|
dark,
|
||||||
|
system,
|
||||||
|
}
|
||||||
|
|
||||||
const val THEME_LIGHT_DEFAULT = "default"
|
// Keys are lowercase to match legacy string values
|
||||||
const val THEME_LIGHT_BLUE = "blue"
|
enum class LightThemeVariant {
|
||||||
|
default,
|
||||||
|
blue,
|
||||||
|
}
|
||||||
|
|
||||||
const val THEME_DARK_DEFAULT = "default"
|
// Keys are lowercase to match legacy string values
|
||||||
const val THEME_DARK_BLUE = "blue"
|
enum class DarkThemeVariant {
|
||||||
const val THEME_DARK_AMOLED = "amoled"
|
default,
|
||||||
|
blue,
|
||||||
|
amoled,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DisplayMode {
|
||||||
|
COMPACT_GRID,
|
||||||
|
COMFORTABLE_GRID,
|
||||||
|
LIST,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TappingInvertMode {
|
||||||
|
NONE,
|
||||||
|
HORIZONTAL,
|
||||||
|
VERTICAL,
|
||||||
|
BOTH
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.f2prateek.rx.preferences.Preference as RxPreference
|
|
||||||
import com.f2prateek.rx.preferences.RxSharedPreferences
|
|
||||||
import com.tfcporciuncula.flow.FlowSharedPreferences
|
import com.tfcporciuncula.flow.FlowSharedPreferences
|
||||||
import com.tfcporciuncula.flow.Preference
|
import com.tfcporciuncula.flow.Preference
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -22,8 +20,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
fun <T> RxPreference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
||||||
block(get())
|
block(get())
|
||||||
@ -31,44 +27,31 @@ fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
|||||||
.onEach { block(it) }
|
.onEach { block(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DateFormatConverter : RxPreference.Adapter<DateFormat> {
|
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
|
||||||
override fun get(key: String, preferences: SharedPreferences): DateFormat {
|
set(get() + item)
|
||||||
val dateFormat = preferences.getString(Keys.dateFormat, "")!!
|
}
|
||||||
|
|
||||||
if (dateFormat != "") {
|
operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
|
||||||
return SimpleDateFormat(dateFormat, Locale.getDefault())
|
set(get() - item)
|
||||||
}
|
|
||||||
|
|
||||||
return DateFormat.getDateInstance(DateFormat.SHORT)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun set(key: String, value: DateFormat, editor: SharedPreferences.Editor) {
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class PreferencesHelper(val context: Context) {
|
class PreferencesHelper(val context: Context) {
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
private val rxPrefs = RxSharedPreferences.create(prefs)
|
|
||||||
private val flowPrefs = FlowSharedPreferences(prefs)
|
private val flowPrefs = FlowSharedPreferences(prefs)
|
||||||
|
|
||||||
private val defaultDownloadsDir = Uri.fromFile(
|
private val defaultDownloadsDir = File(
|
||||||
File(
|
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
context.getString(R.string.app_name),
|
||||||
context.getString(R.string.app_name),
|
"downloads"
|
||||||
"downloads"
|
).toUri()
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private val defaultBackupDir = Uri.fromFile(
|
private val defaultBackupDir = File(
|
||||||
File(
|
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
context.getString(R.string.app_name),
|
||||||
context.getString(R.string.app_name),
|
"backup"
|
||||||
"backup"
|
).toUri()
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
|
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
|
||||||
|
|
||||||
@ -84,15 +67,19 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
|
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
|
||||||
|
|
||||||
|
fun autoUpdateMetadata() = prefs.getBoolean(Keys.autoUpdateMetadata, false)
|
||||||
|
|
||||||
|
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
|
||||||
|
|
||||||
fun clear() = prefs.edit().clear().apply()
|
fun clear() = prefs.edit().clear().apply()
|
||||||
|
|
||||||
fun themeMode() = flowPrefs.getString(Keys.themeMode, Values.THEME_MODE_SYSTEM)
|
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system)
|
||||||
|
|
||||||
fun themeLight() = flowPrefs.getString(Keys.themeLight, Values.THEME_LIGHT_DEFAULT)
|
fun themeLight() = flowPrefs.getEnum(Keys.themeLight, Values.LightThemeVariant.default)
|
||||||
|
|
||||||
fun themeDark() = flowPrefs.getString(Keys.themeDark, Values.THEME_DARK_DEFAULT)
|
fun themeDark() = flowPrefs.getEnum(Keys.themeDark, Values.DarkThemeVariant.default)
|
||||||
|
|
||||||
fun rotation() = rxPrefs.getInteger(Keys.rotation, 1)
|
fun rotation() = flowPrefs.getInt(Keys.rotation, 1)
|
||||||
|
|
||||||
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
||||||
|
|
||||||
@ -100,6 +87,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
||||||
|
|
||||||
|
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
||||||
|
|
||||||
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
|
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
|
||||||
|
|
||||||
fun fullscreen() = flowPrefs.getBoolean(Keys.fullscreen, true)
|
fun fullscreen() = flowPrefs.getBoolean(Keys.fullscreen, true)
|
||||||
@ -118,7 +107,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
|
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
|
||||||
|
|
||||||
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1)
|
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 2)
|
||||||
|
|
||||||
fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
|
fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
|
||||||
|
|
||||||
@ -136,27 +125,31 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
|
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
|
||||||
|
|
||||||
|
fun readWithTappingInverted() = flowPrefs.getEnum(Keys.readWithTappingInverted, Values.TappingInvertMode.NONE)
|
||||||
|
|
||||||
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
|
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
|
||||||
|
|
||||||
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
||||||
|
|
||||||
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
|
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
|
||||||
|
|
||||||
fun portraitColumns() = rxPrefs.getInteger(Keys.portraitColumns, 0)
|
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
|
||||||
|
|
||||||
fun landscapeColumns() = rxPrefs.getInteger(Keys.landscapeColumns, 0)
|
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
||||||
|
|
||||||
|
fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false)
|
||||||
|
|
||||||
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
|
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
|
||||||
|
|
||||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||||
|
|
||||||
fun lastUsedCatalogueSource() = rxPrefs.getLong(Keys.lastUsedCatalogueSource, -1)
|
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
|
||||||
|
|
||||||
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
|
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
|
||||||
|
|
||||||
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
|
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
|
||||||
|
|
||||||
fun catalogueAsList() = rxPrefs.getBoolean(Keys.catalogueAsList, false)
|
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayMode.COMPACT_GRID)
|
||||||
|
|
||||||
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("en", Locale.getDefault().language))
|
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("en", Locale.getDefault().language))
|
||||||
|
|
||||||
@ -177,7 +170,10 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun backupsDirectory() = flowPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
fun backupsDirectory() = flowPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
||||||
|
|
||||||
fun dateFormat() = rxPrefs.getObject(Keys.dateFormat, DateFormat.getDateInstance(DateFormat.SHORT), DateFormatConverter())
|
fun dateFormat(format: String = flowPrefs.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
|
||||||
|
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||||
|
else -> SimpleDateFormat(format, Locale.getDefault())
|
||||||
|
}
|
||||||
|
|
||||||
fun downloadsDirectory() = flowPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
|
fun downloadsDirectory() = flowPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
|
||||||
|
|
||||||
@ -199,12 +195,16 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
||||||
|
|
||||||
fun libraryAsList() = flowPrefs.getBoolean(Keys.libraryAsList, false)
|
fun libraryDisplayMode() = flowPrefs.getEnum(Keys.libraryDisplayMode, DisplayMode.COMPACT_GRID)
|
||||||
|
|
||||||
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
||||||
|
|
||||||
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
|
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
|
||||||
|
|
||||||
|
fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true)
|
||||||
|
|
||||||
|
fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true)
|
||||||
|
|
||||||
fun filterDownloaded() = flowPrefs.getBoolean(Keys.filterDownloaded, false)
|
fun filterDownloaded() = flowPrefs.getBoolean(Keys.filterDownloaded, false)
|
||||||
|
|
||||||
fun filterUnread() = flowPrefs.getBoolean(Keys.filterUnread, false)
|
fun filterUnread() = flowPrefs.getBoolean(Keys.filterUnread, false)
|
||||||
@ -223,9 +223,9 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun searchPinnedSourcesOnly() = prefs.getBoolean(Keys.searchPinnedSourcesOnly, false)
|
fun searchPinnedSourcesOnly() = prefs.getBoolean(Keys.searchPinnedSourcesOnly, false)
|
||||||
|
|
||||||
fun hiddenCatalogues() = flowPrefs.getStringSet("hidden_catalogues", emptySet())
|
fun disabledSources() = flowPrefs.getStringSet("hidden_catalogues", emptySet())
|
||||||
|
|
||||||
fun pinnedCatalogues() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
|
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
|
||||||
|
|
||||||
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
|
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
|
||||||
|
|
||||||
@ -242,4 +242,6 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun migrateFlags() = flowPrefs.getInt("migrate_flags", Int.MAX_VALUE)
|
fun migrateFlags() = flowPrefs.getInt("migrate_flags", Int.MAX_VALUE)
|
||||||
|
|
||||||
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
|
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
|
||||||
|
|
||||||
|
fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.github.salomonbrys.kotson.array
|
import com.github.salomonbrys.kotson.array
|
||||||
import com.github.salomonbrys.kotson.get
|
import com.github.salomonbrys.kotson.get
|
||||||
import com.github.salomonbrys.kotson.jsonObject
|
import com.github.salomonbrys.kotson.jsonObject
|
||||||
@ -271,7 +272,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
return ALManga(
|
return ALManga(
|
||||||
struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
|
struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
|
||||||
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString,
|
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].nullString.orEmpty(),
|
||||||
date, struct["chapters"].nullInt ?: 0
|
date, struct["chapters"].nullInt ?: 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -291,7 +292,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
return baseMangaUrl + mediaId
|
return baseMangaUrl + mediaId
|
||||||
}
|
}
|
||||||
|
|
||||||
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
|
fun authUrl(): Uri = "${baseUrl}oauth/authorize".toUri().buildUpon()
|
||||||
.appendQueryParameter("client_id", clientId)
|
.appendQueryParameter("client_id", clientId)
|
||||||
.appendQueryParameter("response_type", "token")
|
.appendQueryParameter("response_type", "token")
|
||||||
.build()
|
.build()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi
|
package eu.kanade.tachiyomi.data.track.bangumi
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.github.salomonbrys.kotson.array
|
import com.github.salomonbrys.kotson.array
|
||||||
import com.github.salomonbrys.kotson.obj
|
import com.github.salomonbrys.kotson.obj
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
@ -72,9 +73,9 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun search(search: String): Observable<List<TrackSearch>> {
|
fun search(search: String): Observable<List<TrackSearch>> {
|
||||||
val url = Uri.parse(
|
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
||||||
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
.toUri()
|
||||||
).buildUpon()
|
.buildUpon()
|
||||||
.appendQueryParameter("max_results", "20")
|
.appendQueryParameter("max_results", "20")
|
||||||
.build()
|
.build()
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
@ -196,8 +197,8 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
return "$baseMangaUrl/$remoteId"
|
return "$baseMangaUrl/$remoteId"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun authUrl() =
|
fun authUrl(): Uri =
|
||||||
Uri.parse(loginUrl).buildUpon()
|
loginUrl.toUri().buildUpon()
|
||||||
.appendQueryParameter("client_id", clientId)
|
.appendQueryParameter("client_id", clientId)
|
||||||
.appendQueryParameter("response_type", "code")
|
.appendQueryParameter("response_type", "code")
|
||||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
@ -260,13 +260,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
|
|
||||||
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
||||||
|
|
||||||
private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
|
private fun loginUrl() = baseUrl.toUri().buildUpon()
|
||||||
.appendPath("login.php")
|
.appendPath("login.php")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun searchUrl(query: String): String {
|
private fun searchUrl(query: String): String {
|
||||||
val col = "c[]"
|
val col = "c[]"
|
||||||
return Uri.parse(baseUrl).buildUpon()
|
return baseUrl.toUri().buildUpon()
|
||||||
.appendPath("manga.php")
|
.appendPath("manga.php")
|
||||||
.appendQueryParameter("q", query)
|
.appendQueryParameter("q", query)
|
||||||
.appendQueryParameter(col, "a")
|
.appendQueryParameter(col, "a")
|
||||||
@ -278,17 +278,17 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
.toString()
|
.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
|
private fun exportListUrl() = baseUrl.toUri().buildUpon()
|
||||||
.appendPath("panel.php")
|
.appendPath("panel.php")
|
||||||
.appendQueryParameter("go", "export")
|
.appendQueryParameter("go", "export")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
|
private fun editPageUrl(mediaId: Int) = baseModifyListUrl.toUri().buildUpon()
|
||||||
.appendPath(mediaId.toString())
|
.appendPath(mediaId.toString())
|
||||||
.appendPath("edit")
|
.appendPath("edit")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
private fun addUrl() = baseModifyListUrl.toUri().buildUpon()
|
||||||
.appendPath("add.json")
|
.appendPath("add.json")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.shikimori
|
package eu.kanade.tachiyomi.data.track.shikimori
|
||||||
|
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import com.github.salomonbrys.kotson.array
|
import com.github.salomonbrys.kotson.array
|
||||||
import com.github.salomonbrys.kotson.jsonObject
|
import com.github.salomonbrys.kotson.jsonObject
|
||||||
import com.github.salomonbrys.kotson.nullString
|
import com.github.salomonbrys.kotson.nullString
|
||||||
@ -54,7 +54,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
|
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
|
||||||
|
|
||||||
fun search(search: String): Observable<List<TrackSearch>> {
|
fun search(search: String): Observable<List<TrackSearch>> {
|
||||||
val url = Uri.parse("$apiUrl/mangas").buildUpon()
|
val url = "$apiUrl/mangas".toUri().buildUpon()
|
||||||
.appendQueryParameter("order", "popularity")
|
.appendQueryParameter("order", "popularity")
|
||||||
.appendQueryParameter("search", search)
|
.appendQueryParameter("search", search)
|
||||||
.appendQueryParameter("limit", "20")
|
.appendQueryParameter("limit", "20")
|
||||||
@ -102,7 +102,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
|
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
|
||||||
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
|
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
|
||||||
.appendQueryParameter("user_id", user_id)
|
.appendQueryParameter("user_id", user_id)
|
||||||
.appendQueryParameter("target_id", track.media_id.toString())
|
.appendQueryParameter("target_id", track.media_id.toString())
|
||||||
.appendQueryParameter("target_type", "Manga")
|
.appendQueryParameter("target_type", "Manga")
|
||||||
@ -112,7 +112,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
|
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
|
||||||
.appendPath(track.media_id.toString())
|
.appendPath(track.media_id.toString())
|
||||||
.build()
|
.build()
|
||||||
val requestMangas = Request.Builder()
|
val requestMangas = Request.Builder()
|
||||||
@ -187,7 +187,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun authUrl() =
|
fun authUrl() =
|
||||||
Uri.parse(loginUrl).buildUpon()
|
loginUrl.toUri().buildUpon()
|
||||||
.appendQueryParameter("client_id", clientId)
|
.appendQueryParameter("client_id", clientId)
|
||||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||||
.appendQueryParameter("response_type", "code")
|
.appendQueryParameter("response_type", "code")
|
||||||
|
@ -33,14 +33,15 @@ internal class UpdaterNotifier(private val context: Context) {
|
|||||||
*
|
*
|
||||||
* @param title tile of notification.
|
* @param title tile of notification.
|
||||||
*/
|
*/
|
||||||
fun onDownloadStarted(title: String) {
|
fun onDownloadStarted(title: String? = null): NotificationCompat.Builder {
|
||||||
with(notificationBuilder) {
|
with(notificationBuilder) {
|
||||||
setContentTitle(title)
|
title?.let { setContentTitle(title) }
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
|
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
}
|
}
|
||||||
notificationBuilder.show()
|
notificationBuilder.show()
|
||||||
|
return notificationBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,36 +1,82 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
import android.app.IntentService
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.PowerManager
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.ProgressListener
|
import eu.kanade.tachiyomi.network.ProgressListener
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.network.newCallWithProgress
|
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class UpdaterService : IntentService(UpdaterService::class.java.name) {
|
class UpdaterService : Service() {
|
||||||
|
|
||||||
private val network: NetworkHelper by injectLazy()
|
private val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifier for the updater state and progress.
|
* Wake lock that will be held until the service is destroyed.
|
||||||
*/
|
*/
|
||||||
private val notifier by lazy { UpdaterNotifier(this) }
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
override fun onHandleIntent(intent: Intent?) {
|
private lateinit var notifier: UpdaterNotifier
|
||||||
if (intent == null) return
|
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
notifier = UpdaterNotifier(this)
|
||||||
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
|
|
||||||
|
startForeground(Notifications.ID_UPDATER, notifier.onDownloadStarted().build())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method needs to be implemented, but it's not used/needed.
|
||||||
|
*/
|
||||||
|
override fun onBind(intent: Intent): IBinder? = null
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent == null) return START_NOT_STICKY
|
||||||
|
|
||||||
|
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY
|
||||||
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
|
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
|
||||||
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return
|
|
||||||
downloadApk(title, url)
|
launchIO {
|
||||||
|
downloadApk(title, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopSelf(startId)
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopService(name: Intent?): Boolean {
|
||||||
|
destroyJob()
|
||||||
|
return super.stopService(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
destroyJob()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun destroyJob() {
|
||||||
|
if (wakeLock.isHeld) {
|
||||||
|
wakeLock.release()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,12 +84,11 @@ class UpdaterService : IntentService(UpdaterService::class.java.name) {
|
|||||||
*
|
*
|
||||||
* @param url url location of file
|
* @param url url location of file
|
||||||
*/
|
*/
|
||||||
private fun downloadApk(title: String, url: String) {
|
private suspend fun downloadApk(title: String, url: String) {
|
||||||
// Show notification download starting.
|
// Show notification download starting.
|
||||||
notifier.onDownloadStarted(title)
|
notifier.onDownloadStarted(title)
|
||||||
|
|
||||||
val progressListener = object : ProgressListener {
|
val progressListener = object : ProgressListener {
|
||||||
|
|
||||||
// Progress of the download
|
// Progress of the download
|
||||||
var savedProgress = 0
|
var savedProgress = 0
|
||||||
|
|
||||||
@ -51,7 +96,7 @@ class UpdaterService : IntentService(UpdaterService::class.java.name) {
|
|||||||
var lastTick = 0L
|
var lastTick = 0L
|
||||||
|
|
||||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||||
val progress = (100 * bytesRead / contentLength).toInt()
|
val progress = (100 * (bytesRead.toFloat() / contentLength)).toInt()
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
if (progress > savedProgress && currentTime - 200 > lastTick) {
|
if (progress > savedProgress && currentTime - 200 > lastTick) {
|
||||||
savedProgress = progress
|
savedProgress = progress
|
||||||
@ -63,7 +108,7 @@ class UpdaterService : IntentService(UpdaterService::class.java.name) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Download the new update.
|
// Download the new update.
|
||||||
val response = network.client.newCallWithProgress(GET(url), progressListener).execute()
|
val response = network.client.newCallWithProgress(GET(url), progressListener).await()
|
||||||
|
|
||||||
// File where the apk will be saved.
|
// File where the apk will be saved.
|
||||||
val apkFile = File(externalCacheDir, "update.apk")
|
val apkFile = File(externalCacheDir, "update.apk")
|
||||||
@ -82,27 +127,37 @@ class UpdaterService : IntentService(UpdaterService::class.java.name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
|
||||||
* Download url.
|
|
||||||
*/
|
|
||||||
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL"
|
|
||||||
|
|
||||||
/**
|
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL"
|
||||||
* Download title
|
|
||||||
*/
|
|
||||||
internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE"
|
internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status of the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @return true if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun isRunning(context: Context): Boolean =
|
||||||
|
context.isServiceRunning(UpdaterService::class.java)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a new update and let the user install the new version from a notification.
|
* Downloads a new update and let the user install the new version from a notification.
|
||||||
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @param url the url to the new update.
|
* @param url the url to the new update.
|
||||||
*/
|
*/
|
||||||
fun downloadUpdate(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
|
fun start(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
|
||||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
if (!isRunning(context)) {
|
||||||
putExtra(EXTRA_DOWNLOAD_TITLE, title)
|
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||||
putExtra(EXTRA_DOWNLOAD_URL, url)
|
putExtra(EXTRA_DOWNLOAD_TITLE, title)
|
||||||
|
putExtra(EXTRA_DOWNLOAD_URL, url)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
@ -257,8 +258,7 @@ class ExtensionManager(
|
|||||||
if (signature !in untrustedSignatures) return
|
if (signature !in untrustedSignatures) return
|
||||||
|
|
||||||
ExtensionLoader.trustedSignatures += signature
|
ExtensionLoader.trustedSignatures += signature
|
||||||
val preference = preferences.trustedSignatures()
|
preferences.trustedSignatures() += signature
|
||||||
preference.set(preference.get() + signature)
|
|
||||||
|
|
||||||
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
|
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
|
||||||
untrustedExtensions -= nowTrustedExtensions
|
untrustedExtensions -= nowTrustedExtensions
|
||||||
|
@ -18,7 +18,8 @@ sealed class Extension {
|
|||||||
val sources: List<Source>,
|
val sources: List<Source>,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
val hasUpdate: Boolean = false,
|
val hasUpdate: Boolean = false,
|
||||||
val isObsolete: Boolean = false
|
val isObsolete: Boolean = false,
|
||||||
|
val isUnofficial: Boolean = false
|
||||||
) : Extension()
|
) : Extension()
|
||||||
|
|
||||||
data class Available(
|
data class Available(
|
||||||
|
@ -7,6 +7,8 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
@ -63,7 +65,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
// Register the receiver after removing (and unregistering) the previous download
|
// Register the receiver after removing (and unregistering) the previous download
|
||||||
downloadReceiver.register()
|
downloadReceiver.register()
|
||||||
|
|
||||||
val downloadUri = Uri.parse(url)
|
val downloadUri = url.toUri()
|
||||||
val request = DownloadManager.Request(downloadUri)
|
val request = DownloadManager.Request(downloadUri)
|
||||||
.setTitle(extension.name)
|
.setTitle(extension.name)
|
||||||
.setMimeType(APK_MIME)
|
.setMimeType(APK_MIME)
|
||||||
@ -138,8 +140,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
* @param pkgName The package name of the extension to uninstall
|
* @param pkgName The package name of the extension to uninstall
|
||||||
*/
|
*/
|
||||||
fun uninstallApk(pkgName: String) {
|
fun uninstallApk(pkgName: String) {
|
||||||
val packageUri = Uri.parse("package:$pkgName")
|
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
||||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri)
|
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
|
@ -31,13 +31,13 @@ internal object ExtensionLoader {
|
|||||||
|
|
||||||
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
||||||
|
|
||||||
|
// inorichi's key
|
||||||
|
val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||||
/**
|
/**
|
||||||
* List of the trusted signatures.
|
* List of the trusted signatures.
|
||||||
*/
|
*/
|
||||||
var trustedSignatures = mutableSetOf<String>() +
|
var trustedSignatures = mutableSetOf<String>() +
|
||||||
Injekt.get<PreferencesHelper>().trustedSignatures().get() +
|
Injekt.get<PreferencesHelper>().trustedSignatures().get() + officialSignature
|
||||||
// inorichi's key
|
|
||||||
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a list of all the installed extensions initialized concurrently.
|
* Return a list of all the installed extensions initialized concurrently.
|
||||||
@ -159,7 +159,10 @@ internal object ExtensionLoader {
|
|||||||
else -> "all"
|
else -> "all"
|
||||||
}
|
}
|
||||||
|
|
||||||
val extension = Extension.Installed(extName, pkgName, versionName, versionCode, sources, lang)
|
val extension = Extension.Installed(
|
||||||
|
extName, pkgName, versionName, versionCode, sources, lang,
|
||||||
|
isUnofficial = signatureHash != officialSignature
|
||||||
|
)
|
||||||
return LoadResult.Success(extension)
|
return LoadResult.Success(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,9 +12,7 @@ class AndroidCookieJar : CookieJar {
|
|||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
val urlString = url.toString()
|
val urlString = url.toString()
|
||||||
|
|
||||||
for (cookie in cookies) {
|
cookies.forEach { manager.setCookie(urlString, it.toString()) }
|
||||||
manager.setCookie(urlString, cookie.toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
|
@ -2,16 +2,21 @@ package eu.kanade.tachiyomi.network
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebResourceResponse
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.webkit.WebViewClientCompat
|
||||||
|
import androidx.webkit.WebViewFeature
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isOutdated
|
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||||
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
@ -40,9 +45,17 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
|
if (!WebViewUtil.supportsWebView(context)) {
|
||||||
|
launchUI {
|
||||||
|
context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
||||||
|
}
|
||||||
|
return chain.proceed(originalRequest)
|
||||||
|
}
|
||||||
|
|
||||||
initWebView
|
initWebView
|
||||||
|
|
||||||
val originalRequest = chain.request()
|
|
||||||
val response = chain.proceed(originalRequest)
|
val response = chain.proceed(originalRequest)
|
||||||
|
|
||||||
// Check if Cloudflare anti-bot is on
|
// Check if Cloudflare anti-bot is on
|
||||||
@ -83,9 +96,9 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
handler.post {
|
handler.post {
|
||||||
val webview = WebView(context)
|
val webview = WebView(context)
|
||||||
webView = webview
|
webView = webview
|
||||||
webview.settings.javaScriptEnabled = true
|
webview.setDefaultSettings()
|
||||||
|
|
||||||
// Avoid set empty User-Agent, Chromium WebView will reset to default if empty
|
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
||||||
webview.settings.userAgentString = request.header("User-Agent")
|
webview.settings.userAgentString = request.header("User-Agent")
|
||||||
?: HttpSource.DEFAULT_USERAGENT
|
?: HttpSource.DEFAULT_USERAGENT
|
||||||
|
|
||||||
@ -103,7 +116,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HTTP error codes are only received since M
|
// HTTP error codes are only received since M
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
if (WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR) &&
|
||||||
url == origRequestUrl && !challengeFound
|
url == origRequestUrl && !challengeFound
|
||||||
) {
|
) {
|
||||||
// The first request didn't return the challenge, abort.
|
// The first request didn't return the challenge, abort.
|
||||||
@ -111,16 +124,14 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceivedErrorCompat(
|
override fun onReceivedHttpError(
|
||||||
view: WebView,
|
view: WebView,
|
||||||
errorCode: Int,
|
request: WebResourceRequest,
|
||||||
description: String?,
|
errorResponse: WebResourceResponse
|
||||||
failingUrl: String,
|
|
||||||
isMainFrame: Boolean
|
|
||||||
) {
|
) {
|
||||||
if (isMainFrame) {
|
if (request.isForMainFrame) {
|
||||||
if (errorCode == 503) {
|
if (errorResponse.statusCode == 503) {
|
||||||
// Found the cloudflare challenge page.
|
// Found the Cloudflare challenge page.
|
||||||
challengeFound = true
|
challengeFound = true
|
||||||
} else {
|
} else {
|
||||||
// Unlock thread, the challenge wasn't found.
|
// Unlock thread, the challenge wasn't found.
|
||||||
|
@ -1,28 +1,70 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.net.InetAddress
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class NetworkHelper(context: Context) {
|
class NetworkHelper(context: Context) {
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val cacheDir = File(context.cacheDir, "network_cache")
|
private val cacheDir = File(context.cacheDir, "network_cache")
|
||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
|
|
||||||
val cookieManager = AndroidCookieJar()
|
val cookieManager = AndroidCookieJar()
|
||||||
|
|
||||||
val client = OkHttpClient.Builder()
|
val client by lazy {
|
||||||
.cookieJar(cookieManager)
|
val builder = OkHttpClient.Builder()
|
||||||
.cache(Cache(cacheDir, cacheSize))
|
.cookieJar(cookieManager)
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.cache(Cache(cacheDir, cacheSize))
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.build()
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.addInterceptor(UserAgentInterceptor())
|
||||||
|
|
||||||
val cloudflareClient = client.newBuilder()
|
if (BuildConfig.DEBUG) {
|
||||||
.addInterceptor(UserAgentInterceptor())
|
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||||
.addInterceptor(CloudflareInterceptor(context))
|
level = HttpLoggingInterceptor.Level.HEADERS
|
||||||
.build()
|
}
|
||||||
|
builder.addInterceptor(httpLoggingInterceptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferences.enableDoh()) {
|
||||||
|
builder.dns(
|
||||||
|
DnsOverHttps.Builder().client(builder.build())
|
||||||
|
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
|
||||||
|
.bootstrapDnsHosts(
|
||||||
|
listOf(
|
||||||
|
InetAddress.getByName("162.159.36.1"),
|
||||||
|
InetAddress.getByName("162.159.46.1"),
|
||||||
|
InetAddress.getByName("1.1.1.1"),
|
||||||
|
InetAddress.getByName("1.0.0.1"),
|
||||||
|
InetAddress.getByName("162.159.132.53"),
|
||||||
|
InetAddress.getByName("2606:4700:4700::1111"),
|
||||||
|
InetAddress.getByName("2606:4700:4700::1001"),
|
||||||
|
InetAddress.getByName("2606:4700:4700::0064"),
|
||||||
|
InetAddress.getByName("2606:4700:4700::6400")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val cloudflareClient by lazy {
|
||||||
|
client.newBuilder()
|
||||||
|
.addInterceptor(CloudflareInterceptor(context))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.google.gson.Gson
|
import com.google.gson.JsonParser
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
@ -19,7 +18,6 @@ import java.io.File
|
|||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.Scanner
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
@ -30,13 +28,15 @@ import timber.log.Timber
|
|||||||
|
|
||||||
class LocalSource(private val context: Context) : CatalogueSource {
|
class LocalSource(private val context: Context) : CatalogueSource {
|
||||||
companion object {
|
companion object {
|
||||||
|
const val ID = 0L
|
||||||
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
|
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
|
||||||
|
|
||||||
private const val COVER_NAME = "cover.jpg"
|
private const val COVER_NAME = "cover.jpg"
|
||||||
|
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
||||||
|
|
||||||
private val POPULAR_FILTERS = FilterList(OrderBy())
|
private val POPULAR_FILTERS = FilterList(OrderBy())
|
||||||
private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
||||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||||
const val ID = 0L
|
|
||||||
|
|
||||||
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
||||||
val dir = getBaseDirectories(context).firstOrNull()
|
val dir = getBaseDirectories(context).firstOrNull()
|
||||||
@ -47,7 +47,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
||||||
|
|
||||||
// It might not exist if using the external SD card
|
// It might not exist if using the external SD card
|
||||||
cover.parentFile.mkdirs()
|
cover.parentFile?.mkdirs()
|
||||||
input.use {
|
input.use {
|
||||||
cover.outputStream().use {
|
cover.outputStream().use {
|
||||||
input.copyTo(it)
|
input.copyTo(it)
|
||||||
@ -75,9 +75,12 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
val baseDirs = getBaseDirectories(context)
|
val baseDirs = getBaseDirectories(context)
|
||||||
|
|
||||||
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||||
var mangaDirs = baseDirs.mapNotNull { it.listFiles()?.toList() }
|
var mangaDirs = baseDirs
|
||||||
|
.asSequence()
|
||||||
|
.mapNotNull { it.listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter { it.isDirectory && if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
.filter { it.isDirectory }
|
||||||
|
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||||
.distinctBy { it.name }
|
.distinctBy { it.name }
|
||||||
|
|
||||||
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
||||||
@ -134,18 +137,22 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Observable.just(MangasPage(mangas, false))
|
|
||||||
|
return Observable.just(MangasPage(mangas.toList(), false))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
getBaseDirectories(context)
|
getBaseDirectories(context)
|
||||||
|
.asSequence()
|
||||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
.firstOrNull { it.extension == "json" }
|
.firstOrNull { it.extension == "json" }
|
||||||
?.apply {
|
?.apply {
|
||||||
val json = Gson().fromJson(Scanner(this).useDelimiter("\\Z").next(), JsonObject::class.java)
|
val reader = this.inputStream().bufferedReader()
|
||||||
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
manga.title = json["title"]?.asString ?: manga.title
|
manga.title = json["title"]?.asString ?: manga.title
|
||||||
manga.author = json["author"]?.asString ?: manga.author
|
manga.author = json["author"]?.asString ?: manga.author
|
||||||
manga.artist = json["artist"]?.asString ?: manga.artist
|
manga.artist = json["artist"]?.asString ?: manga.artist
|
||||||
@ -154,6 +161,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
?: manga.genre
|
?: manga.genre
|
||||||
manga.status = json["status"]?.asInt ?: manga.status
|
manga.status = json["status"]?.asInt ?: manga.status
|
||||||
}
|
}
|
||||||
|
|
||||||
return Observable.just(manga)
|
return Observable.just(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,8 +212,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
var chapterNameIndex = 0
|
var chapterNameIndex = 0
|
||||||
var mangaTitleIndex = 0
|
var mangaTitleIndex = 0
|
||||||
while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
|
while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
|
||||||
val chapterChar = chapterName.get(chapterNameIndex)
|
val chapterChar = chapterName[chapterNameIndex]
|
||||||
val mangaChar = mangaTitle.get(mangaTitleIndex)
|
val mangaChar = mangaTitle[mangaTitleIndex]
|
||||||
if (!chapterChar.equals(mangaChar, true)) {
|
if (!chapterChar.equals(mangaChar, true)) {
|
||||||
val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
|
val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
|
||||||
val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
|
val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
|
||||||
@ -235,7 +243,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isSupportedFile(extension: String): Boolean {
|
private fun isSupportedFile(extension: String): Boolean {
|
||||||
return extension.toLowerCase() in setOf("zip", "rar", "cbr", "cbz", "epub")
|
return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFormat(chapter: SChapter): Format {
|
fun getFormat(chapter: SChapter): Format {
|
||||||
@ -269,8 +277,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
return when (val format = getFormat(chapter)) {
|
return when (val format = getFormat(chapter)) {
|
||||||
is Format.Directory -> {
|
is Format.Directory -> {
|
||||||
val entry = format.file.listFiles()
|
val entry = format.file.listFiles()
|
||||||
.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
|
?.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, it.inputStream()) }
|
entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||||
}
|
}
|
||||||
|
@ -47,3 +47,5 @@ interface Source {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
||||||
|
|
||||||
|
fun Source.getPreferenceKey(): String = "source_$id"
|
||||||
|
@ -34,7 +34,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
// * Preferences that a source may need.
|
// * Preferences that a source may need.
|
||||||
// */
|
// */
|
||||||
// val preferences: SharedPreferences by lazy {
|
// val preferences: SharedPreferences by lazy {
|
||||||
// Injekt.get<Application>().getSharedPreferences("source_$id", Context.MODE_PRIVATE)
|
// Injekt.get<Application>().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,7 +25,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
|||||||
|
|
||||||
private val lightTheme: Int by lazy {
|
private val lightTheme: Int by lazy {
|
||||||
when (preferences.themeLight().get()) {
|
when (preferences.themeLight().get()) {
|
||||||
Values.THEME_LIGHT_BLUE -> R.style.Theme_Tachiyomi_LightBlue
|
Values.LightThemeVariant.blue -> R.style.Theme_Tachiyomi_LightBlue
|
||||||
else -> {
|
else -> {
|
||||||
when {
|
when {
|
||||||
// Light status + navigation bar
|
// Light status + navigation bar
|
||||||
@ -47,8 +47,8 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
|||||||
|
|
||||||
private val darkTheme: Int by lazy {
|
private val darkTheme: Int by lazy {
|
||||||
when (preferences.themeDark().get()) {
|
when (preferences.themeDark().get()) {
|
||||||
Values.THEME_DARK_BLUE -> R.style.Theme_Tachiyomi_DarkBlue
|
Values.DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_DarkBlue
|
||||||
Values.THEME_DARK_AMOLED -> R.style.Theme_Tachiyomi_Amoled
|
Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
|
||||||
else -> R.style.Theme_Tachiyomi_Dark
|
else -> R.style.Theme_Tachiyomi_Dark
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,14 +61,14 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
setTheme(
|
setTheme(
|
||||||
when (preferences.themeMode().get()) {
|
when (preferences.themeMode().get()) {
|
||||||
Values.THEME_MODE_SYSTEM -> {
|
Values.ThemeMode.system -> {
|
||||||
if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) {
|
if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) {
|
||||||
darkTheme
|
darkTheme
|
||||||
} else {
|
} else {
|
||||||
lightTheme
|
lightTheme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Values.THEME_MODE_DARK -> darkTheme
|
Values.ThemeMode.dark -> darkTheme
|
||||||
else -> lightTheme
|
else -> lightTheme
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -64,8 +64,9 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
|||||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
if (type.isEnter) {
|
if (type.isEnter) {
|
||||||
setTitle()
|
setTitle()
|
||||||
|
setHasOptionsMenu(true)
|
||||||
}
|
}
|
||||||
setHasOptionsMenu(type.isEnter)
|
|
||||||
super.onChangeStarted(handler, type)
|
super.onChangeStarted(handler, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +74,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTitle() {
|
fun setTitle(title: String? = null) {
|
||||||
var parentController = parentController
|
var parentController = parentController
|
||||||
while (parentController != null) {
|
while (parentController != null) {
|
||||||
if (parentController is BaseController<*> && parentController.getTitle() != null) {
|
if (parentController is BaseController<*> && parentController.getTitle() != null) {
|
||||||
@ -82,7 +83,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
|||||||
parentController = parentController.parentController
|
parentController = parentController.parentController
|
||||||
}
|
}
|
||||||
|
|
||||||
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
|
(activity as? AppCompatActivity)?.supportActionBar?.title = title ?: getTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Controller.instance(): String {
|
private fun Controller.instance(): String {
|
||||||
|
@ -28,8 +28,8 @@ fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Controller.withFadeTransaction(): RouterTransaction {
|
fun Controller.withFadeTransaction(duration: Long = 150L): RouterTransaction {
|
||||||
return RouterTransaction.with(this)
|
return RouterTransaction.with(this)
|
||||||
.pushChangeHandler(FadeChangeHandler())
|
.pushChangeHandler(FadeChangeHandler(duration))
|
||||||
.popChangeHandler(FadeChangeHandler())
|
.popChangeHandler(FadeChangeHandler(duration))
|
||||||
}
|
}
|
||||||
|
@ -98,7 +98,7 @@ abstract class DialogController : RestoreViewOnCreateController {
|
|||||||
/**
|
/**
|
||||||
* Dismiss the dialog and pop this controller
|
* Dismiss the dialog and pop this controller
|
||||||
*/
|
*/
|
||||||
fun dismissDialog() {
|
private fun dismissDialog() {
|
||||||
if (dismissed) {
|
if (dismissed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
|
||||||
|
interface FabController {
|
||||||
|
|
||||||
|
fun configureFab(fab: ExtendedFloatingActionButton) {}
|
||||||
|
|
||||||
|
fun cleanupFab(fab: ExtendedFloatingActionButton) {}
|
||||||
|
}
|
@ -10,25 +10,7 @@ import rx.subscriptions.CompositeSubscription
|
|||||||
|
|
||||||
abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseController<VB>(bundle) {
|
abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseController<VB>(bundle) {
|
||||||
|
|
||||||
var untilDetachSubscriptions = CompositeSubscription()
|
private var untilDestroySubscriptions = 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
|
@CallSuper
|
||||||
override fun onViewCreated(view: View) {
|
override fun onViewCreated(view: View) {
|
||||||
@ -43,49 +25,7 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont
|
|||||||
untilDestroySubscriptions.unsubscribe()
|
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 {
|
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
|
||||||
return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
|
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) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.ui.base.controller.RootController
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionController
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
||||||
import kotlinx.android.synthetic.main.main_activity.tabs
|
import kotlinx.android.synthetic.main.main_activity.tabs
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -95,10 +96,6 @@ class BrowseController :
|
|||||||
tabs.getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge()
|
tabs.getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pushController(transaction: RouterTransaction) {
|
|
||||||
router.pushController(transaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setExtensionUpdateBadge() {
|
fun setExtensionUpdateBadge() {
|
||||||
activity?.tabs?.apply {
|
activity?.tabs?.apply {
|
||||||
val updates = preferences.extensionUpdatesCount().get()
|
val updates = preferences.extensionUpdatesCount().get()
|
||||||
@ -115,7 +112,8 @@ class BrowseController :
|
|||||||
|
|
||||||
private val tabTitles = listOf(
|
private val tabTitles = listOf(
|
||||||
R.string.label_sources,
|
R.string.label_sources,
|
||||||
R.string.label_extensions
|
R.string.label_extensions,
|
||||||
|
R.string.label_migration
|
||||||
)
|
)
|
||||||
.map { resources!!.getString(it) }
|
.map { resources!!.getString(it) }
|
||||||
|
|
||||||
@ -128,6 +126,7 @@ class BrowseController :
|
|||||||
val controller: Controller = when (position) {
|
val controller: Controller = when (position) {
|
||||||
SOURCES_CONTROLLER -> SourceController()
|
SOURCES_CONTROLLER -> SourceController()
|
||||||
EXTENSIONS_CONTROLLER -> ExtensionController()
|
EXTENSIONS_CONTROLLER -> ExtensionController()
|
||||||
|
MIGRATION_CONTROLLER -> MigrationSourcesController()
|
||||||
else -> error("Wrong position $position")
|
else -> error("Wrong position $position")
|
||||||
}
|
}
|
||||||
router.setRoot(RouterTransaction.with(controller))
|
router.setRoot(RouterTransaction.with(controller))
|
||||||
@ -144,5 +143,6 @@ class BrowseController :
|
|||||||
|
|
||||||
const val SOURCES_CONTROLLER = 0
|
const val SOURCES_CONTROLLER = 0
|
||||||
const val EXTENSIONS_CONTROLLER = 1
|
const val EXTENSIONS_CONTROLLER = 1
|
||||||
|
const val MIGRATION_CONTROLLER = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@ -64,17 +65,17 @@ open class ExtensionController :
|
|||||||
override fun onViewCreated(view: View) {
|
override fun onViewCreated(view: View) {
|
||||||
super.onViewCreated(view)
|
super.onViewCreated(view)
|
||||||
|
|
||||||
binding.extSwipeRefresh.isRefreshing = true
|
binding.swipeRefresh.isRefreshing = true
|
||||||
binding.extSwipeRefresh.refreshes()
|
binding.swipeRefresh.refreshes()
|
||||||
.onEach { presenter.findAvailableExtensions() }
|
.onEach { presenter.findAvailableExtensions() }
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
// Initialize adapter, scroll listener and recycler views
|
// Initialize adapter, scroll listener and recycler views
|
||||||
adapter = ExtensionAdapter(this)
|
adapter = ExtensionAdapter(this)
|
||||||
// Create recycler and set adapter.
|
// Create recycler and set adapter.
|
||||||
binding.extRecycler.layoutManager = LinearLayoutManager(view.context)
|
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
binding.extRecycler.adapter = adapter
|
binding.recycler.adapter = adapter
|
||||||
binding.extRecycler.addItemDecoration(ExtensionDividerItemDecoration(view.context))
|
binding.recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context))
|
||||||
adapter?.fastScroller = binding.fastScroller
|
adapter?.fastScroller = binding.fastScroller
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,11 +88,10 @@ open class ExtensionController :
|
|||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_search -> expandActionViewFromInteraction = true
|
R.id.action_search -> expandActionViewFromInteraction = true
|
||||||
R.id.action_settings -> {
|
R.id.action_settings -> {
|
||||||
(parentController as BrowseController).pushController(
|
parentController!!.router.pushController(
|
||||||
ExtensionFilterController().withFadeTransaction()
|
ExtensionFilterController().withFadeTransaction()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> return super.onOptionsItemSelected(item)
|
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
@ -129,6 +129,9 @@ open class ExtensionController :
|
|||||||
val searchView = searchItem.actionView as SearchView
|
val searchView = searchItem.actionView as SearchView
|
||||||
searchView.maxWidth = Int.MAX_VALUE
|
searchView.maxWidth = Int.MAX_VALUE
|
||||||
|
|
||||||
|
// Fixes problem with the overflow icon showing up in lieu of search
|
||||||
|
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
|
||||||
|
|
||||||
if (query.isNotEmpty()) {
|
if (query.isNotEmpty()) {
|
||||||
searchItem.expandActionView()
|
searchItem.expandActionView()
|
||||||
searchView.setQuery(query, true)
|
searchView.setQuery(query, true)
|
||||||
@ -142,9 +145,6 @@ open class ExtensionController :
|
|||||||
drawExtensions()
|
drawExtensions()
|
||||||
}
|
}
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
// Fixes problem with the overflow icon showing up in lieu of search
|
|
||||||
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(view: View, position: Int): Boolean {
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
@ -167,7 +167,7 @@ open class ExtensionController :
|
|||||||
|
|
||||||
private fun openDetails(extension: Extension.Installed) {
|
private fun openDetails(extension: Extension.Installed) {
|
||||||
val controller = ExtensionDetailsController(extension.pkgName)
|
val controller = ExtensionDetailsController(extension.pkgName)
|
||||||
(parentController as BrowseController).pushController(controller.withFadeTransaction())
|
parentController!!.router.pushController(controller.withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openTrustDialog(extension: Extension.Untrusted) {
|
private fun openTrustDialog(extension: Extension.Untrusted) {
|
||||||
@ -176,7 +176,7 @@ open class ExtensionController :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setExtensions(extensions: List<ExtensionItem>) {
|
fun setExtensions(extensions: List<ExtensionItem>) {
|
||||||
binding.extSwipeRefresh.isRefreshing = false
|
binding.swipeRefresh.isRefreshing = false
|
||||||
this.extensions = extensions
|
this.extensions = extensions
|
||||||
drawExtensions()
|
drawExtensions()
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import android.graphics.Canvas
|
|||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.marginBottom
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
||||||
@ -25,8 +26,7 @@ class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecora
|
|||||||
if (holder is ExtensionHolder &&
|
if (holder is ExtensionHolder &&
|
||||||
parent.getChildViewHolder(parent.getChildAt(i + 1)) is ExtensionHolder
|
parent.getChildViewHolder(parent.getChildAt(i + 1)) is ExtensionHolder
|
||||||
) {
|
) {
|
||||||
val params = child.layoutParams as RecyclerView.LayoutParams
|
val top = child.bottom + child.marginBottom
|
||||||
val top = child.bottom + params.bottomMargin
|
|
||||||
val bottom = top + divider.intrinsicHeight
|
val bottom = top + divider.intrinsicHeight
|
||||||
val left = parent.paddingStart + holder.margin
|
val left = parent.paddingStart + holder.margin
|
||||||
val right = parent.width - parent.paddingEnd - holder.margin
|
val right = parent.width - parent.paddingEnd - holder.margin
|
||||||
|
@ -14,18 +14,16 @@ import uy.kohesive.injekt.api.get
|
|||||||
class ExtensionFilterController : SettingsController() {
|
class ExtensionFilterController : SettingsController() {
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
|
||||||
titleRes = R.string.action_filter
|
titleRes = R.string.label_extensions
|
||||||
|
|
||||||
val activeLangs = preferences.enabledLanguages().get()
|
val activeLangs = preferences.enabledLanguages().get()
|
||||||
|
|
||||||
val availableLangs =
|
val availableLangs =
|
||||||
Injekt.get<ExtensionManager>().availableExtensions.groupBy {
|
Injekt.get<ExtensionManager>().availableExtensions.groupBy {
|
||||||
it.lang
|
it.lang
|
||||||
}.keys.minus("all").partition {
|
}.keys
|
||||||
it in activeLangs
|
.minus("all")
|
||||||
}.let {
|
.sortedWith(compareBy({ it !in activeLangs }, { LocaleHelper.getSourceDisplayName(it, context) }))
|
||||||
it.first + it.second
|
|
||||||
}
|
|
||||||
|
|
||||||
availableLangs.forEach {
|
availableLangs.forEach {
|
||||||
switchPreference {
|
switchPreference {
|
||||||
@ -38,11 +36,13 @@ class ExtensionFilterController : SettingsController() {
|
|||||||
val checked = newValue as Boolean
|
val checked = newValue as Boolean
|
||||||
val currentActiveLangs = preferences.enabledLanguages().get()
|
val currentActiveLangs = preferences.enabledLanguages().get()
|
||||||
|
|
||||||
if (checked) {
|
preferences.enabledLanguages().set(
|
||||||
preferences.enabledLanguages().set(currentActiveLangs + it)
|
if (checked) {
|
||||||
} else {
|
currentActiveLangs + it
|
||||||
preferences.enabledLanguages().set(currentActiveLangs - it)
|
} else {
|
||||||
}
|
currentActiveLangs - it
|
||||||
|
}
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.extension.model.InstallStep
|
|||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
|
||||||
import io.github.mthli.slice.Slice
|
import io.github.mthli.slice.Slice
|
||||||
import kotlinx.android.synthetic.main.extension_card_item.card
|
import kotlinx.android.synthetic.main.extension_card_item.card
|
||||||
import kotlinx.android.synthetic.main.extension_card_item.ext_button
|
import kotlinx.android.synthetic.main.extension_card_item.ext_button
|
||||||
@ -16,6 +15,7 @@ import kotlinx.android.synthetic.main.extension_card_item.ext_title
|
|||||||
import kotlinx.android.synthetic.main.extension_card_item.image
|
import kotlinx.android.synthetic.main.extension_card_item.image
|
||||||
import kotlinx.android.synthetic.main.extension_card_item.lang
|
import kotlinx.android.synthetic.main.extension_card_item.lang
|
||||||
import kotlinx.android.synthetic.main.extension_card_item.version
|
import kotlinx.android.synthetic.main.extension_card_item.version
|
||||||
|
import kotlinx.android.synthetic.main.extension_card_item.warning
|
||||||
|
|
||||||
class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
||||||
BaseFlexibleViewHolder(view, adapter),
|
BaseFlexibleViewHolder(view, adapter),
|
||||||
@ -38,13 +38,14 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
|||||||
val extension = item.extension
|
val extension = item.extension
|
||||||
setCardEdges(item)
|
setCardEdges(item)
|
||||||
|
|
||||||
// Set source name
|
|
||||||
ext_title.text = extension.name
|
ext_title.text = extension.name
|
||||||
version.text = extension.versionName
|
version.text = extension.versionName
|
||||||
lang.text = if (extension !is Extension.Untrusted) {
|
lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
|
||||||
LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
|
warning.text = when {
|
||||||
} else {
|
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted).toUpperCase()
|
||||||
itemView.context.getString(R.string.ext_untrusted).toUpperCase()
|
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete).toUpperCase()
|
||||||
|
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial).toUpperCase()
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
GlideApp.with(itemView.context).clear(image)
|
GlideApp.with(itemView.context).clear(image)
|
||||||
@ -63,8 +64,6 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
|||||||
isEnabled = true
|
isEnabled = true
|
||||||
isClickable = true
|
isClickable = true
|
||||||
|
|
||||||
setTextColor(context.getResourceColor(R.attr.colorAccent))
|
|
||||||
|
|
||||||
val extension = item.extension
|
val extension = item.extension
|
||||||
|
|
||||||
val installStep = item.installStep
|
val installStep = item.installStep
|
||||||
@ -87,12 +86,8 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
|||||||
extension.hasUpdate -> {
|
extension.hasUpdate -> {
|
||||||
setText(R.string.ext_update)
|
setText(R.string.ext_update)
|
||||||
}
|
}
|
||||||
extension.isObsolete -> {
|
|
||||||
setTextColor(context.getResourceColor(R.attr.colorError))
|
|
||||||
setText(R.string.ext_obsolete)
|
|
||||||
}
|
|
||||||
else -> {
|
else -> {
|
||||||
setText(R.string.ext_details)
|
setText(R.string.action_settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (extension is Extension.Untrusted) {
|
} else if (extension is Extension.Untrusted) {
|
||||||
|
@ -0,0 +1,229 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.extension.details
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceGroupAdapter
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.minusAssign
|
||||||
|
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
|
||||||
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.getPreferenceKey
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.util.preference.DSL
|
||||||
|
import eu.kanade.tachiyomi.util.preference.onChange
|
||||||
|
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||||
|
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||||
|
import eu.kanade.tachiyomi.util.preference.switchSettingsPreference
|
||||||
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||||
|
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
|
||||||
|
NoToolbarElevationController {
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private var preferenceScreen: PreferenceScreen? = null
|
||||||
|
|
||||||
|
constructor(pkgName: String) : this(
|
||||||
|
Bundle().apply {
|
||||||
|
putString(PKGNAME_KEY, pkgName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
|
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
|
||||||
|
binding = ExtensionDetailControllerBinding.inflate(themedInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPresenter(): ExtensionDetailsPresenter {
|
||||||
|
return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String? {
|
||||||
|
return resources?.getString(R.string.label_extension_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("PrivateResource")
|
||||||
|
override fun onViewCreated(view: View) {
|
||||||
|
super.onViewCreated(view)
|
||||||
|
|
||||||
|
val extension = presenter.extension ?: return
|
||||||
|
val context = view.context
|
||||||
|
|
||||||
|
binding.extensionPrefsRecycler.layoutManager = LinearLayoutManager(context)
|
||||||
|
binding.extensionPrefsRecycler.adapter = ConcatAdapter(
|
||||||
|
ExtensionDetailsHeaderAdapter(presenter),
|
||||||
|
initPreferencesAdapter(context, extension)
|
||||||
|
)
|
||||||
|
binding.extensionPrefsRecycler.addItemDecoration(DividerItemDecoration(context, VERTICAL))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initPreferencesAdapter(context: Context, extension: Extension.Installed): PreferenceGroupAdapter {
|
||||||
|
val themedContext = getPreferenceThemeContext()
|
||||||
|
val manager = PreferenceManager(themedContext)
|
||||||
|
manager.preferenceDataStore = EmptyPreferenceDataStore()
|
||||||
|
val screen = manager.createPreferenceScreen(themedContext)
|
||||||
|
preferenceScreen = screen
|
||||||
|
|
||||||
|
val isMultiSource = extension.sources.size > 1
|
||||||
|
val isMultiLangSingleSource = isMultiSource && extension.sources.map { it.name }.distinct().size == 1
|
||||||
|
|
||||||
|
with(screen) {
|
||||||
|
extension.sources
|
||||||
|
.groupBy { (it as CatalogueSource).lang }
|
||||||
|
.toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) })
|
||||||
|
.forEach {
|
||||||
|
val preferenceBlock = {
|
||||||
|
it.value
|
||||||
|
.sortedWith(compareBy({ !it.isEnabled() }, { it.name }))
|
||||||
|
.forEach { source ->
|
||||||
|
val sourcePrefs = mutableListOf<Preference>()
|
||||||
|
|
||||||
|
val block: (@DSL SwitchPreferenceCompat).() -> Unit = {
|
||||||
|
key = source.getPreferenceKey()
|
||||||
|
title = when {
|
||||||
|
isMultiSource && !isMultiLangSingleSource -> source.toString()
|
||||||
|
else -> LocaleHelper.getSourceDisplayName(it.key, context)
|
||||||
|
}
|
||||||
|
isPersistent = false
|
||||||
|
isChecked = source.isEnabled()
|
||||||
|
|
||||||
|
onChange { newValue ->
|
||||||
|
val checked = newValue as Boolean
|
||||||
|
toggleSource(source, checked)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// React to enable/disable all changes
|
||||||
|
preferences.disabledSources().asFlow()
|
||||||
|
.onEach {
|
||||||
|
val enabled = source.isEnabled()
|
||||||
|
isChecked = enabled
|
||||||
|
sourcePrefs.forEach { pref -> pref.isVisible = enabled }
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source enable/disable
|
||||||
|
if (source is ConfigurableSource) {
|
||||||
|
switchSettingsPreference {
|
||||||
|
block()
|
||||||
|
onSettingsClick = View.OnClickListener {
|
||||||
|
router.pushController(
|
||||||
|
SourcePreferencesController(source.id).withFadeTransaction()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switchPreference(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMultiSource && !isMultiLangSingleSource) {
|
||||||
|
preferenceCategory {
|
||||||
|
title = LocaleHelper.getSourceDisplayName(it.key, context)
|
||||||
|
|
||||||
|
preferenceBlock()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
preferenceBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreferenceGroupAdapter(screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView(view: View) {
|
||||||
|
preferenceScreen = null
|
||||||
|
super.onDestroyView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.extension_details, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.action_enable_all -> toggleAllSources(true)
|
||||||
|
R.id.action_disable_all -> toggleAllSources(false)
|
||||||
|
R.id.action_open_in_settings -> openInSettings()
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onExtensionUninstalled() {
|
||||||
|
router.popCurrentController()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleAllSources(enable: Boolean) {
|
||||||
|
presenter.extension?.sources?.forEach { toggleSource(it, enable) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleSource(source: Source, enable: Boolean) {
|
||||||
|
if (enable) {
|
||||||
|
preferences.disabledSources() -= source.id.toString()
|
||||||
|
} else {
|
||||||
|
preferences.disabledSources() += source.id.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openInSettings() {
|
||||||
|
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = Uri.fromParts("package", presenter.pkgName, null)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Source.isEnabled(): Boolean {
|
||||||
|
return id.toString() !in preferences.disabledSources().get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPreferenceThemeContext(): Context {
|
||||||
|
val tv = TypedValue()
|
||||||
|
activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
|
||||||
|
return ContextThemeWrapper(activity, tv.resourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val PKGNAME_KEY = "pkg_name"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.extension.details
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.databinding.ExtensionDetailHeaderBinding
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.extension.getApplicationIcon
|
||||||
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
|
|
||||||
|
class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPresenter) :
|
||||||
|
RecyclerView.Adapter<ExtensionDetailsHeaderAdapter.HeaderViewHolder>() {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
private lateinit var binding: ExtensionDetailHeaderBinding
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
|
||||||
|
binding = ExtensionDetailHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return HeaderViewHolder(binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = 1
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
|
||||||
|
holder.bind()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
fun bind() {
|
||||||
|
val extension = presenter.extension ?: return
|
||||||
|
val context = view.context
|
||||||
|
|
||||||
|
extension.getApplicationIcon(context)?.let { binding.extensionIcon.setImageDrawable(it) }
|
||||||
|
binding.extensionTitle.text = extension.name
|
||||||
|
binding.extensionVersion.text = context.getString(R.string.ext_version_info, extension.versionName)
|
||||||
|
binding.extensionLang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context))
|
||||||
|
binding.extensionPkg.text = extension.pkgName
|
||||||
|
|
||||||
|
binding.extensionUninstallButton.clicks()
|
||||||
|
.onEach { presenter.uninstallExtension() }
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
if (extension.isObsolete) {
|
||||||
|
binding.extensionWarningBanner.isVisible = true
|
||||||
|
binding.extensionWarningBanner.setText(R.string.obsolete_extension_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension.isUnofficial) {
|
||||||
|
binding.extensionWarningBanner.isVisible = true
|
||||||
|
binding.extensionWarningBanner.setText(R.string.unofficial_extension_message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.extension
|
package eu.kanade.tachiyomi.ui.browse.extension.details
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.extension
|
package eu.kanade.tachiyomi.ui.browse.extension.details
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@ -25,20 +25,16 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
|
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
|
||||||
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
||||||
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
|
import eu.kanade.tachiyomi.databinding.SourcePreferencesControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.getPreferenceKey
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
import timber.log.Timber
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import reactivecircus.flowbinding.android.view.clicks
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
class ExtensionDetailsController(bundle: Bundle? = null) :
|
class SourcePreferencesController(bundle: Bundle? = null) :
|
||||||
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
|
NucleusController<SourcePreferencesControllerBinding, SourcePreferencesPresenter>(bundle),
|
||||||
PreferenceManager.OnDisplayPreferenceDialogListener,
|
PreferenceManager.OnDisplayPreferenceDialogListener,
|
||||||
DialogPreference.TargetFragment {
|
DialogPreference.TargetFragment {
|
||||||
|
|
||||||
@ -46,46 +42,33 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
private var preferenceScreen: PreferenceScreen? = null
|
private var preferenceScreen: PreferenceScreen? = null
|
||||||
|
|
||||||
constructor(pkgName: String) : this(
|
constructor(sourceId: Long) : this(
|
||||||
Bundle().apply {
|
Bundle().apply {
|
||||||
putString(PKGNAME_KEY, pkgName)
|
putLong(SOURCE_ID, sourceId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
|
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
|
||||||
binding = ExtensionDetailControllerBinding.inflate(themedInflater)
|
binding = SourcePreferencesControllerBinding.inflate(themedInflater)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createPresenter(): ExtensionDetailsPresenter {
|
override fun createPresenter(): SourcePreferencesPresenter {
|
||||||
return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
|
return SourcePreferencesPresenter(args.getLong(SOURCE_ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): String? {
|
override fun getTitle(): String? {
|
||||||
return resources?.getString(R.string.label_extension_info)
|
return presenter.source?.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("PrivateResource")
|
@SuppressLint("PrivateResource")
|
||||||
override fun onViewCreated(view: View) {
|
override fun onViewCreated(view: View) {
|
||||||
super.onViewCreated(view)
|
super.onViewCreated(view)
|
||||||
|
|
||||||
val extension = presenter.extension ?: return
|
val source = presenter.source ?: return
|
||||||
val context = view.context
|
val context = view.context
|
||||||
|
|
||||||
binding.extensionTitle.text = extension.name
|
|
||||||
binding.extensionVersion.text = context.getString(R.string.ext_version_info, extension.versionName)
|
|
||||||
binding.extensionLang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context))
|
|
||||||
binding.extensionPkg.text = extension.pkgName
|
|
||||||
extension.getApplicationIcon(context)?.let { binding.extensionIcon.setImageDrawable(it) }
|
|
||||||
binding.extensionUninstallButton.clicks()
|
|
||||||
.onEach { presenter.uninstallExtension() }
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
if (extension.isObsolete) {
|
|
||||||
binding.extensionObsolete.visible()
|
|
||||||
}
|
|
||||||
|
|
||||||
val themedContext by lazy { getPreferenceThemeContext() }
|
val themedContext by lazy { getPreferenceThemeContext() }
|
||||||
val manager = PreferenceManager(themedContext)
|
val manager = PreferenceManager(themedContext)
|
||||||
manager.preferenceDataStore = EmptyPreferenceDataStore()
|
manager.preferenceDataStore = EmptyPreferenceDataStore()
|
||||||
@ -93,23 +76,17 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
val screen = manager.createPreferenceScreen(themedContext)
|
val screen = manager.createPreferenceScreen(themedContext)
|
||||||
preferenceScreen = screen
|
preferenceScreen = screen
|
||||||
|
|
||||||
val multiSource = extension.sources.size > 1
|
try {
|
||||||
|
addPreferencesForSource(screen, source)
|
||||||
for (source in extension.sources) {
|
} catch (e: AbstractMethodError) {
|
||||||
if (source is ConfigurableSource) {
|
Timber.e("Source did not implement [addPreferencesForSource]: ${source.name}")
|
||||||
addPreferencesForSource(screen, source, multiSource)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.setPreferences(screen)
|
manager.setPreferences(screen)
|
||||||
|
|
||||||
binding.extensionPrefsRecycler.layoutManager = LinearLayoutManager(context)
|
binding.recycler.layoutManager = LinearLayoutManager(context)
|
||||||
binding.extensionPrefsRecycler.adapter = PreferenceGroupAdapter(screen)
|
binding.recycler.adapter = PreferenceGroupAdapter(screen)
|
||||||
binding.extensionPrefsRecycler.addItemDecoration(DividerItemDecoration(context, VERTICAL))
|
binding.recycler.addItemDecoration(DividerItemDecoration(context, VERTICAL))
|
||||||
|
|
||||||
if (screen.preferenceCount == 0) {
|
|
||||||
binding.extensionPrefsEmptyView.show(R.string.ext_empty_preferences)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
@ -117,10 +94,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
super.onDestroyView(view)
|
super.onDestroyView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onExtensionUninstalled() {
|
|
||||||
router.popCurrentController()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
lastOpenPreferencePosition?.let { outState.putInt(LASTOPENPREFERENCE_KEY, it) }
|
lastOpenPreferencePosition?.let { outState.putInt(LASTOPENPREFERENCE_KEY, it) }
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
@ -131,24 +104,14 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
lastOpenPreferencePosition = savedInstanceState.get(LASTOPENPREFERENCE_KEY) as? Int
|
lastOpenPreferencePosition = savedInstanceState.get(LASTOPENPREFERENCE_KEY) as? Int
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addPreferencesForSource(screen: PreferenceScreen, source: Source, multiSource: Boolean) {
|
private fun addPreferencesForSource(screen: PreferenceScreen, source: Source) {
|
||||||
val context = screen.context
|
val context = screen.context
|
||||||
|
|
||||||
// TODO
|
val dataStore = SharedPreferencesDataStore(
|
||||||
val dataStore = SharedPreferencesDataStore(/*if (source is HttpSource) {
|
context.getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
||||||
source.preferences
|
|
||||||
} else {*/
|
|
||||||
context.getSharedPreferences("source_${source.id}", Context.MODE_PRIVATE)
|
|
||||||
/*}*/
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (source is ConfigurableSource) {
|
if (source is ConfigurableSource) {
|
||||||
if (multiSource) {
|
|
||||||
screen.preferenceCategory {
|
|
||||||
title = source.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val newScreen = screen.preferenceManager.createPreferenceScreen(context)
|
val newScreen = screen.preferenceManager.createPreferenceScreen(context)
|
||||||
source.setupPreferenceScreen(newScreen)
|
source.setupPreferenceScreen(newScreen)
|
||||||
|
|
||||||
@ -207,7 +170,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val PKGNAME_KEY = "pkg_name"
|
const val SOURCE_ID = "source_id"
|
||||||
const val LASTOPENPREFERENCE_KEY = "last_open_preference"
|
const val LASTOPENPREFERENCE_KEY = "last_open_preference"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.extension.details
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class SourcePreferencesPresenter(
|
||||||
|
val sourceId: Long,
|
||||||
|
sourceManager: SourceManager = Injekt.get()
|
||||||
|
) : BasePresenter<SourcePreferencesController>() {
|
||||||
|
|
||||||
|
val source = sourceManager.get(sourceId)
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.migration
|
package eu.kanade.tachiyomi.ui.browse.migration
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
@ -1,8 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.ui.migration
|
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
@ -26,11 +30,13 @@ class MangaHolder(
|
|||||||
|
|
||||||
// Update the cover.
|
// Update the cover.
|
||||||
GlideApp.with(itemView.context).clear(thumbnail)
|
GlideApp.with(itemView.context).clear(thumbnail)
|
||||||
|
|
||||||
|
val radius = itemView.context.resources.getDimensionPixelSize(R.dimen.card_radius)
|
||||||
|
val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
|
||||||
GlideApp.with(itemView.context)
|
GlideApp.with(itemView.context)
|
||||||
.load(item.manga.toMangaThumbnail())
|
.load(item.manga.toMangaThumbnail())
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
.centerCrop()
|
.apply(requestOptions)
|
||||||
.circleCrop()
|
|
||||||
.dontAnimate()
|
.dontAnimate()
|
||||||
.into(thumbnail)
|
.into(thumbnail)
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.migration
|
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
@ -7,15 +8,20 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
|||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
class MangaItem(val manga: Manga) : AbstractFlexibleItem<MangaHolder>() {
|
@Parcelize
|
||||||
|
class MangaItem(val manga: Manga) : AbstractFlexibleItem<MangaHolder>(), Parcelable {
|
||||||
|
|
||||||
override fun getLayoutRes(): Int {
|
override fun getLayoutRes(): Int {
|
||||||
return R.layout.source_list_item
|
return R.layout.source_list_item
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MangaHolder {
|
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MangaHolder {
|
||||||
return MangaHolder(view, adapter)
|
return MangaHolder(
|
||||||
|
view,
|
||||||
|
adapter
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bindViewHolder(
|
override fun bindViewHolder(
|
@ -0,0 +1,81 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
|
import eu.kanade.tachiyomi.databinding.MigrationMangaControllerBinding
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.SourceDividerItemDecoration
|
||||||
|
|
||||||
|
class MigrationMangaController :
|
||||||
|
NucleusController<MigrationMangaControllerBinding, MigrationMangaPresenter>,
|
||||||
|
FlexibleAdapter.OnItemClickListener {
|
||||||
|
|
||||||
|
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||||
|
|
||||||
|
constructor(sourceId: Long, sourceName: String?) : super(
|
||||||
|
Bundle().apply {
|
||||||
|
putLong(SOURCE_ID_EXTRA, sourceId)
|
||||||
|
putString(SOURCE_NAME_EXTRA, sourceName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
constructor(bundle: Bundle) : this(
|
||||||
|
bundle.getLong(SOURCE_ID_EXTRA),
|
||||||
|
bundle.getString(SOURCE_NAME_EXTRA)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val sourceId: Long = args.getLong(SOURCE_ID_EXTRA)
|
||||||
|
private val sourceName: String? = args.getString(SOURCE_NAME_EXTRA)
|
||||||
|
|
||||||
|
override fun getTitle(): String? {
|
||||||
|
return sourceName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPresenter(): MigrationMangaPresenter {
|
||||||
|
return MigrationMangaPresenter(sourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
|
binding = MigrationMangaControllerBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View) {
|
||||||
|
super.onViewCreated(view)
|
||||||
|
|
||||||
|
adapter = FlexibleAdapter<IFlexible<*>>(null, this)
|
||||||
|
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
|
binding.recycler.adapter = adapter
|
||||||
|
binding.recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
||||||
|
adapter?.fastScroller = binding.fastScroller
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView(view: View) {
|
||||||
|
adapter = null
|
||||||
|
super.onDestroyView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setManga(manga: List<MangaItem>) {
|
||||||
|
adapter?.updateDataSet(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
|
val item = adapter?.getItem(position) as? MangaItem ?: return false
|
||||||
|
val controller = SearchController(item.manga)
|
||||||
|
router.pushController(controller.withFadeTransaction())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SOURCE_ID_EXTRA = "source_id_extra"
|
||||||
|
const val SOURCE_NAME_EXTRA = "source_name_extra"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class MigrationMangaPresenter(
|
||||||
|
private val sourceId: Long,
|
||||||
|
private val db: DatabaseHelper = Injekt.get()
|
||||||
|
) : BasePresenter<MigrationMangaController>() {
|
||||||
|
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
db.getFavoriteMangas()
|
||||||
|
.asRxObservable()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.map { libraryToMigrationItem(it) }
|
||||||
|
.subscribeLatestCache(MigrationMangaController::setManga)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun libraryToMigrationItem(library: List<Manga>): List<MangaItem> {
|
||||||
|
return library.filter { it.source == sourceId }
|
||||||
|
.sortedBy { it.title }
|
||||||
|
.map { MangaItem(it) }
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,17 @@
|
|||||||
package eu.kanade.tachiyomi.ui.migration
|
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.list.listItemsMultiChoice
|
import com.afollestad.materialdialogs.list.listItemsMultiChoice
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class SearchController(
|
class SearchController(
|
||||||
@ -21,7 +21,10 @@ class SearchController(
|
|||||||
private var newManga: Manga? = null
|
private var newManga: Manga? = null
|
||||||
|
|
||||||
override fun createPresenter(): GlobalSearchPresenter {
|
override fun createPresenter(): GlobalSearchPresenter {
|
||||||
return SearchPresenter(initialQuery, manga!!)
|
return SearchPresenter(
|
||||||
|
initialQuery,
|
||||||
|
manga!!
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
@ -52,7 +55,8 @@ class SearchController(
|
|||||||
|
|
||||||
override fun onMangaClick(manga: Manga) {
|
override fun onMangaClick(manga: Manga) {
|
||||||
newManga = manga
|
newManga = manga
|
||||||
val dialog = MigrationDialog()
|
val dialog =
|
||||||
|
MigrationDialog()
|
||||||
dialog.targetController = this
|
dialog.targetController = this
|
||||||
dialog.showDialog(router)
|
dialog.showDialog(router)
|
||||||
}
|
}
|
||||||
@ -64,9 +68,9 @@ class SearchController(
|
|||||||
|
|
||||||
fun renderIsReplacingManga(isReplacingManga: Boolean) {
|
fun renderIsReplacingManga(isReplacingManga: Boolean) {
|
||||||
if (isReplacingManga) {
|
if (isReplacingManga) {
|
||||||
binding.progress.visible()
|
binding.progress.isVisible = true
|
||||||
} else {
|
} else {
|
||||||
binding.progress.gone()
|
binding.progress.isVisible = false
|
||||||
router.popController(this)
|
router.popController(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,7 +82,10 @@ class SearchController(
|
|||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
val prefValue = preferences.migrateFlags().get()
|
val prefValue = preferences.migrateFlags().get()
|
||||||
|
|
||||||
val preselected = MigrationFlags.getEnabledFlagsPositions(prefValue)
|
val preselected =
|
||||||
|
MigrationFlags.getEnabledFlagsPositions(
|
||||||
|
prefValue
|
||||||
|
)
|
||||||
|
|
||||||
return MaterialDialog(activity!!)
|
return MaterialDialog(activity!!)
|
||||||
.message(R.string.migration_dialog_what_to_include)
|
.message(R.string.migration_dialog_what_to_include)
|
||||||
@ -87,7 +94,10 @@ class SearchController(
|
|||||||
initialSelection = preselected.toIntArray()
|
initialSelection = preselected.toIntArray()
|
||||||
) { _, positions, _ ->
|
) { _, positions, _ ->
|
||||||
// Save current settings for the next time
|
// Save current settings for the next time
|
||||||
val newValue = MigrationFlags.getFlagsFromPositions(positions.toTypedArray())
|
val newValue =
|
||||||
|
MigrationFlags.getFlagsFromPositions(
|
||||||
|
positions.toTypedArray()
|
||||||
|
)
|
||||||
preferences.migrateFlags().set(newValue)
|
preferences.migrateFlags().set(newValue)
|
||||||
}
|
}
|
||||||
.positiveButton(R.string.migrate) {
|
.positiveButton(R.string.migrate) {
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.migration
|
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
@ -8,10 +8,12 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchCardItem
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchCardItem
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
|
import java.util.Date
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
@ -70,9 +72,18 @@ class SearchPresenter(
|
|||||||
replace: Boolean
|
replace: Boolean
|
||||||
) {
|
) {
|
||||||
val flags = preferences.migrateFlags().get()
|
val flags = preferences.migrateFlags().get()
|
||||||
val migrateChapters = MigrationFlags.hasChapters(flags)
|
val migrateChapters =
|
||||||
val migrateCategories = MigrationFlags.hasCategories(flags)
|
MigrationFlags.hasChapters(
|
||||||
val migrateTracks = MigrationFlags.hasTracks(flags)
|
flags
|
||||||
|
)
|
||||||
|
val migrateCategories =
|
||||||
|
MigrationFlags.hasCategories(
|
||||||
|
flags
|
||||||
|
)
|
||||||
|
val migrateTracks =
|
||||||
|
MigrationFlags.hasTracks(
|
||||||
|
flags
|
||||||
|
)
|
||||||
|
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
// Update chapters read
|
// Update chapters read
|
||||||
@ -137,6 +148,14 @@ class SearchPresenter(
|
|||||||
manga.viewer = prevManga.viewer
|
manga.viewer = prevManga.viewer
|
||||||
db.updateMangaViewer(manga).executeAsBlocking()
|
db.updateMangaViewer(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
// Update date added
|
||||||
|
if (replace) {
|
||||||
|
manga.date_added = prevManga.date_added
|
||||||
|
prevManga.date_added = 0
|
||||||
|
} else {
|
||||||
|
manga.date_added = Date().time
|
||||||
|
}
|
||||||
|
|
||||||
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
|
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
|
||||||
db.updateMangaTitle(manga).executeAsBlocking()
|
db.updateMangaTitle(manga).executeAsBlocking()
|
||||||
}
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.SourceDividerItemDecoration
|
||||||
|
|
||||||
|
class MigrationSourcesController :
|
||||||
|
NucleusController<MigrationSourcesControllerBinding, MigrationSourcesPresenter>(),
|
||||||
|
FlexibleAdapter.OnItemClickListener {
|
||||||
|
|
||||||
|
private var adapter: SourceAdapter? = null
|
||||||
|
|
||||||
|
override fun createPresenter(): MigrationSourcesPresenter {
|
||||||
|
return MigrationSourcesPresenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
|
binding = MigrationSourcesControllerBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View) {
|
||||||
|
super.onViewCreated(view)
|
||||||
|
|
||||||
|
adapter = SourceAdapter(this)
|
||||||
|
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
|
binding.recycler.adapter = adapter
|
||||||
|
binding.recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
||||||
|
adapter?.fastScroller = binding.fastScroller
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView(view: View) {
|
||||||
|
adapter = null
|
||||||
|
super.onDestroyView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSources(sourcesWithManga: List<SourceItem>) {
|
||||||
|
adapter?.updateDataSet(sourcesWithManga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
|
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
||||||
|
val controller = MigrationMangaController(item.source.id, item.source.name)
|
||||||
|
parentController!!.router.pushController(controller.withFadeTransaction())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class MigrationSourcesPresenter(
|
||||||
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
|
private val db: DatabaseHelper = Injekt.get()
|
||||||
|
) : BasePresenter<MigrationSourcesController>() {
|
||||||
|
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
db.getFavoriteMangas()
|
||||||
|
.asRxObservable()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.map { findSourcesWithManga(it) }
|
||||||
|
.subscribeLatestCache(MigrationSourcesController::setSources)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
|
||||||
|
val header = SelectionHeader()
|
||||||
|
return library.map { it.source }.toSet()
|
||||||
|
.mapNotNull { if (it != LocalSource.ID) sourceManager.getOrStub(it) else null }
|
||||||
|
.sortedBy { it.name }
|
||||||
|
.map { SourceItem(it, header) }
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.migration
|
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@ -25,7 +25,10 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
|
|||||||
* Creates a new view holder for this item.
|
* Creates a new view holder for this item.
|
||||||
*/
|
*/
|
||||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||||
return Holder(view, adapter)
|
return Holder(
|
||||||
|
view,
|
||||||
|
adapter
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@ -0,0 +1,22 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||||
|
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter that holds the catalogue cards.
|
||||||
|
*
|
||||||
|
* @param controller instance of [MigrationController].
|
||||||
|
*/
|
||||||
|
class SourceAdapter(val controller: Controller) :
|
||||||
|
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
||||||
|
|
||||||
|
val cardBackground = controller.activity!!.getResourceColor(R.attr.colorSurface)
|
||||||
|
|
||||||
|
init {
|
||||||
|
setDisplayHeadersAtStartUp(true)
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.ui.migration
|
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.source.icon
|
import eu.kanade.tachiyomi.source.icon
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
|
||||||
import io.github.mthli.slice.Slice
|
import io.github.mthli.slice.Slice
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.card
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.card
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.image
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.image
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_browse
|
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_latest
|
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.title
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.title
|
||||||
|
|
||||||
class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
||||||
@ -24,14 +20,6 @@ class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
|||||||
override val viewToSlice: View
|
override val viewToSlice: View
|
||||||
get() = card
|
get() = card
|
||||||
|
|
||||||
init {
|
|
||||||
source_latest.gone()
|
|
||||||
source_browse.setText(R.string.select)
|
|
||||||
source_browse.setOnClickListener {
|
|
||||||
adapter.selectClickListener?.onSelectClick(bindingAdapterPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bind(item: SourceItem) {
|
fun bind(item: SourceItem) {
|
||||||
val source = item.source
|
val source = item.source
|
||||||
setCardEdges(item)
|
setCardEdges(item)
|
||||||
@ -39,12 +27,9 @@ class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
|||||||
// Set source name
|
// Set source name
|
||||||
title.text = source.name
|
title.text = source.name
|
||||||
|
|
||||||
// Set circle letter image.
|
// Set source icon
|
||||||
itemView.post {
|
itemView.post {
|
||||||
val icon = source.icon()
|
image.setImageDrawable(source.icon())
|
||||||
if (icon != null) {
|
|
||||||
image.setImageDrawable(icon)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.migration
|
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
* @param source Instance of [Source] containing source information.
|
* @param source Instance of [Source] containing source information.
|
||||||
* @param header The header for this item.
|
* @param header The header for this item.
|
||||||
*/
|
*/
|
||||||
data class SourceItem(val source: Source, val header: SelectionHeader? = null) :
|
data class SourceItem(val source: Source, val header: SelectionHeader) :
|
||||||
AbstractSectionableItem<SourceHolder, SelectionHeader>(header) {
|
AbstractSectionableItem<SourceHolder, SelectionHeader>(header) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,7 +28,10 @@ data class SourceItem(val source: Source, val header: SelectionHeader? = null) :
|
|||||||
* Creates a new view holder for this item.
|
* Creates a new view holder for this item.
|
||||||
*/
|
*/
|
||||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
|
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
|
||||||
return SourceHolder(view, adapter as SourceAdapter)
|
return SourceHolder(
|
||||||
|
view,
|
||||||
|
adapter as SourceAdapter
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@ -22,26 +22,15 @@ class SourceAdapter(val controller: SourceController) :
|
|||||||
/**
|
/**
|
||||||
* Listener for browse item clicks.
|
* Listener for browse item clicks.
|
||||||
*/
|
*/
|
||||||
val browseClickListener: OnBrowseClickListener = controller
|
val clickListener: OnSourceClickListener = controller
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for latest item clicks.
|
|
||||||
*/
|
|
||||||
val latestClickListener: OnLatestClickListener = controller
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener which should be called when user clicks browse.
|
* Listener which should be called when user clicks browse.
|
||||||
* Note: Should only be handled by [SourceController]
|
* Note: Should only be handled by [SourceController]
|
||||||
*/
|
*/
|
||||||
interface OnBrowseClickListener {
|
interface OnSourceClickListener {
|
||||||
fun onBrowseClick(position: Int)
|
fun onBrowseClick(position: Int)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener which should be called when user clicks latest.
|
|
||||||
* Note: Should only be handled by [SourceController]
|
|
||||||
*/
|
|
||||||
interface OnLatestClickListener {
|
|
||||||
fun onLatestClick(position: Int)
|
fun onLatestClick(position: Int)
|
||||||
|
fun onPinClick(position: Int)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source
|
package eu.kanade.tachiyomi.ui.browse.source
|
||||||
|
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
@ -17,9 +19,13 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
|||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.minusAssign
|
||||||
|
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding
|
import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
@ -27,8 +33,7 @@ import eu.kanade.tachiyomi.ui.browse.BrowseController
|
|||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||||
@ -39,15 +44,14 @@ import uy.kohesive.injekt.api.get
|
|||||||
/**
|
/**
|
||||||
* This controller shows and manages the different catalogues enabled by the user.
|
* This controller shows and manages the different catalogues enabled by the user.
|
||||||
* This controller should only handle UI actions, IO actions should be done by [SourcePresenter]
|
* This controller should only handle UI actions, IO actions should be done by [SourcePresenter]
|
||||||
* [SourceAdapter.OnBrowseClickListener] call function data on browse item click.
|
* [SourceAdapter.OnSourceClickListener] call function data on browse item click.
|
||||||
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
|
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
|
||||||
*/
|
*/
|
||||||
class SourceController :
|
class SourceController :
|
||||||
NucleusController<SourceMainControllerBinding, SourcePresenter>(),
|
NucleusController<SourceMainControllerBinding, SourcePresenter>(),
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
SourceAdapter.OnBrowseClickListener,
|
SourceAdapter.OnSourceClickListener {
|
||||||
SourceAdapter.OnLatestClickListener {
|
|
||||||
|
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
|
||||||
@ -113,49 +117,52 @@ class SourceController :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(view: View, position: Int): Boolean {
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
onItemClick(position)
|
||||||
val source = item.source
|
|
||||||
openCatalogue(source, BrowseSourceController(source))
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onItemClick(position: Int) {
|
||||||
|
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||||
|
val source = item.source
|
||||||
|
openSource(source, BrowseSourceController(source))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onItemLongClick(position: Int) {
|
override fun onItemLongClick(position: Int) {
|
||||||
val activity = activity ?: return
|
val activity = activity ?: return
|
||||||
val item = adapter?.getItem(position) as? SourceItem ?: return
|
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||||
|
|
||||||
val isPinned = item.header?.code?.equals(SourcePresenter.PINNED_KEY) ?: false
|
val isPinned = item.header?.code?.equals(SourcePresenter.PINNED_KEY) ?: false
|
||||||
|
|
||||||
MaterialDialog(activity)
|
val items = mutableListOf(
|
||||||
.title(text = item.source.name)
|
Pair(
|
||||||
.listItems(
|
activity.getString(if (isPinned) R.string.action_unpin else R.string.action_pin),
|
||||||
items = listOf(
|
{ toggleSourcePin(item.source) }
|
||||||
activity.getString(R.string.action_hide),
|
)
|
||||||
activity.getString(if (isPinned) R.string.action_unpin else R.string.action_pin)
|
)
|
||||||
),
|
if (item.source !is LocalSource) {
|
||||||
waitForPositiveButton = false
|
items.add(
|
||||||
) { dialog, which, _ ->
|
Pair(
|
||||||
when (which) {
|
activity.getString(R.string.action_disable),
|
||||||
0 -> hideCatalogue(item.source)
|
{ disableSource(item.source) }
|
||||||
1 -> pinCatalogue(item.source, isPinned)
|
)
|
||||||
}
|
)
|
||||||
dialog.dismiss()
|
}
|
||||||
}
|
|
||||||
.show()
|
SourceOptionsDialog(item, items).showDialog(router)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideCatalogue(source: Source) {
|
private fun disableSource(source: Source) {
|
||||||
val current = preferences.hiddenCatalogues().get()
|
preferences.disabledSources() += source.id.toString()
|
||||||
preferences.hiddenCatalogues().set(current + source.id.toString())
|
|
||||||
|
|
||||||
presenter.updateSources()
|
presenter.updateSources()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pinCatalogue(source: Source, isPinned: Boolean) {
|
private fun toggleSourcePin(source: Source) {
|
||||||
val current = preferences.pinnedCatalogues().get()
|
val isPinned = source.id.toString() in preferences.pinnedSources().get()
|
||||||
if (isPinned) {
|
if (isPinned) {
|
||||||
preferences.pinnedCatalogues().set(current - source.id.toString())
|
preferences.pinnedSources() -= source.id.toString()
|
||||||
} else {
|
} else {
|
||||||
preferences.pinnedCatalogues().set(current + source.id.toString())
|
preferences.pinnedSources() += source.id.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
presenter.updateSources()
|
presenter.updateSources()
|
||||||
@ -165,7 +172,7 @@ class SourceController :
|
|||||||
* Called when browse is clicked in [SourceAdapter]
|
* Called when browse is clicked in [SourceAdapter]
|
||||||
*/
|
*/
|
||||||
override fun onBrowseClick(position: Int) {
|
override fun onBrowseClick(position: Int) {
|
||||||
onItemClick(view!!, position)
|
onItemClick(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -173,15 +180,23 @@ class SourceController :
|
|||||||
*/
|
*/
|
||||||
override fun onLatestClick(position: Int) {
|
override fun onLatestClick(position: Int) {
|
||||||
val item = adapter?.getItem(position) as? SourceItem ?: return
|
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||||
openCatalogue(item.source, LatestUpdatesController(item.source))
|
openSource(item.source, LatestUpdatesController(item.source))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when pin icon is clicked in [SourceAdapter]
|
||||||
|
*/
|
||||||
|
override fun onPinClick(position: Int) {
|
||||||
|
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||||
|
toggleSourcePin(item.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a catalogue with the given controller.
|
* Opens a catalogue with the given controller.
|
||||||
*/
|
*/
|
||||||
private fun openCatalogue(source: CatalogueSource, controller: BrowseSourceController) {
|
private fun openSource(source: CatalogueSource, controller: BrowseSourceController) {
|
||||||
preferences.lastUsedCatalogueSource().set(source.id)
|
preferences.lastUsedSource().set(source.id)
|
||||||
(parentController as BrowseController).pushController(controller.withFadeTransaction())
|
parentController!!.router.pushController(controller.withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -204,13 +219,13 @@ class SourceController :
|
|||||||
|
|
||||||
// Create query listener which opens the global search view.
|
// Create query listener which opens the global search view.
|
||||||
searchView.queryTextEvents()
|
searchView.queryTextEvents()
|
||||||
.filter { it is QueryTextEvent.QuerySubmitted }
|
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
|
||||||
.onEach { performGlobalSearch(it.queryText.toString()) }
|
.onEach { performGlobalSearch(it.queryText.toString()) }
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performGlobalSearch(query: String) {
|
private fun performGlobalSearch(query: String) {
|
||||||
(parentController as BrowseController).pushController(
|
parentController!!.router.pushController(
|
||||||
GlobalSearchController(query).withFadeTransaction()
|
GlobalSearchController(query).withFadeTransaction()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -225,8 +240,9 @@ class SourceController :
|
|||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
// Initialize option to open catalogue settings.
|
// Initialize option to open catalogue settings.
|
||||||
R.id.action_settings -> {
|
R.id.action_settings -> {
|
||||||
(parentController as BrowseController).pushController(
|
parentController!!.router.pushController(
|
||||||
SettingsSourcesController().withFadeTransaction()
|
SourceFilterController()
|
||||||
|
.withFadeTransaction()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -250,4 +266,27 @@ class SourceController :
|
|||||||
adapter?.addScrollableHeader(LangItem(SourcePresenter.LAST_USED_KEY))
|
adapter?.addScrollableHeader(LangItem(SourcePresenter.LAST_USED_KEY))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SourceOptionsDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
|
|
||||||
|
private lateinit var item: SourceItem
|
||||||
|
private lateinit var items: List<Pair<String, () -> Unit>>
|
||||||
|
|
||||||
|
constructor(item: SourceItem, items: List<Pair<String, () -> Unit>>) : this() {
|
||||||
|
this.item = item
|
||||||
|
this.items = items
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
return MaterialDialog(activity!!)
|
||||||
|
.title(text = item.source.toString())
|
||||||
|
.listItems(
|
||||||
|
items = items.map { it.first },
|
||||||
|
waitForPositiveButton = false
|
||||||
|
) { dialog, which, _ ->
|
||||||
|
items[which].second()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user