Compare commits
509 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51a109285a | |||
| 02601aa32a | |||
| 60e520e37e | |||
| 3fb4b565fa | |||
| b2e33ab950 | |||
| acad70d8db | |||
| ebc1f2bb41 | |||
| 391e477e04 | |||
| 5fa3d55bc9 | |||
| ef02bd112c | |||
| ae7d94a1f2 | |||
| 0a96252ce8 | |||
| 7907723623 | |||
| 8d3c0199b4 | |||
| d5fcded898 | |||
| 674a9c5067 | |||
| 1f37d571cf | |||
| 36c45c9450 | |||
| cc2f976c81 | |||
| d92e790c5e | |||
| 7d659f559e | |||
| 3a20e24ad0 | |||
| 9a97a97aa7 | |||
| 84076c2582 | |||
| b67d2bba40 | |||
| 9fa278f708 | |||
| 56ea025e20 | |||
| dab002cf4d | |||
| bca6f39a33 | |||
| fbfad27c27 | |||
| a93129c4f2 | |||
| 89531cd549 | |||
| e50f04ec54 | |||
| d8b40c2dc4 | |||
| d62de90b77 | |||
| ab14ce0d75 | |||
| f2efe49ea1 | |||
| 2eb5436b25 | |||
| fba4bd163b | |||
| ae073f9207 | |||
| 1c084d42df | |||
| f245bbcfeb | |||
| 29310c86e2 | |||
| f492ad2529 | |||
| 33b6912c22 | |||
| 9e63f32a82 | |||
| 834e3d78ab | |||
| 2d2378a1e2 | |||
| 3cc61bc3b8 | |||
| 6b771b4a70 | |||
| 9c2eadd8ca | |||
| f09c977661 | |||
| 2acc364960 | |||
| c63300d9ad | |||
| c90aec5c3d | |||
| 8e6f04f258 | |||
| 354c61cf59 | |||
| 606101dc8b | |||
| 97eda86ac4 | |||
| dd4384767c | |||
| b18075de14 | |||
| c39ec81f42 | |||
| 99d23a47b7 | |||
| 481f600056 | |||
| 143d0d2518 | |||
| 38fef11287 | |||
| 9adbb1b115 | |||
| ec9967d2d6 | |||
| ba0e353ea1 | |||
| e5131e1985 | |||
| 43555b3b1a | |||
| 3419bebb70 | |||
| b13ca1a097 | |||
| dd914047f8 | |||
| 666447faac | |||
| e9a21a6bbe | |||
| de4e0abef4 | |||
| ac7e2909ba | |||
| 14de1973a3 | |||
| 3053bf9d5d | |||
| 3192140421 | |||
| 80cf38a70d | |||
| 42bbf07859 | |||
| 9b4aac7ce5 | |||
| ab64e51c6b | |||
| 82a92b9497 | |||
| a855c4a929 | |||
| de6c428d72 | |||
| a34133f526 | |||
| 56d127003f | |||
| 323ce4dbab | |||
| a802eb1cca | |||
| 3bb005acb2 | |||
| d8ee654a65 | |||
| 2b075b5a39 | |||
| e085855ec6 | |||
| 1518c2aa25 | |||
| 623b64aa79 | |||
| 2f9069765e | |||
| eaa2bb22ed | |||
| 1389649553 | |||
| b3ddc1dfa3 | |||
| d55802a2f2 | |||
| 08cb46f6cd | |||
| 15482914ef | |||
| 3e5e983b9e | |||
| 36484bc349 | |||
| 85362e2030 | |||
| 3ebdd7b351 | |||
| 2a32c9cb5d | |||
| 677f93a6ec | |||
| 584f7b516f | |||
| 7d49479783 | |||
| 970e6e2560 | |||
| 19d539b197 | |||
| 97e47b98b1 | |||
| 844fb8129c | |||
| 7a9c2afe87 | |||
| 35879921dd | |||
| f5a90d46ec | |||
| d332f98b34 | |||
| b339bd4f3f | |||
| de414fb49b | |||
| 185b7fe70e | |||
| a947fcd9b9 | |||
| 7b9f5474bb | |||
| 6238f06d39 | |||
| 7e612e63b4 | |||
| 63139a5c08 | |||
| b63df25f7b | |||
| 44385ed9cc | |||
| 9eb1927d2e | |||
| 20cbadb23d | |||
| e37d4afce6 | |||
| fbc98ddb0a | |||
| 91ca176c28 | |||
| fd65db51c1 | |||
| 33a590d895 | |||
| bc871cd2ee | |||
| 65f66630cf | |||
| ab0f5d107f | |||
| 4a2d9dbdf8 | |||
| 08f1eff450 | |||
| e500d0bebf | |||
| 2629b3420b | |||
| 81e7d674a2 | |||
| 0c33b7915b | |||
| 29e4392490 | |||
| ba9db7ceb9 | |||
| 32197b1491 | |||
| 3f56c81c03 | |||
| b657af8d1c | |||
| 273d61e69b | |||
| 2ec5581e8c | |||
| be6637c7fd | |||
| 93d317629f | |||
| df188b7b90 | |||
| 27f2e8ecbc | |||
| 44ba757ad8 | |||
| d04cdd9b34 | |||
| 333f55a44b | |||
| 1f9b69fc07 | |||
| 09802c3609 | |||
| 93fe927de2 | |||
| 428a9e82f3 | |||
| 7e1389ef05 | |||
| fc354f5792 | |||
| c26adbb81d | |||
| 31473351af | |||
| 5c2d26aa7c | |||
| f0a2b85dd5 | |||
| 85398f7c30 | |||
| c787498b85 | |||
| 8532a9e2c5 | |||
| aa6013b7ca | |||
| cb2432cce9 | |||
| dd81e5f2b9 | |||
| c3be087472 | |||
| 84a1da2952 | |||
| e62de734aa | |||
| 866a92474f | |||
| b297d580b0 | |||
| 858a5e6eee | |||
| 7c3c452ac2 | |||
| b9b5ef55ab | |||
| 8686fecb1f | |||
| c5148b4739 | |||
| a4933388aa | |||
| 9095c98159 | |||
| 8c2de86b16 | |||
| 635dd0cda5 | |||
| bde5d4da26 | |||
| 4f02f652d9 | |||
| 6dad90e19c | |||
| 8fdd6c3bf9 | |||
| 4cbf647365 | |||
| 86d67b9bf7 | |||
| 69aebb5571 | |||
| 00afc11d4f | |||
| c518b593ce | |||
| 5c352cb3c0 | |||
| 73c9df9c43 | |||
| 8b2f24c86a | |||
| 39c61a77b1 | |||
| d09bcafe7d | |||
| 1fdf2225d9 | |||
| b4a226157c | |||
| 39d6319e8f | |||
| 765eac843d | |||
| 9a6b8a3f41 | |||
| 28fab7a918 | |||
| 900aa155ca | |||
| f9b4cb5bc9 | |||
| 1147bab1ce | |||
| 0aebe1da43 | |||
| f45fdca168 | |||
| fc5eb4cccc | |||
| 8ac309c4ae | |||
| f170446c5f | |||
| 643bec9bbb | |||
| 134be3893e | |||
| 5855822edd | |||
| 3343b766a2 | |||
| 329d24c7db | |||
| bdfbc641d9 | |||
| 6e570d7fad | |||
| b5d696ebe2 | |||
| 5299ae4856 | |||
| a9038831da | |||
| f1a8132307 | |||
| 76185338bf | |||
| bda4aae83d | |||
| 80bf908133 | |||
| 91b49f8a0c | |||
| 80a5a54e60 | |||
| 3104f3a8b5 | |||
| 4fa2c968a9 | |||
| be1e7f28ef | |||
| 4118b13e5b | |||
| 7e0f2950c1 | |||
| a8c4da9e2b | |||
| 72d315b6ed | |||
| b886f0a55a | |||
| 63fa1ee75e | |||
| 7d1ad7efb6 | |||
| 56400febd1 | |||
| aa56698dac | |||
| d37b24adb1 | |||
| d3778ac6e1 | |||
| e43777bba7 | |||
| 0b3209284a | |||
| a1be070e99 | |||
| eda47cd546 | |||
| 54d8748c58 | |||
| 77c17f2556 | |||
| d81e4158cb | |||
| 77061067ee | |||
| 707af702c1 | |||
| 1b0b98b140 | |||
| 2220b6a91d | |||
| 22b8f51fa3 | |||
| 08f0e515d5 | |||
| 28b4281683 | |||
| 676f716fcb | |||
| 665784a241 | |||
| 0d16609f95 | |||
| b46500c837 | |||
| fb5872ef51 | |||
| bc28e2d617 | |||
| de0c55117d | |||
| 936997b52e | |||
| 885c251fb4 | |||
| b8e907cea2 | |||
| f4c6b2e09c | |||
| 13f4bfa7bc | |||
| 780c1e68a6 | |||
| f10944521c | |||
| af5ebeca56 | |||
| 01ad3dc92b | |||
| 5c1423be86 | |||
| 382c23e0fd | |||
| 189b15fee6 | |||
| d3d937fe17 | |||
| 5af0e7e847 | |||
| 142bdd14b7 | |||
| 0483097fc3 | |||
| a3c26c63d4 | |||
| fbe10151f4 | |||
| 92fc5ea4a0 | |||
| d2b620f485 | |||
| 78aa57579d | |||
| b0a2d8908f | |||
| de36cd0626 | |||
| b322ecd34a | |||
| 540e234562 | |||
| f6be2c7a2a | |||
| c5df8725de | |||
| 738e2f7cf1 | |||
| 0ac56750c8 | |||
| 3fb1b4affa | |||
| 2602c49756 | |||
| d23b3c82ba | |||
| 6fa67c9a5f | |||
| 7a85d6b163 | |||
| 5a909f48b6 | |||
| 4d22db919d | |||
| 8a9f2cce10 | |||
| ede0892cda | |||
| 5df0eb7ed1 | |||
| 67cb42ff30 | |||
| e65ea94a08 | |||
| fdac8a0380 | |||
| 1c56624d13 | |||
| 7c05c59501 | |||
| af77a58dcb | |||
| 5ee87ce8fc | |||
| 348ef2cf0f | |||
| 828944950b | |||
| 1c67e82325 | |||
| a45e273e2c | |||
| 45cf4adb5b | |||
| eb823cb208 | |||
| 056358fb9d | |||
| 9e40625c08 | |||
| 9684e34241 | |||
| 84fdd097e0 | |||
| a3c44fc5ad | |||
| 196e437da5 | |||
| 5e8b5ef6cf | |||
| 1ba07466ef | |||
| eb88c9c94b | |||
| d6cab9f9a5 | |||
| bcc120056c | |||
| 0e8aec7929 | |||
| 2d4e589db8 | |||
| 3eecf5cb20 | |||
| 6b08889c15 | |||
| 3bf070d88a | |||
| 6d9753f361 | |||
| f6b9867ce8 | |||
| 03366ae7e5 | |||
| a70a6cbe49 | |||
| b5a109440f | |||
| f6acf9325a | |||
| b0c0b12499 | |||
| 30ed1f11ee | |||
| 3077dc24ec | |||
| c2e882cb5b | |||
| 835351f206 | |||
| cee8335518 | |||
| 3aa5a36fdd | |||
| 74795bcc5e | |||
| 38a46825e2 | |||
| 7073e9b9e5 | |||
| 620887f90b | |||
| e38a0d47ac | |||
| eb9de3e6f1 | |||
| 37d9a51706 | |||
| acb9bafa0a | |||
| 7c4e89cbc5 | |||
| 5842765eda | |||
| 0925bd6a37 | |||
| 2ddf5f5037 | |||
| 367d95c825 | |||
| 6951314744 | |||
| d294db3e4e | |||
| b2cf1266ba | |||
| fb01b547de | |||
| d3482ef734 | |||
| d622c659eb | |||
| d1c497aa60 | |||
| 29a882eebb | |||
| 90ffb8cdf6 | |||
| dc760c0596 | |||
| 7be8062a2e | |||
| de9ce8f949 | |||
| 3c3f5cf35d | |||
| 7407e22b4e | |||
| 3a18e76089 | |||
| fa67ff165e | |||
| b9d2591e2a | |||
| 404a6a621a | |||
| aa376dc3a5 | |||
| 4ee110e225 | |||
| 26d52f5ad7 | |||
| 8b37c27a73 | |||
| 6e9043c633 | |||
| 2988524fd8 | |||
| 95c828bed6 | |||
| 8721d8c9ec | |||
| 5b9d2175e2 | |||
| 75f0ab2f40 | |||
| 709f76d53d | |||
| ac654340d8 | |||
| 438f64a358 | |||
| 41aec8bc96 | |||
| 97342723bf | |||
| a1cb3afe77 | |||
| 1165c57ffa | |||
| 565f005692 | |||
| 3a148c73ac | |||
| 12962b3486 | |||
| 75da7dcbdd | |||
| f02e3ae28f | |||
| c6369ed73f | |||
| fae2bd7ab7 | |||
| 03912407d5 | |||
| 879b41e97d | |||
| 6c3a957733 | |||
| 3d7c00c057 | |||
| 6e1adf6e04 | |||
| 23091cf50a | |||
| 78d49b0742 | |||
| 30250e350f | |||
| d9b3b7b266 | |||
| 5558790e15 | |||
| a1a9b4b812 | |||
| aac2fcb7d4 | |||
| 69ddd04256 | |||
| 7624abbebd | |||
| 67310ada53 | |||
| aa73670d50 | |||
| 2bde782211 | |||
| 7b01f0c608 | |||
| 781f4e393e | |||
| 93c92b674d | |||
| 368f565942 | |||
| 01c298bbc1 | |||
| 1399042efb | |||
| b2bfccdeae | |||
| 0d46e00b31 | |||
| 9aca115977 | |||
| e31e71ad44 | |||
| df950219f5 | |||
| 23e4b661bc | |||
| 7164f686d4 | |||
| 3122f783a9 | |||
| 6be8e2de3c | |||
| c092127404 | |||
| e7dd5f3c25 | |||
| 142fc0e4a6 | |||
| 300e04e8f6 | |||
| 07f684ac9e | |||
| 6840382df2 | |||
| c7b6216d24 | |||
| a989426d95 | |||
| d255ee805b | |||
| 21240cad06 | |||
| 5b8b10a96b | |||
| c600d45e84 | |||
| e9fd6ab470 | |||
| 3d507600cb | |||
| 84abe044a3 | |||
| 04200bb590 | |||
| 42d49b7cba | |||
| 5dace4fd74 | |||
| ccdae6bb9a | |||
| 984956ce95 | |||
| 0fd9b2a8f6 | |||
| 39f4949189 | |||
| f7d52e0372 | |||
| 6cad8411fe | |||
| f35abccfd9 | |||
| f3573d16b4 | |||
| e6f288e2c9 | |||
| 833bd6e655 | |||
| 4a30c68cfc | |||
| 346bd5f57a | |||
| c2e3b4d35a | |||
| 1fdb03f7db | |||
| da3681e602 | |||
| d64a8907eb | |||
| 7e91ae02f1 | |||
| 9457b832fc | |||
| d0561705fe | |||
| 3601968342 | |||
| fa2cde79ba | |||
| 1827fe0ce1 | |||
| 3447e0c237 | |||
| 4f9ae9cc75 | |||
| cd1c6cbc89 | |||
| 66cd4c9b40 | |||
| 2e1cf49d99 | |||
| 0c150694e7 | |||
| a4c10394b6 | |||
| f78836dac4 | |||
| c88de1ab1b | |||
| 9694c8310c | |||
| 1b09eecfce | |||
| 853e8faec5 | |||
| cfd2d43f1c | |||
| 1d3542b648 | |||
| 6dc7b9de92 | |||
| 48a63e26f3 | |||
| 33b1c93949 | |||
| 7a115d8080 | |||
| 9be7c5e6e1 | |||
| e38d1dfdc4 | |||
| f1f993bf38 | |||
| 2845d8cc98 | |||
| 0185d5f7d6 | |||
| 079ca1d0b3 | |||
| 5a67d8169d | |||
| f1cb4c38a2 | |||
| 50a5ec45b3 | |||
| f76216c038 | |||
| d55692dc0d | |||
| ded8f15913 | |||
| 845dbbfa1e |
@@ -1,2 +1 @@
|
||||
github: inorichi
|
||||
ko_fi: inorichi
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.5.0)
|
||||
- I have updated all extensions
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v1.7.0)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
@@ -24,3 +30,5 @@ I acknowledge that:
|
||||
|
||||
## Other details
|
||||
Additional details and attachments.
|
||||
|
||||
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
||||
|
||||
@@ -9,9 +9,15 @@ labels: "bug"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.5.0)
|
||||
- I have updated all extensions
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v1.7.0)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
@@ -34,3 +40,5 @@ This happened instead.
|
||||
|
||||
## Other details
|
||||
Additional details and attachments.
|
||||
|
||||
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
||||
|
||||
@@ -9,9 +9,14 @@ labels: "feature"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.5.0)
|
||||
- I have updated all extensions
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v1.7.0)
|
||||
- All extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 489 KiB |
@@ -32,10 +32,10 @@ jobs:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JDK 11
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 1.8
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
|
||||
@@ -7,31 +7,30 @@ jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Autoclose when created in wrong repo
|
||||
uses: arkon/issue-closer-action@v1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
type: title
|
||||
regex: ".*THIS ISSUE IS IN THE WRONG REPO.*"
|
||||
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."
|
||||
- name: Autoclose issues
|
||||
uses: arkon/issue-closer-action@v3.4
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
rules: |
|
||||
[
|
||||
{
|
||||
"type": "title",
|
||||
"regex": ".*THIS ISSUE IS IN THE WRONG REPO.*",
|
||||
"message": "It was not opened in the correct repo, as the template mentioned."
|
||||
},
|
||||
{
|
||||
"type": "title",
|
||||
"regex": ".*<Write short description here>*",
|
||||
"message": "The description in the title was not filled out."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||
"message": "Requested information in the template was not filled out."
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Issue moderator
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
moderate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Moderate issues
|
||||
uses: tachiyomiorg/issue-moderator-action@v1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Lock threads
|
||||
|
||||
on:
|
||||
# Daily
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
# Manual trigger
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: '2'
|
||||
pr-lock-inactive-days: '2'
|
||||
@@ -0,0 +1,76 @@
|
||||
# Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -1,17 +1,17 @@
|
||||
| Preview Builds | Release Builds | Tachiyomi Support Server |
|
||||
|-------|----------|----------|
|
||||
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases/latest) | [](https://discord.gg/tachiyomi) |
|
||||
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases/latest) | [](https://discord.gg/tachiyomi) |
|
||||
|
||||
|
||||
# TachiyomiSY
|
||||
Tachiyomi is a free and open source manga reader for Android 5.0 and above. This version of Tachiyomi, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
|
||||
Tachiyomi is a free and open source manga reader for Android 6.0 and above. This version of Tachiyomi, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
Features of Tachiyomi(original) include:
|
||||
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
||||
* Online reading from [a variety of sources](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
||||
* Local reading of downloaded manga
|
||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
||||
@@ -109,7 +109,12 @@ Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-e
|
||||
|
||||
<details><summary>Contributing</summary>
|
||||
|
||||
See [CONTRIBUTING.md](https://github.com/tachiyomiorg/tachiyomi/blob/master/CONTRIBUTING.md).
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
</details>
|
||||
|
||||
<details><summary>Code of Conduct</summary>
|
||||
|
||||
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
|
||||
</details>
|
||||
|
||||
## FAQ
|
||||
|
||||
+42
-50
@@ -8,11 +8,11 @@ plugins {
|
||||
id("com.android.application")
|
||||
id("com.mikepenz.aboutlibraries.plugin")
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
kotlin("plugin.parcelize")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.github.zellius.shortcut-helper")
|
||||
// Realm (EH)
|
||||
kotlin("kapt")
|
||||
id("realm-android")
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ android {
|
||||
minSdkVersion(AndroidConfig.minSdk)
|
||||
targetSdkVersion(AndroidConfig.targetSdk)
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode = 13
|
||||
versionName = "1.5.0"
|
||||
versionCode = 19
|
||||
versionName = "1.7.0"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@@ -62,14 +62,12 @@ android {
|
||||
applicationIdSuffix = ".rt"
|
||||
//isMinifyEnabled = true
|
||||
//isShrinkResources = true
|
||||
isZipAlignEnabled = true
|
||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
|
||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
|
||||
}
|
||||
named("release") {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
isZipAlignEnabled = true
|
||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
|
||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +93,7 @@ android {
|
||||
exclude("META-INF/LICENSE")
|
||||
exclude("META-INF/LICENSE.txt")
|
||||
exclude("META-INF/NOTICE")
|
||||
exclude("META-INF/*.kotlin_module")
|
||||
|
||||
// Compatibility for two RxJava versions (EXH)
|
||||
exclude("META-INF/rxjava.properties")
|
||||
@@ -123,34 +122,34 @@ android {
|
||||
dependencies {
|
||||
|
||||
// Source models and interfaces from Tachiyomi 1.x
|
||||
implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||
implementation("org.tachiyomi:source-api:1.1")
|
||||
|
||||
// AndroidX libraries
|
||||
implementation("androidx.annotation:annotation:1.2.0-beta01")
|
||||
implementation("androidx.appcompat:appcompat:1.3.0-beta01")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha02")
|
||||
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha01")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
||||
implementation("androidx.browser:browser:1.3.0")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0-alpha2")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta02")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||
implementation("androidx.core:core-ktx:1.5.0-beta01")
|
||||
implementation("androidx.core:core-ktx:1.6.0-beta01")
|
||||
implementation("androidx.multidex:multidex:2.0.1")
|
||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.0-beta01")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
|
||||
val lifecycleVersion = "2.3.0-rc01"
|
||||
val lifecycleVersion = "2.4.0-alpha01"
|
||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||
|
||||
// Job scheduling
|
||||
implementation("androidx.work:work-runtime-ktx:2.5.0")
|
||||
implementation("androidx.work:work-runtime-ktx:2.7.0-alpha03")
|
||||
|
||||
// UI library
|
||||
implementation("com.google.android.material:material:1.3.0")
|
||||
implementation("com.google.android.material:material:1.4.0-beta01")
|
||||
|
||||
"standardImplementation"("com.google.firebase:firebase-core:18.0.2")
|
||||
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
||||
|
||||
// ReactiveX
|
||||
implementation("io.reactivex:rxandroid:1.2.1")
|
||||
@@ -159,7 +158,7 @@ dependencies {
|
||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||
|
||||
// Network client
|
||||
val okhttpVersion = "4.10.0-RC1"
|
||||
val okhttpVersion = "4.9.1"
|
||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||
@@ -169,7 +168,7 @@ dependencies {
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.1")
|
||||
|
||||
// JSON
|
||||
val kotlinSerializationVersion = "1.0.1"
|
||||
val kotlinSerializationVersion = "1.1.0"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
implementation("com.google.code.gson:gson:2.8.6")
|
||||
@@ -180,7 +179,7 @@ dependencies {
|
||||
|
||||
// Disk
|
||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||
implementation("com.github.inorichi:unifile:e9ee588")
|
||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
||||
implementation("com.github.junrar:junrar:7.4.0")
|
||||
|
||||
// HTML parser
|
||||
@@ -190,10 +189,10 @@ dependencies {
|
||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||
implementation("io.requery:sqlite-android:3.33.0")
|
||||
implementation("com.github.requery:sqlite-android:3.35.5")
|
||||
|
||||
// Preferences
|
||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3")
|
||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
||||
|
||||
// Model View Presenter
|
||||
val nucleusVersion = "3.0.0"
|
||||
@@ -204,14 +203,14 @@ dependencies {
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
|
||||
// Image library
|
||||
val glideVersion = "4.11.0"
|
||||
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
||||
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
|
||||
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
||||
val coilVersion = "1.2.0"
|
||||
implementation("io.coil-kt:coil:$coilVersion")
|
||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
||||
|
||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:6caf219")
|
||||
// TODO: switch to new decoder for stable releases
|
||||
// implementation("com.github.tachiyomiorg:subsampling-scale-image-view:ca26317")
|
||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
||||
exclude(module = "image-decoder")
|
||||
}
|
||||
implementation("com.github.tachiyomiorg:image-decoder:7a44c9b")
|
||||
|
||||
// Logging
|
||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||
@@ -223,13 +222,13 @@ dependencies {
|
||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||
|
||||
// UI
|
||||
implementation("com.dmitrymalkovich.android:material-design-dimens:1.4")
|
||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
||||
implementation("eu.davidea:flexible-adapter:5.1.0")
|
||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:7d0617d")
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
||||
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
||||
|
||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
||||
val materialDialogsVersion = "3.1.1"
|
||||
@@ -242,10 +241,10 @@ dependencies {
|
||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
||||
exclude(group = "com.android.support")
|
||||
}
|
||||
implementation("com.github.tachiyomiorg:conductor-support-preference:1.1.1")
|
||||
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
|
||||
|
||||
// FlowBinding
|
||||
val flowbindingVersion = "0.12.0"
|
||||
val flowbindingVersion = "1.0.0"
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||
@@ -256,7 +255,7 @@ dependencies {
|
||||
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||
|
||||
// Tests
|
||||
testImplementation("junit:junit:4.13.1")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
||||
testImplementation("org.mockito:mockito-core:1.10.19")
|
||||
|
||||
@@ -267,12 +266,12 @@ dependencies {
|
||||
|
||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||
|
||||
val coroutinesVersion = "1.4.2"
|
||||
val coroutinesVersion = "1.5.0"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
|
||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
||||
|
||||
// SY -->
|
||||
// [EXH] Android 7 SSL Workaround
|
||||
@@ -285,11 +284,11 @@ dependencies {
|
||||
implementation ("info.debatty:java-string-similarity:2.0.0")
|
||||
|
||||
// Firebase (EH)
|
||||
implementation("com.google.firebase:firebase-analytics-ktx:18.0.0")
|
||||
implementation("com.google.firebase:firebase-crashlytics-ktx:17.3.0")
|
||||
implementation("com.google.firebase:firebase-analytics-ktx:19.0.0")
|
||||
implementation("com.google.firebase:firebase-crashlytics-ktx:18.0.0")
|
||||
|
||||
// Better logging (EH)
|
||||
implementation("com.elvishew:xlog:1.7.1")
|
||||
implementation("com.elvishew:xlog:1.9.0")
|
||||
|
||||
// Debug utils (EH)
|
||||
val debugOverlayVersion = "1.1.3"
|
||||
@@ -299,15 +298,7 @@ dependencies {
|
||||
testImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
|
||||
|
||||
// RatingBar (SY)
|
||||
implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
|
||||
|
||||
// JsonReader for similar manga
|
||||
implementation("com.squareup.moshi:moshi:1.11.0")
|
||||
|
||||
implementation("androidx.gridlayout:gridlayout:1.0.0")
|
||||
|
||||
implementation("com.mikepenz:fastadapter:5.3.4")
|
||||
// SY -->
|
||||
implementation("me.zhanghai.android.materialratingbar:library:1.4.0")
|
||||
}
|
||||
|
||||
tasks {
|
||||
@@ -320,7 +311,8 @@ tasks {
|
||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi"
|
||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
-allowaccessmodification
|
||||
-dontusemixedcaseclassnames
|
||||
-verbose
|
||||
|
||||
-keepattributes *Annotation*
|
||||
|
||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
public static ** valueOf(java.lang.String);
|
||||
}
|
||||
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
-keep class androidx.annotation.Keep
|
||||
|
||||
-keep @androidx.annotation.Keep class * {*;}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <methods>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <fields>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <init>(...);
|
||||
}
|
||||
Vendored
+15
-42
@@ -58,6 +58,7 @@
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
# Filter serializer
|
||||
-keep,includedescriptorclasses class xyz.nulldev.ts.api.http.serializer.**$$serializer { *; }
|
||||
-keepclassmembers class xyz.nulldev.ts.api.http.serializer.** {
|
||||
*** Companion;
|
||||
@@ -66,37 +67,22 @@
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
# Madokami extension username and password crash fix
|
||||
-keepclassmembers class androidx.preference.EditTextPreference {
|
||||
*** mOnBindEditTextListener;
|
||||
*** mText;
|
||||
public *;
|
||||
}
|
||||
|
||||
# Hitomi extension crash fix
|
||||
-keepclassmembers class rx.Single {
|
||||
*** onSubscribe;
|
||||
final *;
|
||||
protected *;
|
||||
public *;
|
||||
}
|
||||
# Keep extension's common dependencies
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||
-keep,allowoptimization class rx.** { public protected *; }
|
||||
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
||||
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
||||
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
||||
-keep,allowoptimization class androidx.preference.** { *; }
|
||||
-keep,allowoptimization class okio.** { *; }
|
||||
-keep,allowoptimization class kotlinx.serialization.** { *; }
|
||||
|
||||
# RxJava 1.1.0
|
||||
-dontwarn sun.misc.**
|
||||
|
||||
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
||||
long producerIndex;
|
||||
long consumerIndex;
|
||||
}
|
||||
|
||||
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
|
||||
rx.internal.util.atomic.LinkedQueueNode producerNode;
|
||||
}
|
||||
|
||||
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
|
||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||
}
|
||||
|
||||
-dontnote rx.internal.util.PlatformDependent
|
||||
|
||||
# === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration
|
||||
@@ -133,8 +119,9 @@
|
||||
# Application classes that will be serialized/deserialized over Gson
|
||||
-keep class com.google.gson.examples.android.model.** { <fields>; }
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||
# Prevent proguard from stripping interface information from TypeAdapterFactory, TypeAdapter,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * extends com.google.gson.TypeAdapter
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
@@ -155,20 +142,6 @@
|
||||
## From original config: "Attempt to fix: java.lang.NoClassDefFoundError: uy.kohesive.injekt.registry.default.DefaultRegistrar$NOKEY$1"
|
||||
-keep class uy.kohesive.injekt.** { *; }
|
||||
|
||||
|
||||
# === Glide
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
|
||||
-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
|
||||
|
||||
# === Glide-transformations: https://github.com/wasabeef/glide-transformations/blob/3aa8e53c6a51b8351d312f802ba1354c5b115168/transformations/proguard-rules.txt
|
||||
-dontwarn jp.co.cyberagent.android.gpuimage.**
|
||||
|
||||
# === Conductor
|
||||
# This isn't in the consumer proguard rules yet: https://github.com/bluelinelabs/Conductor/pull/550/files
|
||||
-keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler {
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
android:shortcutDisabledMessage="@string/app_not_available"
|
||||
android:shortcutId="show_recently_updated"
|
||||
android:shortcutLongLabel="@string/label_recent_updates"
|
||||
android:shortcutShortLabel="@string/short_recent_updates">
|
||||
android:shortcutShortLabel="@string/label_recent_updates">
|
||||
<intent
|
||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- For managing extensions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
@@ -32,7 +33,7 @@
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.Tachiyomi.Light"
|
||||
android:theme="@style/Theme.Base"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
@@ -83,8 +84,8 @@
|
||||
android:resource="@xml/s_pen_actions"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.security.BiometricUnlockActivity"
|
||||
android:theme="@style/Theme.Splash" />
|
||||
android:name=".ui.security.UnlockActivity"
|
||||
android:theme="@style/Theme.Base" />
|
||||
<activity
|
||||
android:name=".ui.webview.WebViewActivity"
|
||||
android:configChanges="uiMode|orientation|screenSize" />
|
||||
@@ -149,6 +150,10 @@
|
||||
android:name=".extension.util.ExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<activity
|
||||
android:name="exh.ui.login.EhLoginActivity"
|
||||
android:label="EHentaiLogin" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
@@ -184,10 +189,6 @@
|
||||
android:exported="false" />
|
||||
|
||||
<!-- EH -->
|
||||
<service
|
||||
android:name="exh.md.similar.SimilarUpdateService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name="exh.eh.EHentaiUpdateWorker"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
@@ -195,7 +196,7 @@
|
||||
<activity
|
||||
android:name="exh.ui.intercept.InterceptActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.EHActivity">
|
||||
android:theme="@style/Theme.Base">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -315,7 +316,7 @@
|
||||
android:scheme="https" />
|
||||
|
||||
<!-- MangaDex -->
|
||||
<data
|
||||
<!--<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.org"
|
||||
android:pathPrefix="/manga/" />
|
||||
@@ -364,12 +365,12 @@
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.cc"
|
||||
android:pathPrefix="/chapter/" />
|
||||
android:pathPrefix="/chapter/" />-->
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="exh.ui.captcha.BrowserActionActivity"
|
||||
android:theme="@style/Theme.EHActivity" />
|
||||
android:theme="@style/Theme.Base" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.webkit.WebView
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.multidex.MultiDex
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import com.elvishew.xlog.LogConfiguration
|
||||
import com.elvishew.xlog.LogLevel
|
||||
import com.elvishew.xlog.XLog
|
||||
@@ -22,28 +35,34 @@ import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||
import com.google.android.gms.security.ProviderInstaller
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.ktx.analytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.ms_square.debugoverlay.DebugOverlay
|
||||
import com.ms_square.debugoverlay.modules.FpsModule
|
||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import exh.debug.DebugToggles
|
||||
import exh.log.CrashlyticsPrinter
|
||||
import exh.log.EHDebugModeOverlay
|
||||
import exh.log.EHLogLevel
|
||||
import exh.log.EnhancedFilePrinter
|
||||
import exh.log.XLogTree
|
||||
import exh.log.xLogD
|
||||
import exh.log.xLogE
|
||||
import exh.syDebugVersion
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.conscrypt.Conscrypt
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.security.NoSuchAlgorithmException
|
||||
@@ -51,20 +70,20 @@ import java.security.Security
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import javax.net.ssl.SSLContext
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.days
|
||||
|
||||
open class App : Application(), LifecycleObserver {
|
||||
open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
setupExhLogging() // EXH logging
|
||||
Timber.plant(XLogTree()) // SY Redirect Timber to XLog
|
||||
if (!BuildConfig.DEBUG) addAnalytics()
|
||||
|
||||
workaroundAndroid7BrokenSSL()
|
||||
@@ -74,11 +93,16 @@ open class App : Application(), LifecycleObserver {
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||
}
|
||||
|
||||
// Avoid potential crashes
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val process = getProcessName()
|
||||
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
||||
}
|
||||
|
||||
Injekt.importModule(AppModule(this))
|
||||
|
||||
setupNotificationChannels()
|
||||
Realm.init(this)
|
||||
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
||||
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
||||
setupDebugOverlay()
|
||||
}
|
||||
@@ -86,6 +110,34 @@ open class App : Application(), LifecycleObserver {
|
||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
|
||||
// Show notification to disable Incognito Mode when it's enabled
|
||||
preferences.incognitoMode().asFlow()
|
||||
.onEach { enabled ->
|
||||
val notificationManager = NotificationManagerCompat.from(this)
|
||||
if (enabled) {
|
||||
disableIncognitoReceiver.register()
|
||||
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
|
||||
setContentTitle(getString(R.string.pref_incognito_mode))
|
||||
setContentText(getString(R.string.notification_incognito_text))
|
||||
setSmallIcon(R.drawable.ic_glasses_black_24dp)
|
||||
setOngoing(true)
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
this@App,
|
||||
0,
|
||||
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
||||
PendingIntent.FLAG_ONE_SHOT
|
||||
)
|
||||
setContentIntent(pendingIntent)
|
||||
}
|
||||
notificationManager.notify(Notifications.ID_INCOGNITO_MODE, notification)
|
||||
} else {
|
||||
disableIncognitoReceiver.unregister()
|
||||
notificationManager.cancel(Notifications.ID_INCOGNITO_MODE)
|
||||
}
|
||||
}
|
||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
@@ -98,6 +150,23 @@ open class App : Application(), LifecycleObserver {
|
||||
LocaleHelper.updateConfiguration(this, newConfig, true)
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this).apply {
|
||||
componentRegistry {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
add(ImageDecoderDecoder(this@App))
|
||||
} else {
|
||||
add(GifDecoder())
|
||||
}
|
||||
add(ByteBufferFetcher())
|
||||
add(MangaCoverFetcher())
|
||||
}
|
||||
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
||||
crossfade(300)
|
||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun workaroundAndroid7BrokenSSL() {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N ||
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
|
||||
@@ -105,23 +174,22 @@ open class App : Application(), LifecycleObserver {
|
||||
try {
|
||||
SSLContext.getInstance("TLSv1.2")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e)
|
||||
xLogE("Could not install Android 7 broken SSL workaround!", e)
|
||||
}
|
||||
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(applicationContext)
|
||||
} catch (e: GooglePlayServicesRepairableException) {
|
||||
XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e)
|
||||
xLogE("Could not install Android 7 broken SSL workaround!", e)
|
||||
} catch (e: GooglePlayServicesNotAvailableException) {
|
||||
XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e)
|
||||
xLogE("Could not install Android 7 broken SSL workaround!", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addAnalytics() {
|
||||
firebaseAnalytics = Firebase.analytics
|
||||
if (syDebugVersion != "0") {
|
||||
firebaseAnalytics.setUserProperty("preview_version", syDebugVersion)
|
||||
Firebase.analytics.setUserProperty("preview_version", syDebugVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,36 +205,13 @@ open class App : Application(), LifecycleObserver {
|
||||
Notifications.createChannels(this)
|
||||
}
|
||||
|
||||
// EXH
|
||||
private fun deleteOldMetadataRealm() {
|
||||
val config = RealmConfiguration.Builder()
|
||||
.name("gallery-metadata.realm")
|
||||
.schemaVersion(3)
|
||||
.deleteRealmIfMigrationNeeded()
|
||||
.build()
|
||||
Realm.deleteRealm(config)
|
||||
|
||||
// Delete old paper db files
|
||||
listOf(
|
||||
File(filesDir, "gallery-ex"),
|
||||
File(filesDir, "gallery-perveden"),
|
||||
File(filesDir, "gallery-nhentai")
|
||||
).forEach {
|
||||
if (it.exists()) {
|
||||
thread {
|
||||
it.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EXH
|
||||
private fun setupExhLogging() {
|
||||
EHLogLevel.init(this)
|
||||
|
||||
val logLevel = when {
|
||||
EHLogLevel.shouldLog(EHLogLevel.EXTRA) -> LogLevel.ALL
|
||||
BuildConfig.DEBUG -> LogLevel.DEBUG
|
||||
EHLogLevel.shouldLog(EHLogLevel.EXTREME) -> LogLevel.ALL
|
||||
EHLogLevel.shouldLog(EHLogLevel.EXTRA) || BuildConfig.DEBUG -> LogLevel.DEBUG
|
||||
else -> LogLevel.WARN
|
||||
}
|
||||
|
||||
@@ -188,9 +233,8 @@ open class App : Application(), LifecycleObserver {
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
printers += EnhancedFilePrinter
|
||||
.Builder(logFolder.absolutePath)
|
||||
.fileNameGenerator(
|
||||
object : DateFileNameGenerator() {
|
||||
.Builder(logFolder.absolutePath) {
|
||||
fileNameGenerator = object : DateFileNameGenerator() {
|
||||
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
||||
return super.generateFileName(
|
||||
logLevel,
|
||||
@@ -198,13 +242,12 @@ open class App : Application(), LifecycleObserver {
|
||||
) + "-${BuildConfig.BUILD_TYPE}.log"
|
||||
}
|
||||
}
|
||||
)
|
||||
.flattener { timeMillis, level, tag, message ->
|
||||
"${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
|
||||
flattener { timeMillis, level, tag, message ->
|
||||
"${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
|
||||
}
|
||||
cleanStrategy = FileLastModifiedCleanStrategy(7.days.toLongMilliseconds())
|
||||
backupStrategy = NeverBackupStrategy()
|
||||
}
|
||||
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.toLongMilliseconds()))
|
||||
.backupStrategy(NeverBackupStrategy())
|
||||
.build()
|
||||
|
||||
// Install Crashlytics in prod
|
||||
if (!BuildConfig.DEBUG) {
|
||||
@@ -216,8 +259,8 @@ open class App : Application(), LifecycleObserver {
|
||||
*printers.toTypedArray()
|
||||
)
|
||||
|
||||
XLog.tag("Init").d("Application booting...")
|
||||
XLog.tag("Init").disableStackTrace().d(
|
||||
xLogD("Application booting...")
|
||||
xLogD(
|
||||
"App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" +
|
||||
"Preview build: $syDebugVersion\n" +
|
||||
"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" +
|
||||
@@ -242,7 +285,33 @@ open class App : Application(), LifecycleObserver {
|
||||
.install()
|
||||
} catch (e: IllegalStateException) {
|
||||
// Crashes if app is in background
|
||||
XLog.tag("Init").e("Failed to initialize debug overlay, app in background?", e)
|
||||
xLogE("Failed to initialize debug overlay, app in background?", e)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DisableIncognitoReceiver : BroadcastReceiver() {
|
||||
private var registered = false
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
preferences.incognitoMode().set(false)
|
||||
}
|
||||
|
||||
fun register() {
|
||||
if (!registered) {
|
||||
registerReceiver(this, IntentFilter(ACTION_DISABLE_INCOGNITO_MODE))
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister() {
|
||||
if (registered) {
|
||||
unregisterReceiver(this)
|
||||
registered = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
@@ -44,8 +43,6 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
addSingletonFactory { TrackManager(app) }
|
||||
|
||||
addSingletonFactory { Gson() }
|
||||
|
||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||
|
||||
// SY -->
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import android.os.Build
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||
@@ -9,7 +10,9 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -129,6 +132,61 @@ object Migrations {
|
||||
context.toast(R.string.myanimelist_relogin)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 57) {
|
||||
// Migrate DNS over HTTPS setting
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
||||
if (wasDohEnabled) {
|
||||
prefs.edit {
|
||||
putInt(PreferenceKeys.dohProvider, PREF_DOH_CLOUDFLARE)
|
||||
remove("enable_doh")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 59) {
|
||||
// Reset rotation to Free after replacing Lock
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
if (prefs.contains("pref_rotation_type_key")) {
|
||||
prefs.edit {
|
||||
putInt("pref_rotation_type_key", 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Disable update check for Android 5.x users
|
||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||
UpdaterJob.cancelTask(context)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 60) {
|
||||
// Migrate Rotation and Viewer values to default values for viewer_flags
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
||||
1 -> OrientationType.FREE.flagValue
|
||||
2 -> OrientationType.PORTRAIT.flagValue
|
||||
3 -> OrientationType.LANDSCAPE.flagValue
|
||||
4 -> OrientationType.LOCKED_PORTRAIT.flagValue
|
||||
5 -> OrientationType.LOCKED_LANDSCAPE.flagValue
|
||||
else -> OrientationType.FREE.flagValue
|
||||
}
|
||||
|
||||
// Reading mode flag and prefValue is the same value
|
||||
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
|
||||
|
||||
prefs.edit {
|
||||
putInt("pref_default_orientation_type_key", newOrientation)
|
||||
remove("pref_rotation_type_key")
|
||||
putInt("pref_default_reading_mode_key", newReadingMode)
|
||||
remove("pref_default_viewer_key")
|
||||
}
|
||||
}
|
||||
if (oldVersion < 61) {
|
||||
// Handle removed every 1 or 2 hour library updates
|
||||
val updateInterval = preferences.libraryUpdateInterval().get()
|
||||
if (updateInterval == 1 || updateInterval == 2) {
|
||||
preferences.libraryUpdateInterval().set(3)
|
||||
LibraryUpdateJob.setupTask(context, 3)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
@@ -23,6 +24,10 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
||||
internal val trackManager: TrackManager by injectLazy()
|
||||
protected val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
// SY -->
|
||||
protected val customMangaManager: CustomMangaManager by injectLazy()
|
||||
// SY <--
|
||||
|
||||
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||
@@ -24,6 +25,10 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
|
||||
protected val db: DatabaseHelper by injectLazy()
|
||||
protected val trackManager: TrackManager by injectLazy()
|
||||
|
||||
// SY -->
|
||||
protected val customMangaManager: CustomMangaManager by injectLazy()
|
||||
// SY <--
|
||||
|
||||
var job: Job? = null
|
||||
|
||||
protected lateinit var backupManager: T
|
||||
|
||||
@@ -8,7 +8,6 @@ object BackupConst {
|
||||
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE"
|
||||
const val EXTRA_TYPE = "$ID.$NAME.EXTRA_TYPE"
|
||||
|
||||
const val BACKUP_TYPE_LEGACY = 0
|
||||
const val BACKUP_TYPE_FULL = 1
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
@@ -30,7 +29,14 @@ class BackupCreateService : Service() {
|
||||
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||
internal const val BACKUP_TRACK = 0x8
|
||||
internal const val BACKUP_TRACK_MASK = 0x8
|
||||
internal const val BACKUP_ALL = 0xF
|
||||
|
||||
// SY -->
|
||||
internal const val BACKUP_CUSTOM_INFO = 0x10
|
||||
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
|
||||
internal const val BACKUP_READ_MANGA = 0x20
|
||||
internal const val BACKUP_READ_MANGA_MASK = 0x20
|
||||
internal const val BACKUP_ALL = 0x3F
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Returns the status of the service.
|
||||
@@ -48,12 +54,11 @@ class BackupCreateService : Service() {
|
||||
* @param uri path of Uri
|
||||
* @param flags determines what to backup
|
||||
*/
|
||||
fun start(context: Context, uri: Uri, flags: Int, type: Int) {
|
||||
fun start(context: Context, uri: Uri, flags: Int) {
|
||||
if (!isRunning(context)) {
|
||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
||||
putExtra(BackupConst.EXTRA_TYPE, type)
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
@@ -101,17 +106,11 @@ class BackupCreateService : Service() {
|
||||
if (intent == null) return START_NOT_STICKY
|
||||
|
||||
try {
|
||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)!!
|
||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||
val backupType = intent.getIntExtra(BackupConst.EXTRA_TYPE, BackupConst.BACKUP_TYPE_LEGACY)
|
||||
val backupManager = when (backupType) {
|
||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupManager(this)
|
||||
else -> LegacyBackupManager(this)
|
||||
}
|
||||
|
||||
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
||||
val backupFileUri = FullBackupManager(this).createBackup(uri, backupFlags, false)?.toUri()
|
||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||
notifier.showBackupComplete(unifile, backupType == BackupConst.BACKUP_TYPE_LEGACY)
|
||||
notifier.showBackupComplete(unifile)
|
||||
} catch (e: Exception) {
|
||||
notifier.showBackupError(e.message)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -23,9 +22,6 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
||||
val flags = BackupCreateService.BACKUP_ALL
|
||||
return try {
|
||||
FullBackupManager(context).createBackup(uri, flags, true)
|
||||
if (preferences.createLegacyBackup().get()) {
|
||||
LegacyBackupManager(context).createBackup(uri, flags, true)
|
||||
}
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Result.failure()
|
||||
|
||||
@@ -24,6 +24,7 @@ class BackupNotifier(private val context: Context) {
|
||||
setSmallIcon(R.drawable.ic_tachi)
|
||||
setAutoCancel(false)
|
||||
setOngoing(true)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
|
||||
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
|
||||
@@ -41,7 +42,6 @@ class BackupNotifier(private val context: Context) {
|
||||
setContentTitle(context.getString(R.string.creating_backup))
|
||||
|
||||
setProgress(0, 0, true)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
|
||||
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
||||
@@ -60,7 +60,7 @@ class BackupNotifier(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun showBackupComplete(unifile: UniFile, isLegacyFormat: Boolean) {
|
||||
fun showBackupComplete(unifile: UniFile) {
|
||||
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||
|
||||
with(completeNotificationBuilder) {
|
||||
@@ -73,7 +73,7 @@ class BackupNotifier(private val context: Context) {
|
||||
addAction(
|
||||
R.drawable.ic_share_24dp,
|
||||
context.getString(R.string.action_share),
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, isLegacyFormat, Notifications.ID_BACKUP_COMPLETE)
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
||||
)
|
||||
|
||||
show(Notifications.ID_BACKUP_COMPLETE)
|
||||
@@ -141,7 +141,7 @@ class BackupNotifier(private val context: Context) {
|
||||
|
||||
addAction(
|
||||
R.drawable.ic_folder_24dp,
|
||||
context.getString(R.string.action_open_log),
|
||||
context.getString(R.string.action_show_errors),
|
||||
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,12 +43,11 @@ class BackupRestoreService : Service() {
|
||||
* @param context context of application
|
||||
* @param uri path of Uri
|
||||
*/
|
||||
fun start(context: Context, uri: Uri, mode: Int, online: Boolean?) {
|
||||
fun start(context: Context, uri: Uri, mode: Int) {
|
||||
if (!isRunning(context)) {
|
||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
putExtra(BackupConst.EXTRA_MODE, mode)
|
||||
online?.let { putExtra(BackupConst.EXTRA_TYPE, it) }
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
@@ -119,13 +118,12 @@ class BackupRestoreService : Service() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
|
||||
val online = intent.getBooleanExtra(BackupConst.EXTRA_TYPE, true)
|
||||
|
||||
// Cancel any previous job if needed.
|
||||
backupRestore?.job?.cancel()
|
||||
|
||||
backupRestore = when (mode) {
|
||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
|
||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier)
|
||||
else -> LegacyBackupRestore(this, notifier)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,12 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATE
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_READ_MANGA
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_READ_MANGA_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
||||
@@ -29,11 +33,8 @@ import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
||||
import exh.savedsearches.JsonSavedSearch
|
||||
@@ -65,7 +66,11 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
var backup: Backup? = null
|
||||
|
||||
databaseHelper.inTransaction {
|
||||
val databaseManga = getFavoriteManga() /* SY --> */ + getReadManga() + getMergedManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY <-- */
|
||||
val databaseManga = getFavoriteManga() /* SY --> */ + if (flags and BACKUP_READ_MANGA_MASK == BACKUP_READ_MANGA) {
|
||||
getReadManga()
|
||||
} else {
|
||||
emptyList()
|
||||
} + getMergedManga() /* SY <-- */
|
||||
|
||||
backup = Backup(
|
||||
backupManga(databaseManga, flags),
|
||||
@@ -164,7 +169,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
*/
|
||||
private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
|
||||
// Entry for this manga
|
||||
val mangaObject = BackupManga.copyFrom(manga)
|
||||
val mangaObject = BackupManga.copyFrom(manga /* SY --> */, if (options and BACKUP_CUSTOM_INFO_MASK == BACKUP_CUSTOM_INFO) customMangaManager else null /* SY <-- */)
|
||||
|
||||
// SY -->
|
||||
if (manga.source == MERGED_SOURCE_ID) {
|
||||
@@ -237,24 +242,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
/**
|
||||
* Fetches manga information
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return Updated manga info.
|
||||
*/
|
||||
suspend fun restoreMangaFetch(source: Source?, manga: Manga, online: Boolean): Manga {
|
||||
return if (online && source != null /* SY --> */ && source !is MergedSource /* SY <-- */) {
|
||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
manga.also {
|
||||
it.copyFrom(networkManga.toSManga())
|
||||
it.favorite = manga.favorite
|
||||
it.initialized = true
|
||||
it.id = insertManga(manga)
|
||||
}
|
||||
} else {
|
||||
manga.also {
|
||||
it.initialized = it.description != null
|
||||
it.id = insertManga(it)
|
||||
}
|
||||
fun restoreManga(manga: Manga): Manga {
|
||||
return manga.also {
|
||||
it.initialized = it.description != null
|
||||
it.id = insertManga(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,29 +357,26 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
val trackToUpdate = mutableListOf<Track>()
|
||||
|
||||
tracks.forEach { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged) {
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.sync_id == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
if (track.media_id != dbTrack.media_id) {
|
||||
dbTrack.media_id = track.media_id
|
||||
}
|
||||
if (track.library_id != dbTrack.library_id) {
|
||||
dbTrack.library_id = track.library_id
|
||||
}
|
||||
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||
isInDatabase = true
|
||||
trackToUpdate.add(dbTrack)
|
||||
break
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.sync_id == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
if (track.media_id != dbTrack.media_id) {
|
||||
dbTrack.media_id = track.media_id
|
||||
}
|
||||
if (track.library_id != dbTrack.library_id) {
|
||||
dbTrack.library_id = track.library_id
|
||||
}
|
||||
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||
isInDatabase = true
|
||||
trackToUpdate.add(dbTrack)
|
||||
break
|
||||
}
|
||||
if (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
track.id = null
|
||||
trackToUpdate.add(track)
|
||||
}
|
||||
}
|
||||
if (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
track.id = null
|
||||
trackToUpdate.add(track)
|
||||
}
|
||||
}
|
||||
// Update database
|
||||
@@ -394,47 +385,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the chapters for manga if chapters already in database
|
||||
*
|
||||
* @param manga manga of chapters
|
||||
* @param chapters list containing chapters that get restored
|
||||
* @return boolean answering if chapter fetch is not needed
|
||||
*/
|
||||
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
|
||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
|
||||
// Return if fetch is needed
|
||||
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
|
||||
return false
|
||||
}
|
||||
|
||||
chapters.forEach { chapter ->
|
||||
val dbChapter = dbChapters.find { it.url == chapter.url }
|
||||
if (dbChapter != null) {
|
||||
chapter.id = dbChapter.id
|
||||
chapter.copyFrom(dbChapter)
|
||||
if (dbChapter.read && !chapter.read) {
|
||||
chapter.read = dbChapter.read
|
||||
chapter.last_page_read = dbChapter.last_page_read
|
||||
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) {
|
||||
chapter.last_page_read = dbChapter.last_page_read
|
||||
}
|
||||
if (!chapter.bookmark && dbChapter.bookmark) {
|
||||
chapter.bookmark = dbChapter.bookmark
|
||||
}
|
||||
}
|
||||
|
||||
chapter.manga_id = manga.id
|
||||
}
|
||||
|
||||
// Filter the chapters that couldn't be found.
|
||||
updateChapters(chapters.filter { it.id != null })
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun restoreChaptersForMangaOffline(manga: Manga, chapters: List<Chapter>) {
|
||||
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
|
||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
|
||||
chapters.forEach { chapter ->
|
||||
@@ -527,8 +478,9 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
|
||||
manga.id?.let { mangaId ->
|
||||
internal fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
|
||||
val mangaId = manga.id ?: return
|
||||
launchIO {
|
||||
databaseHelper.getFlatMetadataForManga(mangaId).executeOnIO().let {
|
||||
if (it == null) {
|
||||
val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId)
|
||||
|
||||
@@ -15,8 +15,7 @@ import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import exh.EXHMigrations
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import okio.buffer
|
||||
@@ -24,7 +23,7 @@ import okio.gzip
|
||||
import okio.source
|
||||
import java.util.Date
|
||||
|
||||
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
||||
class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
||||
|
||||
override suspend fun performRestore(uri: Uri): Boolean {
|
||||
// SY -->
|
||||
@@ -57,9 +56,11 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
return false
|
||||
}
|
||||
|
||||
restoreManga(it, backup.backupCategories, online)
|
||||
restoreManga(it, backup.backupCategories)
|
||||
}
|
||||
|
||||
// TODO: optionally trigger online library + tracker update
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -81,8 +82,8 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
|
||||
var manga = backupManga.getMangaImpl()
|
||||
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
|
||||
val manga = backupManga.getMangaImpl()
|
||||
val chapters = backupManga.getChaptersImpl()
|
||||
val categories = backupManga.categories
|
||||
val history = backupManga.history
|
||||
@@ -90,22 +91,17 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
// SY -->
|
||||
val mergedMangaReferences = backupManga.mergedMangaReferences
|
||||
val flatMetadata = backupManga.flatMetadata
|
||||
val customManga = backupManga.getCustomMangaInfo()
|
||||
// SY <--
|
||||
|
||||
// SY -->
|
||||
manga = EXHMigrations.migrateBackupEntry(manga)
|
||||
EXHMigrations.migrateBackupEntry(manga)
|
||||
// SY <--
|
||||
|
||||
val source = backupManager.sourceManager.get(manga.source)
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
|
||||
try {
|
||||
if (source != null || !online) {
|
||||
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online)
|
||||
} else {
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
|
||||
}
|
||||
restoreMangaData(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
} catch (e: Exception) {
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||
}
|
||||
|
||||
@@ -117,35 +113,35 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
* Returns a manga restore observable
|
||||
*
|
||||
* @param manga manga data from json
|
||||
* @param source source to get manga data from
|
||||
* @param chapters chapters data from json
|
||||
* @param categories categories data from json
|
||||
* @param history history data from json
|
||||
* @param tracks tracking data from json
|
||||
*/
|
||||
private suspend fun restoreMangaData(
|
||||
private fun restoreMangaData(
|
||||
manga: Manga,
|
||||
source: Source?,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
// SY -->
|
||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||
flatMetadata: BackupFlatMetadata?,
|
||||
online: Boolean
|
||||
customManga: CustomMangaManager.MangaJson?,
|
||||
// SY -->
|
||||
) {
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||
|
||||
db.inTransaction {
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||
if (dbManga == null) {
|
||||
// Manga not in database
|
||||
restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online)
|
||||
} else { // Manga in database
|
||||
restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
} else {
|
||||
// Manga in database
|
||||
// Copy information from manga already in database
|
||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||
// Fetch rest of manga information
|
||||
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online)
|
||||
restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,66 +153,60 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
* @param chapters chapters of manga that needs updating
|
||||
* @param categories categories that need updating
|
||||
*/
|
||||
private suspend fun restoreMangaFetch(
|
||||
source: Source?,
|
||||
private fun restoreMangaFetch(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
// SY -->
|
||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||
flatMetadata: BackupFlatMetadata?,
|
||||
online: Boolean
|
||||
customManga: CustomMangaManager.MangaJson?,
|
||||
// SY <--
|
||||
) {
|
||||
try {
|
||||
val fetchedManga = backupManager.restoreMangaFetch(source, manga, online)
|
||||
val fetchedManga = backupManager.restoreManga(manga)
|
||||
fetchedManga.id ?: return
|
||||
backupManager.restoreChaptersForManga(fetchedManga, chapters)
|
||||
|
||||
if (online && source != null) {
|
||||
// SY -->
|
||||
if (source !is MergedSource) {
|
||||
updateChapters(source, fetchedManga, chapters)
|
||||
}
|
||||
// SY <--
|
||||
} else {
|
||||
backupManager.restoreChaptersForMangaOffline(fetchedManga, chapters)
|
||||
}
|
||||
|
||||
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
|
||||
|
||||
updateTracking(fetchedManga, tracks)
|
||||
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories /* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreMangaNoFetch(
|
||||
source: Source?,
|
||||
private fun restoreMangaNoFetch(
|
||||
backupManga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
// SY -->
|
||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||
flatMetadata: BackupFlatMetadata?,
|
||||
online: Boolean
|
||||
customManga: CustomMangaManager.MangaJson?,
|
||||
// SY <--
|
||||
) {
|
||||
if (online && source != null) {
|
||||
if (/* SY --> */ source !is MergedSource && /* SY <-- */ !backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
||||
updateChapters(source, backupManga, chapters)
|
||||
}
|
||||
} else {
|
||||
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
|
||||
}
|
||||
backupManager.restoreChaptersForManga(backupManga, chapters)
|
||||
|
||||
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
|
||||
|
||||
updateTracking(backupManga, tracks)
|
||||
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
}
|
||||
|
||||
private suspend fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>, mergedMangaReferences: List<BackupMergedMangaReference>, flatMetadata: BackupFlatMetadata?) {
|
||||
private fun restoreExtraForManga(
|
||||
manga: Manga,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
// SY -->
|
||||
mergedMangaReferences: List<BackupMergedMangaReference>,
|
||||
flatMetadata: BackupFlatMetadata?,
|
||||
customManga: CustomMangaManager.MangaJson?,
|
||||
// SY <--
|
||||
) {
|
||||
// Restore categories
|
||||
backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
|
||||
|
||||
@@ -232,6 +222,10 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
|
||||
// Restore flat metadata for metadata sources
|
||||
flatMetadata?.let { backupManager.restoreFlatMetadata(manga, it) }
|
||||
|
||||
// Restore Custom Info
|
||||
customManga?.id = manga.id!!
|
||||
customManga?.let { customMangaManager.saveMangaInfo(it) }
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
|
||||
@@ -25,7 +26,7 @@ data class BackupManga(
|
||||
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x
|
||||
// @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x
|
||||
@ProtoNumber(13) var dateAdded: Long = 0,
|
||||
@ProtoNumber(14) var viewer: Int = 0,
|
||||
@ProtoNumber(14) var viewer: Int = 0, // Replaced by viewer_flags
|
||||
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
|
||||
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
|
||||
@ProtoNumber(17) var categories: List<Int> = emptyList(),
|
||||
@@ -34,9 +35,22 @@ data class BackupManga(
|
||||
@ProtoNumber(100) var favorite: Boolean = true,
|
||||
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
||||
@ProtoNumber(103) var viewer_flags: Int? = null,
|
||||
|
||||
// SY specific values
|
||||
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
||||
@ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null
|
||||
@ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null,
|
||||
@ProtoNumber(602) var customStatus: Int = 0,
|
||||
|
||||
// J2K specific values
|
||||
@ProtoNumber(800) var customTitle: String? = null,
|
||||
@ProtoNumber(801) var customArtist: String? = null,
|
||||
@ProtoNumber(802) var customAuthor: String? = null,
|
||||
@ProtoNumber(803) var customDescription: String? = null,
|
||||
@ProtoNumber(803) var customGenre: List<String>? = null,
|
||||
|
||||
// Neko specific values
|
||||
@ProtoNumber(901) var filtered_scanlators: String? = null,
|
||||
) {
|
||||
fun getMangaImpl(): MangaImpl {
|
||||
return MangaImpl().apply {
|
||||
@@ -51,8 +65,9 @@ data class BackupManga(
|
||||
favorite = this@BackupManga.favorite
|
||||
source = this@BackupManga.source
|
||||
date_added = this@BackupManga.dateAdded
|
||||
viewer = this@BackupManga.viewer
|
||||
viewer_flags = this@BackupManga.viewer_flags ?: this@BackupManga.viewer
|
||||
chapter_flags = this@BackupManga.chapterFlags
|
||||
filtered_scanlators = this@BackupManga.filtered_scanlators
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +77,29 @@ data class BackupManga(
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun getCustomMangaInfo(): CustomMangaManager.MangaJson? {
|
||||
if (customTitle != null ||
|
||||
customArtist != null ||
|
||||
customAuthor != null ||
|
||||
customDescription != null ||
|
||||
customGenre != null ||
|
||||
customStatus != 0
|
||||
) {
|
||||
return CustomMangaManager.MangaJson(
|
||||
id = 0L,
|
||||
title = customTitle,
|
||||
author = customAuthor,
|
||||
artist = customArtist,
|
||||
description = customDescription,
|
||||
genre = customGenre,
|
||||
status = customStatus.takeUnless { it == 0 }
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
// SY <--
|
||||
|
||||
fun getTrackingImpl(): List<TrackImpl> {
|
||||
return tracking.map {
|
||||
it.getTrackingImpl()
|
||||
@@ -69,22 +107,37 @@ data class BackupManga(
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun copyFrom(manga: Manga): BackupManga {
|
||||
fun copyFrom(manga: Manga /* SY --> */, customMangaManager: CustomMangaManager?/* SY <-- */): BackupManga {
|
||||
return BackupManga(
|
||||
url = manga.url,
|
||||
title = manga.title,
|
||||
artist = manga.artist,
|
||||
author = manga.author,
|
||||
description = manga.description,
|
||||
genre = manga.getGenres() ?: emptyList(),
|
||||
status = manga.status,
|
||||
// SY -->
|
||||
title = manga.originalTitle,
|
||||
artist = manga.originalArtist,
|
||||
author = manga.originalAuthor,
|
||||
description = manga.originalDescription,
|
||||
genre = manga.getOriginalGenres() ?: emptyList(),
|
||||
status = manga.originalStatus,
|
||||
// SY <--
|
||||
thumbnailUrl = manga.thumbnail_url,
|
||||
favorite = manga.favorite,
|
||||
source = manga.source,
|
||||
dateAdded = manga.date_added,
|
||||
viewer = manga.viewer,
|
||||
chapterFlags = manga.chapter_flags
|
||||
)
|
||||
viewer = manga.readingModeType,
|
||||
viewer_flags = manga.viewer_flags,
|
||||
chapterFlags = manga.chapter_flags,
|
||||
filtered_scanlators = manga.filtered_scanlators
|
||||
// SY -->
|
||||
).also { backupManga ->
|
||||
customMangaManager?.getManga(manga)?.let {
|
||||
backupManga.customTitle = it.title
|
||||
backupManga.customArtist = it.artist
|
||||
backupManga.customAuthor = it.author
|
||||
backupManga.customDescription = it.description
|
||||
backupManga.customGenre = it.getGenres()
|
||||
backupManga.customStatus = it.status
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,33 +5,13 @@ import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.github.salomonbrys.kotson.registerTypeAdapter
|
||||
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
||||
import com.github.salomonbrys.kotson.set
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CATEGORIES
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.EXTENSIONS
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGA
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MERGEDMANGAREFERENCES
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.SAVEDSEARCHES
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
||||
@@ -49,8 +29,6 @@ import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
@@ -62,8 +40,6 @@ import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.lang.RuntimeException
|
||||
import kotlin.math.max
|
||||
|
||||
@@ -89,180 +65,8 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
* @param uri path of Uri
|
||||
* @param isJob backup called from job
|
||||
*/
|
||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||
// Create root object
|
||||
val root = JsonObject()
|
||||
|
||||
// Create manga array
|
||||
val mangaEntries = JsonArray()
|
||||
|
||||
// Create category array
|
||||
val categoryEntries = JsonArray()
|
||||
|
||||
// Create extension ID/name mapping
|
||||
val extensionEntries = JsonArray()
|
||||
|
||||
// Merged Manga References
|
||||
val mergedMangaReferenceEntries = JsonArray()
|
||||
|
||||
// Add value's to root
|
||||
root[Backup.VERSION] = CURRENT_VERSION
|
||||
root[Backup.MANGAS] = mangaEntries
|
||||
root[CATEGORIES] = categoryEntries
|
||||
root[EXTENSIONS] = extensionEntries
|
||||
// SY -->
|
||||
root[MERGEDMANGAREFERENCES] = mergedMangaReferenceEntries
|
||||
// SY <--
|
||||
|
||||
databaseHelper.inTransaction {
|
||||
val mangas = getFavoriteManga()/* SY --> */.filterNot { it.source == MERGED_SOURCE_ID } + getMergedManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY <-- */
|
||||
|
||||
val extensions: MutableSet<String> = mutableSetOf()
|
||||
|
||||
// Backup library manga and its dependencies
|
||||
mangas.forEach { manga ->
|
||||
mangaEntries.add(backupMangaObject(manga, flags))
|
||||
|
||||
// Maintain set of extensions/sources used (excludes local source)
|
||||
if (manga.source != LocalSource.ID) {
|
||||
sourceManager.get(manga.source)?.let {
|
||||
extensions.add("${manga.source}:${it.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backup categories
|
||||
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
||||
backupCategories(categoryEntries)
|
||||
}
|
||||
|
||||
// Backup extension ID/name mapping
|
||||
backupExtensionInfo(extensionEntries, extensions)
|
||||
// SY -->
|
||||
root[SAVEDSEARCHES] =
|
||||
Injekt.get<PreferencesHelper>().savedSearches().get().joinToString(separator = "***")
|
||||
|
||||
backupMergedMangaReferences(mergedMangaReferenceEntries)
|
||||
// SY <--
|
||||
}
|
||||
|
||||
try {
|
||||
val file: UniFile = (
|
||||
if (isJob) {
|
||||
// Get dir of file and create
|
||||
var dir = UniFile.fromUri(context, uri)
|
||||
dir = dir.createDirectory("automatic")
|
||||
|
||||
// Delete older backups
|
||||
val numberOfBackups = numberOfBackups()
|
||||
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
||||
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||
.orEmpty()
|
||||
.sortedByDescending { it.name }
|
||||
.drop(numberOfBackups - 1)
|
||||
.forEach { it.delete() }
|
||||
|
||||
// Create new file to place backup
|
||||
dir.createFile(Backup.getDefaultFilename())
|
||||
} else {
|
||||
UniFile.fromUri(context, uri)
|
||||
}
|
||||
)
|
||||
?: throw Exception("Couldn't create backup file")
|
||||
|
||||
file.openOutputStream().bufferedWriter().use {
|
||||
parser.toJson(root, it)
|
||||
}
|
||||
return file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
||||
extensions.sorted().forEach {
|
||||
root.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun backupMergedMangaReferences(root: JsonArray) {
|
||||
val mergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
||||
mergedMangaReferences.forEach { root.add(parser.toJsonTree(it)) }
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Backup the categories of library
|
||||
*
|
||||
* @param root root of categories json
|
||||
*/
|
||||
internal fun backupCategories(root: JsonArray) {
|
||||
val categories = databaseHelper.getCategories().executeAsBlocking()
|
||||
categories.forEach { root.add(parser.toJsonTree(it)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a manga to Json
|
||||
*
|
||||
* @param manga manga that gets converted
|
||||
* @return [JsonElement] containing manga information
|
||||
*/
|
||||
internal fun backupMangaObject(manga: Manga, options: Int): JsonElement {
|
||||
// Entry for this manga
|
||||
val entry = JsonObject()
|
||||
|
||||
// Backup manga fields
|
||||
entry[MANGA] = parser.toJsonTree(manga)
|
||||
|
||||
// Check if user wants chapter information in backup
|
||||
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||
// Backup all the chapters
|
||||
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
if (chapters.isNotEmpty()) {
|
||||
val chaptersJson = parser.toJsonTree(chapters)
|
||||
if (chaptersJson.asJsonArray.size() > 0) {
|
||||
entry[CHAPTERS] = chaptersJson
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants category information in backup
|
||||
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||
// Backup categories for this manga
|
||||
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
||||
if (categoriesForManga.isNotEmpty()) {
|
||||
val categoriesNames = categoriesForManga.map { it.name }
|
||||
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants track information in backup
|
||||
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||
if (tracks.isNotEmpty()) {
|
||||
entry[TRACK] = parser.toJsonTree(tracks)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants history information in backup
|
||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||
if (historyForManga.isNotEmpty()) {
|
||||
val historyData = historyForManga.mapNotNull { history ->
|
||||
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||
url?.let { DHistory(url, history.last_read) }
|
||||
}
|
||||
val historyJson = parser.toJsonTree(historyData)
|
||||
if (historyJson.asJsonArray.size() > 0) {
|
||||
entry[HISTORY] = historyJson
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean) =
|
||||
throw IllegalStateException("Legacy backup creation is not supported")
|
||||
|
||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||
manga.id = dbManga.id
|
||||
|
||||
@@ -91,7 +91,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
// SY <--
|
||||
|
||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||
/* SY --> */ var /* SY <-- */ manga = backupManager.parser.fromJson<MangaImpl>(
|
||||
val manga = backupManager.parser.fromJson<MangaImpl>(
|
||||
mangaJson.get(
|
||||
Backup.MANGA
|
||||
)
|
||||
@@ -114,7 +114,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
)
|
||||
|
||||
// EXH -->
|
||||
manga = EXHMigrations.migrateBackupEntry(manga)
|
||||
EXHMigrations.migrateBackupEntry(manga)
|
||||
// <-- EXH
|
||||
|
||||
val source = backupManager.sourceManager.get(manga.source)
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ object MangaTypeAdapter {
|
||||
value(it.originalTitle)
|
||||
// SY <--
|
||||
value(it.source)
|
||||
value(it.viewer)
|
||||
value(it.viewer_flags)
|
||||
value(it.chapter_flags)
|
||||
endArray()
|
||||
}
|
||||
@@ -29,7 +29,7 @@ object MangaTypeAdapter {
|
||||
manga.url = nextString()
|
||||
manga.title = nextString()
|
||||
manga.source = nextLong()
|
||||
manga.viewer = nextInt()
|
||||
manga.viewer_flags = nextInt()
|
||||
manga.chapter_flags = nextInt()
|
||||
endArray()
|
||||
manga
|
||||
|
||||
@@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.cache
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.Formatter
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.Gson
|
||||
import com.jakewharton.disklrucache.DiskLruCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@@ -15,10 +13,12 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Response
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@@ -48,14 +48,12 @@ class ChapterCache(private val context: Context) {
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
/** Google Json class used for parsing JSON files. */
|
||||
private val gson: Gson by injectLazy()
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// --> EH
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
// <-- EH
|
||||
|
||||
/** Cache class used for cache management. */
|
||||
// --> EH
|
||||
private var diskCache = setupDiskCache(prefs.cacheSize().get().toLong())
|
||||
|
||||
init {
|
||||
@@ -73,7 +71,7 @@ class ChapterCache(private val context: Context) {
|
||||
/**
|
||||
* Returns directory of cache.
|
||||
*/
|
||||
val cacheDir: File
|
||||
private val cacheDir: File
|
||||
get() = diskCache.directory
|
||||
|
||||
/**
|
||||
@@ -100,43 +98,19 @@ class ChapterCache(private val context: Context) {
|
||||
}
|
||||
// <-- EH
|
||||
|
||||
/**
|
||||
* Remove file from cache.
|
||||
*
|
||||
* @param file name of file "md5.0".
|
||||
* @return status of deletion for the file.
|
||||
*/
|
||||
fun removeFileFromCache(file: String): Boolean {
|
||||
// Make sure we don't delete the journal file (keeps track of cache).
|
||||
if (file == "journal" || file.startsWith("journal.")) {
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
// Remove the extension from the file to get the key of the cache
|
||||
val key = file.substringBeforeLast(".")
|
||||
// Remove file from cache.
|
||||
diskCache.remove(key)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page list from cache.
|
||||
*
|
||||
* @param chapter the chapter.
|
||||
* @return an observable of the list of pages.
|
||||
* @return the list of pages.
|
||||
*/
|
||||
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
|
||||
return Observable.fromCallable {
|
||||
// Get the key for the chapter.
|
||||
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||
fun getPageListFromCache(chapter: Chapter): List<Page> {
|
||||
// Get the key for the chapter.
|
||||
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||
|
||||
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||
diskCache.get(key).use {
|
||||
gson.fromJson<List<Page>>(it.getString(0))
|
||||
}
|
||||
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||
return diskCache.get(key).use {
|
||||
json.decodeFromString(it.getString(0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +122,7 @@ class ChapterCache(private val context: Context) {
|
||||
*/
|
||||
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
||||
// Convert list of pages to json string.
|
||||
val cachedValue = gson.toJson(pages)
|
||||
val cachedValue = json.encodeToString(pages)
|
||||
|
||||
// Initialize the editor (edits the values for an entry).
|
||||
var editor: DiskLruCache.Editor? = null
|
||||
@@ -228,6 +202,38 @@ class ChapterCache(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun clear(): Int {
|
||||
var deletedFiles = 0
|
||||
cacheDir.listFiles()?.forEach {
|
||||
if (removeFileFromCache(it.name)) {
|
||||
deletedFiles++
|
||||
}
|
||||
}
|
||||
return deletedFiles
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove file from cache.
|
||||
*
|
||||
* @param file name of file "md5.0".
|
||||
* @return status of deletion for the file.
|
||||
*/
|
||||
private fun removeFileFromCache(file: String): Boolean {
|
||||
// Make sure we don't delete the journal file (keeps track of cache).
|
||||
if (file == "journal" || file.startsWith("journal.")) {
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
// Remove the extension from the file to get the key of the cache
|
||||
val key = file.substringBeforeLast(".")
|
||||
// Remove file from cache.
|
||||
diskCache.remove(key)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getKey(chapter: Chapter): String {
|
||||
return "${chapter.manga_id}${chapter.url}"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.cache
|
||||
|
||||
import android.content.Context
|
||||
import coil.imageLoader
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import java.io.File
|
||||
@@ -99,6 +100,13 @@ class CoverCache(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear coil's memory cache.
|
||||
*/
|
||||
fun clearMemoryCache() {
|
||||
context.imageLoader.memoryCache.clear()
|
||||
}
|
||||
|
||||
private fun getCacheDir(dir: String): File {
|
||||
return context.getExternalFilesDir(dir)
|
||||
?: File(context.filesDir, dir).also { it.mkdirs() }
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import coil.bitmap.BitmapPool
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.Options
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.size.Size
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class ByteBufferFetcher : Fetcher<ByteBuffer> {
|
||||
override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
|
||||
return SourceResult(
|
||||
source = ByteArrayInputStream(data.array()).source().buffer(),
|
||||
mimeType = null,
|
||||
dataSource = DataSource.MEMORY
|
||||
)
|
||||
}
|
||||
|
||||
override fun key(data: ByteBuffer): String? = null
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import coil.bitmap.BitmapPool
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.Options
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.network.HttpException
|
||||
import coil.request.get
|
||||
import coil.size.Size
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Call
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
||||
*
|
||||
* Available request parameter:
|
||||
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
||||
*/
|
||||
class MangaCoverFetcher : Fetcher<Manga> {
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val defaultClient = Injekt.get<NetworkHelper>().coilClient
|
||||
|
||||
override fun key(data: Manga): String? {
|
||||
if (data.thumbnail_url.isNullOrBlank()) return null
|
||||
return data.thumbnail_url!!
|
||||
}
|
||||
|
||||
override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
|
||||
// Use custom cover if exists
|
||||
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
|
||||
val customCoverFile = coverCache.getCustomCoverFile(data)
|
||||
if (useCustomCover && customCoverFile.exists()) {
|
||||
return fileLoader(customCoverFile)
|
||||
}
|
||||
|
||||
val cover = data.thumbnail_url
|
||||
return when (getResourceType(cover)) {
|
||||
Type.URL -> httpLoader(data, options)
|
||||
Type.File -> fileLoader(data)
|
||||
null -> error("Invalid image")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
||||
val coverFile = coverCache.getCoverFile(manga) ?: error("No cover specified")
|
||||
|
||||
// Use previously cached cover if exist
|
||||
if (coverFile.exists() && options.diskCachePolicy.readEnabled) {
|
||||
if (!manga.favorite) {
|
||||
coverFile.setLastModified(Date().time)
|
||||
}
|
||||
return fileLoader(coverFile)
|
||||
}
|
||||
|
||||
val (response, body) = awaitGetCall(manga, options)
|
||||
if (!response.isSuccessful) {
|
||||
body.close()
|
||||
throw HttpException(response)
|
||||
}
|
||||
|
||||
// Write to disk for future use
|
||||
if (options.diskCachePolicy.writeEnabled) {
|
||||
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||
val tmpFile = File(coverFile.absolutePath + "_tmp")
|
||||
tmpFile.parentFile?.mkdirs()
|
||||
tmpFile.sink().buffer().use { output ->
|
||||
output.writeAll(input)
|
||||
}
|
||||
if (coverFile.exists()) {
|
||||
coverFile.delete()
|
||||
}
|
||||
tmpFile.renameTo(coverFile)
|
||||
}
|
||||
}
|
||||
|
||||
return SourceResult(
|
||||
source = body.source(),
|
||||
mimeType = "image/*",
|
||||
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
|
||||
val call = getCall(manga, options)
|
||||
val response = call.await()
|
||||
return response to checkNotNull(response.body) { "Null response source" }
|
||||
}
|
||||
|
||||
private fun getCall(manga: Manga, options: Options): Call {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
val client = source?.client ?: defaultClient
|
||||
|
||||
val newClient = client.newBuilder().build()
|
||||
|
||||
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
||||
if (source != null) {
|
||||
it.headers(source.headers)
|
||||
}
|
||||
|
||||
val networkRead = options.networkCachePolicy.readEnabled
|
||||
val diskRead = options.diskCachePolicy.readEnabled
|
||||
when {
|
||||
!networkRead && diskRead -> {
|
||||
it.cacheControl(CacheControl.FORCE_CACHE)
|
||||
}
|
||||
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
||||
it.cacheControl(CacheControl.FORCE_NETWORK)
|
||||
} else {
|
||||
it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
||||
}
|
||||
!networkRead && !diskRead -> {
|
||||
// This causes the request to fail with a 504 Unsatisfiable Request.
|
||||
it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return newClient.newCall(request)
|
||||
}
|
||||
|
||||
private fun fileLoader(manga: Manga): FetchResult {
|
||||
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
|
||||
}
|
||||
|
||||
private fun fileLoader(file: File): FetchResult {
|
||||
return SourceResult(
|
||||
source = file.source().buffer(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
private fun getResourceType(cover: String?): Type? {
|
||||
return when {
|
||||
cover.isNullOrEmpty() -> null
|
||||
cover.startsWith("http") || cover.startsWith("Custom-", true) -> Type.URL
|
||||
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Type {
|
||||
File, URL
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val USE_CUSTOM_COVER = "use_custom_cover"
|
||||
|
||||
private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
|
||||
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,6 @@ import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
|
||||
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
||||
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||
import exh.md.similar.sql.mappers.SimilarTypeMapping
|
||||
import exh.md.similar.sql.models.MangaSimilar
|
||||
import exh.md.similar.sql.queries.SimilarQueries
|
||||
import exh.merged.sql.mappers.MergedMangaTypeMapping
|
||||
import exh.merged.sql.models.MergedMangaReference
|
||||
import exh.merged.sql.queries.MergedQueries
|
||||
@@ -42,7 +39,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||
* This class provides operations to manage the database through its interfaces.
|
||||
*/
|
||||
open class DatabaseHelper(context: Context) :
|
||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, SimilarQueries /* SY <-- */ {
|
||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries /* SY <-- */ {
|
||||
|
||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||
.name(DbOpenCallback.DATABASE_NAME)
|
||||
@@ -62,7 +59,6 @@ open class DatabaseHelper(context: Context) :
|
||||
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
||||
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
||||
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
||||
.addTypeMapping(MangaSimilar::class.java, SimilarTypeMapping())
|
||||
// SY <--
|
||||
.build()
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||
import exh.md.similar.sql.tables.SimilarTable
|
||||
import exh.merged.sql.tables.MergedTable
|
||||
import exh.metadata.sql.tables.SearchMetadataTable
|
||||
import exh.metadata.sql.tables.SearchTagTable
|
||||
@@ -25,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
/**
|
||||
* Version of the database.
|
||||
*/
|
||||
const val DATABASE_VERSION = /* SY --> */ 5 /* SY <-- */
|
||||
const val DATABASE_VERSION = /* SY --> */ 7 /* SY <-- */
|
||||
}
|
||||
|
||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||
@@ -40,7 +39,6 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
execSQL(SearchTagTable.createTableQuery)
|
||||
execSQL(SearchTitleTable.createTableQuery)
|
||||
execSQL(MergedTable.createTableQuery)
|
||||
execSQL(SimilarTable.createTableQuery)
|
||||
// SY <--
|
||||
|
||||
// DB indexes
|
||||
@@ -57,7 +55,6 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||
execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||
execSQL(MergedTable.createIndexQuery)
|
||||
execSQL(SimilarTable.createMangaIdIndexQuery)
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -74,9 +71,15 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
db.execSQL(MergedTable.createTableQuery)
|
||||
db.execSQL(MergedTable.createIndexQuery)
|
||||
}
|
||||
if (oldVersion < 5) {
|
||||
/*if (oldVersion < 5) {
|
||||
db.execSQL(SimilarTable.createTableQuery)
|
||||
db.execSQL(SimilarTable.createMangaIdIndexQuery)
|
||||
}*/
|
||||
if (oldVersion < 6) {
|
||||
db.execSQL(MangaTable.addFilteredScanlators)
|
||||
}
|
||||
if (oldVersion < 7) {
|
||||
db.execSQL("DROP TABLE IF EXISTS manga_related")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFI
|
||||
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_FAVORITE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FILTERED_SCANLATORS
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||
@@ -59,16 +60,17 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
COL_DESCRIPTION to obj.originalDescription,
|
||||
COL_GENRE to obj.originalGenre,
|
||||
COL_TITLE to obj.originalTitle,
|
||||
COL_STATUS to obj.originalStatus,
|
||||
// SY <--
|
||||
COL_STATUS to obj.status,
|
||||
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
||||
COL_FAVORITE to obj.favorite,
|
||||
COL_LAST_UPDATE to obj.last_update,
|
||||
COL_INITIALIZED to obj.initialized,
|
||||
COL_VIEWER to obj.viewer,
|
||||
COL_VIEWER to obj.viewer_flags,
|
||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
||||
COL_DATE_ADDED to obj.date_added
|
||||
COL_DATE_ADDED to obj.date_added,
|
||||
COL_FILTERED_SCANLATORS to obj.filtered_scanlators
|
||||
)
|
||||
}
|
||||
|
||||
@@ -87,10 +89,11 @@ interface BaseMangaGetResolver {
|
||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
||||
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||
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))
|
||||
filtered_scanlators = cursor.getString(cursor.getColumnIndex(COL_FILTERED_SCANLATORS))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
|
||||
interface Manga : SManga {
|
||||
@@ -15,18 +17,20 @@ interface Manga : SManga {
|
||||
|
||||
var date_added: Long
|
||||
|
||||
var viewer: Int
|
||||
var viewer_flags: Int
|
||||
|
||||
var chapter_flags: Int
|
||||
|
||||
var cover_last_modified: Long
|
||||
|
||||
var filtered_scanlators: String?
|
||||
|
||||
fun setChapterOrder(order: Int) {
|
||||
setFlags(order, SORT_MASK)
|
||||
setChapterFlags(order, CHAPTER_SORT_MASK)
|
||||
}
|
||||
|
||||
fun sortDescending(): Boolean {
|
||||
return chapter_flags and SORT_MASK == SORT_DESC
|
||||
return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
|
||||
}
|
||||
|
||||
fun getGenres(): List<String>? {
|
||||
@@ -39,60 +43,72 @@ interface Manga : SManga {
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private fun setFlags(flag: Int, mask: Int) {
|
||||
private fun setChapterFlags(flag: Int, mask: Int) {
|
||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||
}
|
||||
|
||||
private fun setViewerFlags(flag: Int, mask: Int) {
|
||||
viewer_flags = viewer_flags and mask.inv() or (flag and mask)
|
||||
}
|
||||
|
||||
// Used to display the chapter's title one way or another
|
||||
var displayMode: Int
|
||||
get() = chapter_flags and DISPLAY_MASK
|
||||
set(mode) = setFlags(mode, DISPLAY_MASK)
|
||||
get() = chapter_flags and CHAPTER_DISPLAY_MASK
|
||||
set(mode) = setChapterFlags(mode, CHAPTER_DISPLAY_MASK)
|
||||
|
||||
var readFilter: Int
|
||||
get() = chapter_flags and READ_MASK
|
||||
set(filter) = setFlags(filter, READ_MASK)
|
||||
get() = chapter_flags and CHAPTER_READ_MASK
|
||||
set(filter) = setChapterFlags(filter, CHAPTER_READ_MASK)
|
||||
|
||||
var downloadedFilter: Int
|
||||
get() = chapter_flags and DOWNLOADED_MASK
|
||||
set(filter) = setFlags(filter, DOWNLOADED_MASK)
|
||||
get() = chapter_flags and CHAPTER_DOWNLOADED_MASK
|
||||
set(filter) = setChapterFlags(filter, CHAPTER_DOWNLOADED_MASK)
|
||||
|
||||
var bookmarkedFilter: Int
|
||||
get() = chapter_flags and BOOKMARKED_MASK
|
||||
set(filter) = setFlags(filter, BOOKMARKED_MASK)
|
||||
get() = chapter_flags and CHAPTER_BOOKMARKED_MASK
|
||||
set(filter) = setChapterFlags(filter, CHAPTER_BOOKMARKED_MASK)
|
||||
|
||||
var sorting: Int
|
||||
get() = chapter_flags and SORTING_MASK
|
||||
set(sort) = setFlags(sort, SORTING_MASK)
|
||||
get() = chapter_flags and CHAPTER_SORTING_MASK
|
||||
set(sort) = setChapterFlags(sort, CHAPTER_SORTING_MASK)
|
||||
|
||||
var readingModeType: Int
|
||||
get() = viewer_flags and ReadingModeType.MASK
|
||||
set(readingMode) = setViewerFlags(readingMode, ReadingModeType.MASK)
|
||||
|
||||
var orientationType: Int
|
||||
get() = viewer_flags and OrientationType.MASK
|
||||
set(rotationType) = setViewerFlags(rotationType, OrientationType.MASK)
|
||||
|
||||
companion object {
|
||||
|
||||
const val SORT_DESC = 0x00000000
|
||||
const val SORT_ASC = 0x00000001
|
||||
const val SORT_MASK = 0x00000001
|
||||
|
||||
// Generic filter that does not filter anything
|
||||
const val SHOW_ALL = 0x00000000
|
||||
|
||||
const val SHOW_UNREAD = 0x00000002
|
||||
const val SHOW_READ = 0x00000004
|
||||
const val READ_MASK = 0x00000006
|
||||
const val CHAPTER_SORT_DESC = 0x00000000
|
||||
const val CHAPTER_SORT_ASC = 0x00000001
|
||||
const val CHAPTER_SORT_MASK = 0x00000001
|
||||
|
||||
const val SHOW_DOWNLOADED = 0x00000008
|
||||
const val SHOW_NOT_DOWNLOADED = 0x00000010
|
||||
const val DOWNLOADED_MASK = 0x00000018
|
||||
const val CHAPTER_SHOW_UNREAD = 0x00000002
|
||||
const val CHAPTER_SHOW_READ = 0x00000004
|
||||
const val CHAPTER_READ_MASK = 0x00000006
|
||||
|
||||
const val SHOW_BOOKMARKED = 0x00000020
|
||||
const val SHOW_NOT_BOOKMARKED = 0x00000040
|
||||
const val BOOKMARKED_MASK = 0x00000060
|
||||
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008
|
||||
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010
|
||||
const val CHAPTER_DOWNLOADED_MASK = 0x00000018
|
||||
|
||||
const val SORTING_SOURCE = 0x00000000
|
||||
const val SORTING_NUMBER = 0x00000100
|
||||
const val SORTING_UPLOAD_DATE = 0x00000200
|
||||
const val SORTING_MASK = 0x00000300
|
||||
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020
|
||||
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040
|
||||
const val CHAPTER_BOOKMARKED_MASK = 0x00000060
|
||||
|
||||
const val DISPLAY_NAME = 0x00000000
|
||||
const val DISPLAY_NUMBER = 0x00100000
|
||||
const val DISPLAY_MASK = 0x00100000
|
||||
const val CHAPTER_SORTING_SOURCE = 0x00000000
|
||||
const val CHAPTER_SORTING_NUMBER = 0x00000100
|
||||
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200
|
||||
const val CHAPTER_SORTING_MASK = 0x00000300
|
||||
|
||||
const val CHAPTER_DISPLAY_NAME = 0x00000000
|
||||
const val CHAPTER_DISPLAY_NUMBER = 0x00100000
|
||||
const val CHAPTER_DISPLAY_MASK = 0x00100000
|
||||
|
||||
fun create(source: Long): Manga = MangaImpl().apply {
|
||||
this.source = source
|
||||
|
||||
@@ -40,9 +40,11 @@ open class MangaImpl : Manga {
|
||||
override var genre: String?
|
||||
get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre
|
||||
set(value) { ogGenre = value }
|
||||
// SY <--
|
||||
|
||||
override var status: Int = 0
|
||||
override var status: Int
|
||||
get() = if (favorite) customMangaManager.getManga(this)?.status?.takeUnless { it == 0 } ?: ogStatus else ogStatus
|
||||
set(value) { ogStatus = value }
|
||||
// SY <--
|
||||
|
||||
override var thumbnail_url: String? = null
|
||||
|
||||
@@ -54,12 +56,14 @@ open class MangaImpl : Manga {
|
||||
|
||||
override var initialized: Boolean = false
|
||||
|
||||
override var viewer: Int = 0
|
||||
override var viewer_flags: Int = 0
|
||||
|
||||
override var chapter_flags: Int = 0
|
||||
|
||||
override var cover_last_modified: Long = 0
|
||||
|
||||
override var filtered_scanlators: String? = null
|
||||
|
||||
// SY -->
|
||||
lateinit var ogTitle: String
|
||||
private set
|
||||
@@ -71,6 +75,8 @@ open class MangaImpl : Manga {
|
||||
private set
|
||||
var ogGenre: String? = null
|
||||
private set
|
||||
var ogStatus: Int = 0
|
||||
private set
|
||||
// SY <--
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -9,12 +9,13 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
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.MangaFilteredScanlatorsPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
@@ -102,20 +103,35 @@ interface MangaQueries : DbProvider {
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaMigrationPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateMangaThumbnail(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaThumbnailPutResolver())
|
||||
.prepare()
|
||||
// SY <--
|
||||
|
||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||
|
||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
||||
|
||||
fun updateFlags(manga: Manga) = db.put()
|
||||
fun updateChapterFlags(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver())
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
|
||||
.prepare()
|
||||
|
||||
fun updateFlags(mangas: List<Manga>) = db.put()
|
||||
.objects(mangas)
|
||||
.withPutResolver(MangaFlagsPutResolver(true))
|
||||
fun updateChapterFlags(manga: List<Manga>) = db.put()
|
||||
.objects(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags, true))
|
||||
.prepare()
|
||||
|
||||
fun updateViewerFlags(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
||||
.prepare()
|
||||
|
||||
fun updateViewerFlags(manga: List<Manga>) = db.put()
|
||||
.objects(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
|
||||
.prepare()
|
||||
|
||||
fun updateLastUpdated(manga: Manga) = db.put()
|
||||
@@ -128,11 +144,6 @@ interface MangaQueries : DbProvider {
|
||||
.withPutResolver(MangaFavoritePutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateMangaViewer(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaViewerPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateMangaTitle(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaTitlePutResolver())
|
||||
@@ -143,6 +154,13 @@ interface MangaQueries : DbProvider {
|
||||
.withPutResolver(MangaCoverLastModifiedPutResolver())
|
||||
.prepare()
|
||||
|
||||
// SY -->
|
||||
fun updateMangaFilteredScanlators(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaFilteredScanlatorsPutResolver())
|
||||
.prepare()
|
||||
// SY <--
|
||||
|
||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||
|
||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||
@@ -151,12 +169,38 @@ interface MangaQueries : DbProvider {
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE})")
|
||||
.where(
|
||||
"""
|
||||
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
|
||||
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
.whereArgs(0)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
// SY -->
|
||||
fun deleteMangasNotInLibraryAndNotRead() = db.delete()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where(
|
||||
"""
|
||||
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
|
||||
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
|
||||
) AND ${MangaTable.COL_ID} NOT IN (
|
||||
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
.whereArgs(0)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
// SY <--
|
||||
|
||||
fun deleteMangas() = db.delete()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
@@ -195,6 +239,16 @@ interface MangaQueries : DbProvider {
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getChapterFetchDateManga() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getChapterFetchDateMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
// SY -->
|
||||
fun getMangaWithMetadata() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
|
||||
@@ -69,7 +69,7 @@ fun getReadMangaNotInLibraryQuery() =
|
||||
SELECT ${Manga.TABLE}.*
|
||||
FROM ${Manga.TABLE}
|
||||
WHERE ${Manga.COL_FAVORITE} = 0 AND ${Manga.COL_ID} IN(
|
||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} FROM ${Chapter.TABLE} WHERE ${Chapter.COL_READ} = 1
|
||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} FROM ${Chapter.TABLE} WHERE ${Chapter.COL_READ} = 1 OR ${Chapter.COL_LAST_PAGE_READ} != 0
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -221,6 +221,16 @@ fun getLatestChapterMangaQuery() =
|
||||
ORDER by max DESC
|
||||
"""
|
||||
|
||||
fun getChapterFetchDateMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) AS max
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER by max DESC
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the categories for a manga.
|
||||
*/
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import androidx.core.content.contentValuesOf
|
||||
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
|
||||
|
||||
// [EXH]
|
||||
class MangaFilteredScanlatorsPutResolver : 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_FILTERED_SCANLATORS} = ?")
|
||||
.whereArgs(manga.filtered_scanlators)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||
MangaTable.COL_FILTERED_SCANLATORS to manga.filtered_scanlators
|
||||
)
|
||||
}
|
||||
+3
-2
@@ -8,8 +8,9 @@ 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
|
||||
import kotlin.reflect.KProperty1
|
||||
|
||||
class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
||||
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>, private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
@@ -37,6 +38,6 @@ class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolve
|
||||
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags
|
||||
colName to fieldGetter.get(manga)
|
||||
)
|
||||
}
|
||||
|
||||
+14
-9
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
@@ -9,6 +8,7 @@ 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
|
||||
import exh.util.nullIfZero
|
||||
|
||||
class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
||||
|
||||
@@ -31,15 +31,20 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
||||
MangaTable.COL_GENRE to manga.originalGenre,
|
||||
MangaTable.COL_AUTHOR to manga.originalAuthor,
|
||||
MangaTable.COL_ARTIST to manga.originalArtist,
|
||||
MangaTable.COL_DESCRIPTION to manga.originalDescription
|
||||
MangaTable.COL_DESCRIPTION to manga.originalDescription,
|
||||
MangaTable.COL_STATUS to manga.originalStatus
|
||||
)
|
||||
|
||||
fun resetToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
val splitter = "▒ ▒∩▒"
|
||||
put(MangaTable.COL_TITLE, manga.title.split(splitter).last())
|
||||
put(MangaTable.COL_GENRE, manga.genre?.split(splitter)?.lastOrNull())
|
||||
put(MangaTable.COL_AUTHOR, manga.author?.split(splitter)?.lastOrNull())
|
||||
put(MangaTable.COL_ARTIST, manga.artist?.split(splitter)?.lastOrNull())
|
||||
put(MangaTable.COL_DESCRIPTION, manga.description?.split(splitter)?.lastOrNull())
|
||||
private fun resetToContentValues(manga: Manga) = contentValuesOf(
|
||||
MangaTable.COL_TITLE to manga.title.split(splitter).last(),
|
||||
MangaTable.COL_GENRE to manga.genre?.split(splitter)?.lastOrNull(),
|
||||
MangaTable.COL_AUTHOR to manga.author?.split(splitter)?.lastOrNull(),
|
||||
MangaTable.COL_ARTIST to manga.artist?.split(splitter)?.lastOrNull(),
|
||||
MangaTable.COL_DESCRIPTION to manga.description?.split(splitter)?.lastOrNull(),
|
||||
MangaTable.COL_STATUS to manga.status.nullIfZero()?.toString()?.split(splitter)?.lastOrNull()
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val splitter = "▒ ▒∩▒"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -30,6 +30,6 @@ class MangaMigrationPutResolver : PutResolver<Manga>() {
|
||||
MangaTable.COL_DATE_ADDED to manga.date_added,
|
||||
MangaTable.COL_TITLE to manga.title,
|
||||
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags,
|
||||
MangaTable.COL_VIEWER to manga.viewer
|
||||
MangaTable.COL_VIEWER to manga.viewer_flags
|
||||
)
|
||||
}
|
||||
|
||||
+5
-5
@@ -9,7 +9,8 @@ import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
class MangaViewerPutResolver : PutResolver<Manga>() {
|
||||
// SY
|
||||
class MangaThumbnailPutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
@@ -25,8 +26,7 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_VIEWER to manga.viewer
|
||||
)
|
||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||
MangaTable.COL_THUMBNAIL_URL to manga.thumbnail_url
|
||||
)
|
||||
}
|
||||
@@ -40,6 +40,8 @@ object MangaTable {
|
||||
|
||||
// SY ->>
|
||||
const val COL_READ = "read"
|
||||
|
||||
const val COL_FILTERED_SCANLATORS = "filtered_scanlators"
|
||||
// SY <--
|
||||
|
||||
const val COL_CATEGORY = "category"
|
||||
@@ -65,7 +67,8 @@ object MangaTable {
|
||||
$COL_VIEWER INTEGER NOT NULL,
|
||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
|
||||
$COL_DATE_ADDED LONG NOT NULL
|
||||
$COL_DATE_ADDED LONG NOT NULL,
|
||||
$COL_FILTERED_SCANLATORS TEXT
|
||||
)"""
|
||||
|
||||
val createUrlIndexQuery: String
|
||||
@@ -90,4 +93,7 @@ object MangaTable {
|
||||
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
||||
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
||||
"GROUP BY $TABLE.$COL_ID)"
|
||||
|
||||
val addFilteredScanlators: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -23,7 +24,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class DownloadManager(/* SY private */ val context: Context) {
|
||||
class DownloadManager(private val context: Context) {
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
@@ -202,6 +203,15 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
deleteChapters(listOf(download.chapter), download.manga, download.source)
|
||||
}
|
||||
|
||||
fun deletePendingDownloads(vararg downloads: Download) {
|
||||
val downloadsByManga = downloads.groupBy { it.manga.id }
|
||||
downloadsByManga.map { entry ->
|
||||
val manga = entry.value.first().manga
|
||||
val source = entry.value.first().source
|
||||
deleteChapters(entry.value.map { it.chapter }, manga, source)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the directories of a list of downloaded chapters.
|
||||
*
|
||||
@@ -211,16 +221,16 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
*/
|
||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
||||
val filteredChapters = getChaptersToDelete(chapters)
|
||||
launchIO {
|
||||
removeFromDownloadQueue(filteredChapters)
|
||||
|
||||
removeFromDownloadQueue(filteredChapters)
|
||||
|
||||
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||
chapterDirs.forEach { it.delete() }
|
||||
cache.removeChapters(filteredChapters, manga)
|
||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||
chapterDirs.forEach { it.delete() }
|
||||
cache.removeChapters(filteredChapters, manga)
|
||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
return filteredChapters
|
||||
}
|
||||
|
||||
@@ -262,7 +272,7 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
|
||||
if (removeNonFavorite && !manga.favorite) {
|
||||
val mangaFolder = provider.getMangaDir(manga, source)
|
||||
cleaned += 1 + (mangaFolder.listFiles()?.size ?: 0)
|
||||
cleaned += 1 + mangaFolder.listFiles().orEmpty().size
|
||||
mangaFolder.delete()
|
||||
cache.removeManga(manga)
|
||||
return cleaned
|
||||
@@ -283,8 +293,7 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
|
||||
if (cache.getDownloadCount(manga) == 0) {
|
||||
val mangaFolder = provider.getMangaDir(manga, source)
|
||||
val size = mangaFolder.listFiles()?.size ?: 0
|
||||
if (size == 0) {
|
||||
if (!mangaFolder.listFiles().isNullOrEmpty()) {
|
||||
mangaFolder.delete()
|
||||
cache.removeManga(manga)
|
||||
} else {
|
||||
@@ -302,9 +311,11 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
* @param source the source of the manga.
|
||||
*/
|
||||
fun deleteManga(manga: Manga, source: Source) {
|
||||
downloader.queue.remove(manga)
|
||||
provider.findMangaDir(manga, source)?.delete()
|
||||
cache.removeManga(manga)
|
||||
launchIO {
|
||||
downloader.queue.remove(manga)
|
||||
provider.findMangaDir(manga, source)?.delete()
|
||||
cache.removeManga(manga)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,8 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
private val progressNotificationBuilder by lazy {
|
||||
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||
setAutoCancel(false)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,10 +83,8 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
*/
|
||||
fun onProgressChange(download: Download) {
|
||||
with(progressNotificationBuilder) {
|
||||
// Check if first call.
|
||||
if (!isDownloading) {
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
setAutoCancel(false)
|
||||
clearActions()
|
||||
// Open download manager when clicked
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
@@ -114,6 +114,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
}
|
||||
|
||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||
setOngoing(true)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||
}
|
||||
@@ -127,8 +128,8 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
setContentTitle(context.getString(R.string.chapter_paused))
|
||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||
setSmallIcon(R.drawable.ic_pause_24dp)
|
||||
setAutoCancel(false)
|
||||
setProgress(0, 0, false)
|
||||
setOngoing(false)
|
||||
clearActions()
|
||||
// Open download manager when clicked
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
@@ -217,7 +218,6 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
clearActions()
|
||||
setAutoCancel(false)
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ class DownloadProvider(private val context: Context) {
|
||||
return downloadsDir
|
||||
.createDirectory(getSourceDirName(source))
|
||||
.createDirectory(getMangaDirName(manga))
|
||||
} catch (e: NullPointerException) {
|
||||
Timber.w(e)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Invalid download directory")
|
||||
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ class DownloadProvider(private val context: Context) {
|
||||
* @param source the source to query.
|
||||
*/
|
||||
fun findSourceDir(source: Source): UniFile? {
|
||||
return downloadsDir.findFile(getSourceDirName(source))
|
||||
return downloadsDir.findFile(getSourceDirName(source), true)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,7 +76,7 @@ class DownloadProvider(private val context: Context) {
|
||||
*/
|
||||
fun findMangaDir(manga: Manga, source: Source): UniFile? {
|
||||
val sourceDir = findSourceDir(source)
|
||||
return sourceDir?.findFile(getMangaDirName(manga))
|
||||
return sourceDir?.findFile(getMangaDirName(manga), true)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,7 +89,7 @@ class DownloadProvider(private val context: Context) {
|
||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||
val mangaDir = findMangaDir(manga, source)
|
||||
return getValidChapterDirNames(chapter).asSequence()
|
||||
.mapNotNull { mangaDir?.findFile(it) ?: mangaDir?.findFile("$it.cbz") }
|
||||
.mapNotNull { mangaDir?.findFile(it, true) ?: mangaDir?.findFile("$it.cbz", true) }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
@@ -123,14 +123,12 @@ class DownloadProvider(private val context: Context) {
|
||||
source: Source
|
||||
): List<UniFile> {
|
||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||
return mangaDir.listFiles()!!.asList().filter {
|
||||
(
|
||||
chapters.find { chp ->
|
||||
getValidChapterDirNames(chp).any { dir ->
|
||||
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
|
||||
}
|
||||
} == null
|
||||
) || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
||||
return mangaDir.listFiles().orEmpty().asList().filter {
|
||||
chapters.find { chp ->
|
||||
getValidChapterDirNames(chp).any { dir ->
|
||||
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
|
||||
}
|
||||
} == null || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
@@ -141,7 +139,7 @@ class DownloadProvider(private val context: Context) {
|
||||
* @param source the source to query.
|
||||
*/
|
||||
fun getSourceDirName(source: Source): String {
|
||||
return source.toString()
|
||||
return DiskUtil.buildValidFilename(source.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,6 +176,7 @@ class DownloadProvider(private val context: Context) {
|
||||
return listOf(
|
||||
getChapterDirName(chapter),
|
||||
|
||||
// TODO: remove this
|
||||
// Legacy chapter directory name used in v0.9.2 and before
|
||||
DiskUtil.buildValidFilename(chapter.name)
|
||||
)
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.ContentValues.TAG
|
||||
import android.util.Log
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
|
||||
|
||||
private var data: InputStream? = null
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
loadFromFile(callback)
|
||||
}
|
||||
|
||||
private fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
loadFromFile(File(filePath), callback)
|
||||
}
|
||||
|
||||
protected fun loadFromFile(file: File, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
try {
|
||||
data = FileInputStream(file)
|
||||
} catch (e: FileNotFoundException) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Timber.d(e, "Failed to open file")
|
||||
}
|
||||
callback.onLoadFailed(e)
|
||||
return
|
||||
}
|
||||
|
||||
callback.onDataReady(data)
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
try {
|
||||
data?.close()
|
||||
} catch (e: IOException) {
|
||||
// Ignored.
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun getDataClass(): Class<InputStream> {
|
||||
return InputStream::class.java
|
||||
}
|
||||
|
||||
override fun getDataSource(): DataSource {
|
||||
return DataSource.LOCAL
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import eu.kanade.tachiyomi.data.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() }
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* A [DataFetcher] for loading a cover of a library manga.
|
||||
* It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network
|
||||
* and copies the result to the cache.
|
||||
*
|
||||
* @param networkFetcher the network fetcher for this cover.
|
||||
* @param manga the manga of the cover to load.
|
||||
* @param file the file where this cover should be. It may exists or not.
|
||||
*/
|
||||
class LibraryMangaUrlFetcher(
|
||||
private val networkFetcher: DataFetcher<InputStream>,
|
||||
private val manga: Manga,
|
||||
private val coverCache: CoverCache
|
||||
) : LibraryMangaCustomCoverFetcher(manga, coverCache) {
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
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(
|
||||
priority,
|
||||
object : DataFetcher.DataCallback<InputStream> {
|
||||
override fun onDataReady(data: InputStream?) {
|
||||
if (data != null) {
|
||||
val tmpFile = File(cover.path + ".tmp")
|
||||
try {
|
||||
// Retrieve destination stream, create parent folders if needed.
|
||||
val output = try {
|
||||
tmpFile.outputStream()
|
||||
} catch (e: FileNotFoundException) {
|
||||
tmpFile.parentFile!!.mkdirs()
|
||||
tmpFile.outputStream()
|
||||
}
|
||||
|
||||
// Copy the file and rename to the original.
|
||||
data.use { output.use { data.copyTo(output) } }
|
||||
tmpFile.renameTo(cover)
|
||||
loadFromFile(cover, callback)
|
||||
} catch (e: Exception) {
|
||||
tmpFile.delete()
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
} else {
|
||||
callback.onLoadFailed(Exception("Null data"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadFailed(e: Exception) {
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
loadFromFile(cover, callback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
super.cleanup()
|
||||
networkFetcher.cleanup()
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
super.cancel()
|
||||
networkFetcher.cancel()
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.load.Key
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.security.MessageDigest
|
||||
|
||||
data class MangaThumbnail(val manga: Manga, val coverLastModified: Long) : Key {
|
||||
val key = manga.url + coverLastModified
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(key.toByteArray(Key.CHARSET))
|
||||
}
|
||||
}
|
||||
|
||||
fun Manga.toMangaThumbnail() = MangaThumbnail(this, cover_last_modified)
|
||||
@@ -1,134 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.Headers
|
||||
import com.bumptech.glide.load.model.LazyHeaders
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
||||
* Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
|
||||
*
|
||||
* - Check in RAM LRU.
|
||||
* - Check in disk LRU.
|
||||
* - Check in this module.
|
||||
* - Fetch from the network connection.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
||||
|
||||
/**
|
||||
* Cover cache where persistent covers are stored.
|
||||
*/
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Default network client.
|
||||
*/
|
||||
private val defaultClient = Injekt.get<NetworkHelper>().client
|
||||
|
||||
/**
|
||||
* Map where request headers are stored for a source.
|
||||
*/
|
||||
private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
|
||||
|
||||
/**
|
||||
* Factory class for creating [MangaThumbnailModelLoader] instances.
|
||||
*/
|
||||
class Factory : ModelLoaderFactory<MangaThumbnail, InputStream> {
|
||||
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MangaThumbnail, InputStream> {
|
||||
return MangaThumbnailModelLoader()
|
||||
}
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
|
||||
override fun handles(model: MangaThumbnail): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fetcher for the given manga or null if the url is empty.
|
||||
*
|
||||
* @param mangaThumbnail the model.
|
||||
* @param width the width of the view where the resource will be loaded.
|
||||
* @param height the height of the view where the resource will be loaded.
|
||||
*/
|
||||
override fun buildLoadData(
|
||||
mangaThumbnail: MangaThumbnail,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
): ModelLoader.LoadData<InputStream>? {
|
||||
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)) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
||||
|
||||
// Get the resource fetcher for this request url.
|
||||
val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
|
||||
|
||||
if (!manga.favorite) {
|
||||
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
||||
}
|
||||
|
||||
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, coverCache)
|
||||
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return ModelLoader.LoadData(mangaThumbnail, libraryFetcher)
|
||||
} else {
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return ModelLoader.LoadData(mangaThumbnail, FileFetcher(url.removePrefix("file://")))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request headers for a source copying its OkHttp headers and caching them.
|
||||
*
|
||||
* @param manga the model.
|
||||
*/
|
||||
private fun getHeaders(manga: Manga, source: HttpSource?): Headers {
|
||||
if (source == null) return LazyHeaders.DEFAULT
|
||||
|
||||
return cachedHeaders.getOrPut(manga.source) {
|
||||
LazyHeaders.Builder().apply {
|
||||
val nullStr: String? = null
|
||||
setHeader("User-Agent", nullStr)
|
||||
for ((key, value) in source.headers.toMultimap()) {
|
||||
addHeader(key, value[0])
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import com.bumptech.glide.signature.ObjectKey
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
||||
|
||||
override fun buildLoadData(
|
||||
model: InputStream,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
): ModelLoader.LoadData<InputStream>? {
|
||||
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
|
||||
}
|
||||
|
||||
override fun handles(model: InputStream): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
class Fetcher(private val stream: InputStream) : DataFetcher<InputStream> {
|
||||
|
||||
override fun getDataClass(): Class<InputStream> {
|
||||
return InputStream::class.java
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
try {
|
||||
stream.close()
|
||||
} catch (e: IOException) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDataSource(): DataSource {
|
||||
return DataSource.LOCAL
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
override fun loadData(
|
||||
priority: Priority,
|
||||
callback: DataFetcher.DataCallback<in InputStream>
|
||||
) {
|
||||
callback.onDataReady(stream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory class for creating [PassthroughModelLoader] instances.
|
||||
*/
|
||||
class Factory : ModelLoaderFactory<InputStream, InputStream> {
|
||||
|
||||
override fun build(
|
||||
multiFactory: MultiModelLoaderFactory
|
||||
): ModelLoader<InputStream, InputStream> {
|
||||
return PassthroughModelLoader()
|
||||
}
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Class used to update Glide module settings
|
||||
*/
|
||||
@GlideModule
|
||||
class TachiGlideModule : AppGlideModule() {
|
||||
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
|
||||
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
|
||||
builder.setDefaultTransitionOptions(
|
||||
Drawable::class.java,
|
||||
DrawableTransitionOptions.withCrossFade()
|
||||
)
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
||||
|
||||
registry.replace(
|
||||
GlideUrl::class.java,
|
||||
InputStream::class.java,
|
||||
networkFactory
|
||||
)
|
||||
registry.append(
|
||||
MangaThumbnail::class.java,
|
||||
InputStream::class.java,
|
||||
MangaThumbnailModelLoader.Factory()
|
||||
)
|
||||
registry.append(
|
||||
InputStream::class.java,
|
||||
InputStream::class.java,
|
||||
PassthroughModelLoader.Factory()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.util.Scanner
|
||||
|
||||
class CustomMangaManager(val context: Context) {
|
||||
|
||||
@@ -23,7 +22,7 @@ class CustomMangaManager(val context: Context) {
|
||||
|
||||
val json = try {
|
||||
Json.decodeFromString<MangaList>(
|
||||
Scanner(editJson).useDelimiter("\\Z").next()
|
||||
editJson.bufferedReader().use { it.readText() }
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
@@ -32,30 +31,15 @@ class CustomMangaManager(val context: Context) {
|
||||
val mangasJson = json.mangas ?: return mutableMapOf()
|
||||
return mangasJson.mapNotNull { mangaJson ->
|
||||
val id = mangaJson.id ?: return@mapNotNull null
|
||||
val manga = MangaImpl().apply {
|
||||
this.id = id
|
||||
title = mangaJson.title ?: ""
|
||||
author = mangaJson.author
|
||||
artist = mangaJson.artist
|
||||
description = mangaJson.description
|
||||
genre = mangaJson.genre?.joinToString(", ")
|
||||
}
|
||||
id to manga
|
||||
id to mangaJson.toManga()
|
||||
}.toMap().toMutableMap()
|
||||
}
|
||||
|
||||
fun saveMangaInfo(manga: MangaJson) {
|
||||
if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null) {
|
||||
if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null && manga.status == null) {
|
||||
customMangaMap.remove(manga.id!!)
|
||||
} else {
|
||||
customMangaMap[manga.id!!] = MangaImpl().apply {
|
||||
id = manga.id
|
||||
title = manga.title ?: ""
|
||||
author = manga.author
|
||||
artist = manga.artist
|
||||
description = manga.description
|
||||
genre = manga.genre?.joinToString(", ")
|
||||
}
|
||||
customMangaMap[manga.id!!] = manga.toManga()
|
||||
}
|
||||
saveCustomInfo()
|
||||
}
|
||||
@@ -75,7 +59,8 @@ class CustomMangaManager(val context: Context) {
|
||||
author,
|
||||
artist,
|
||||
description,
|
||||
genre?.split(", ")
|
||||
genre?.split(", "),
|
||||
status
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,24 +71,23 @@ class CustomMangaManager(val context: Context) {
|
||||
|
||||
@Serializable
|
||||
data class MangaJson(
|
||||
val id: Long? = null,
|
||||
var id: Long? = null,
|
||||
val title: String? = null,
|
||||
val author: String? = null,
|
||||
val artist: String? = null,
|
||||
val description: String? = null,
|
||||
val genre: List<String>? = null
|
||||
val genre: List<String>? = null,
|
||||
val status: Int? = null
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as MangaJson
|
||||
if (id != other.id) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
fun toManga() = MangaImpl().apply {
|
||||
id = this@MangaJson.id
|
||||
title = this@MangaJson.title ?: ""
|
||||
author = this@MangaJson.author
|
||||
artist = this@MangaJson.artist
|
||||
description = this@MangaJson.description
|
||||
genre = this@MangaJson.genre?.joinToString(", ")
|
||||
status = this@MangaJson.status ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.data.preference.CHARGING
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.UNMETERED_NETWORK
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -31,9 +33,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
||||
if (interval > 0) {
|
||||
val restrictions = preferences.libraryUpdateRestriction()!!
|
||||
val acRestriction = "ac" in restrictions
|
||||
val wifiRestriction = if ("wifi" in restrictions) {
|
||||
val restrictions = preferences.libraryUpdateRestriction().get()
|
||||
val acRestriction = CHARGING in restrictions
|
||||
val wifiRestriction = if (UNMETERED_NETWORK in restrictions) {
|
||||
NetworkType.UNMETERED
|
||||
} else {
|
||||
NetworkType.CONNECTED
|
||||
|
||||
@@ -6,20 +6,23 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.bumptech.glide.Glide
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
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.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.lang.chop
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
@@ -76,7 +79,8 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
context.notificationManager.notify(
|
||||
Notifications.ID_LIBRARY_PROGRESS,
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(title)
|
||||
.setContentTitle(title.chop(40))
|
||||
.setContentText("($current/$total)")
|
||||
.setProgress(total, current, false)
|
||||
.build()
|
||||
)
|
||||
@@ -111,7 +115,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
setContentIntent(errorLogIntent)
|
||||
addAction(
|
||||
R.drawable.ic_folder_24dp,
|
||||
context.getString(R.string.action_open_log),
|
||||
context.getString(R.string.action_show_errors),
|
||||
errorLogIntent
|
||||
)
|
||||
}
|
||||
@@ -166,14 +170,17 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
|
||||
// Per-manga notification
|
||||
if (!preferences.hideNotificationContent()) {
|
||||
updates.forEach { (manga, chapters) ->
|
||||
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
|
||||
launchUI {
|
||||
updates.forEach { (manga, chapters) ->
|
||||
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
|
||||
private suspend fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
|
||||
val icon = getMangaIcon(manga)
|
||||
return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||
setContentTitle(manga.title)
|
||||
|
||||
@@ -183,7 +190,6 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
|
||||
setSmallIcon(R.drawable.ic_tachi)
|
||||
|
||||
val icon = getMangaIcon(manga)
|
||||
if (icon != null) {
|
||||
setLargeIcon(icon)
|
||||
}
|
||||
@@ -227,23 +233,14 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
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 suspend fun getMangaIcon(manga: Manga): Bitmap? {
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(manga)
|
||||
.transformations(CircleCropTransformation())
|
||||
.size(NOTIF_ICON_SIZE)
|
||||
.build()
|
||||
val drawable = context.imageLoader.execute(request).drawable
|
||||
return (drawable as? BitmapDrawable)?.bitmap
|
||||
}
|
||||
|
||||
private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
|
||||
|
||||
@@ -22,16 +22,18 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryGroup
|
||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||
@@ -41,10 +43,11 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
||||
import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import exh.source.getMainSource
|
||||
import exh.source.isMdBasedSource
|
||||
import exh.source.mangaDexSourceIds
|
||||
import exh.util.executeOnIO
|
||||
import exh.util.nullIfBlank
|
||||
@@ -88,6 +91,7 @@ class LibraryUpdateService(
|
||||
private lateinit var notifier: LibraryUpdateNotifier
|
||||
private lateinit var ioScope: CoroutineScope
|
||||
|
||||
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
|
||||
private var updateJob: Job? = null
|
||||
|
||||
/**
|
||||
@@ -109,6 +113,8 @@ class LibraryUpdateService(
|
||||
|
||||
companion object {
|
||||
|
||||
private var instance: LibraryUpdateService? = null
|
||||
|
||||
/**
|
||||
* Key for category to update.
|
||||
*/
|
||||
@@ -147,7 +153,7 @@ class LibraryUpdateService(
|
||||
* @return true if service newly started, false otherwise
|
||||
*/
|
||||
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS /* SY --> */, group: Int = LibraryGroup.BY_DEFAULT, groupExtra: String? = null /* SY <-- */): Boolean {
|
||||
if (!isRunning(context)) {
|
||||
return if (!isRunning(context)) {
|
||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||
putExtra(KEY_TARGET, target)
|
||||
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
||||
@@ -158,10 +164,11 @@ class LibraryUpdateService(
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
|
||||
return true
|
||||
true
|
||||
} else {
|
||||
instance?.addMangaToQueue(category?.id ?: -1, group, groupExtra, target)
|
||||
false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,6 +205,9 @@ class LibraryUpdateService(
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
if (instance == this) {
|
||||
instance = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -221,23 +231,27 @@ class LibraryUpdateService(
|
||||
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
||||
?: return START_NOT_STICKY
|
||||
|
||||
// Unsubscribe from any previous subscription if needed.
|
||||
instance = this
|
||||
|
||||
// Unsubscribe from any previous subscription if needed
|
||||
updateJob?.cancel()
|
||||
|
||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||
val selectedScheme = preferences.libraryUpdatePrioritization().get()
|
||||
val mangaList = getMangaToUpdate(intent, target)
|
||||
.sortedWith(rankingScheme[selectedScheme])
|
||||
// Update favorite manga
|
||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
|
||||
val groupExtra = intent.getStringExtra(KEY_GROUP_EXTRA)
|
||||
addMangaToQueue(categoryId, group, groupExtra, target)
|
||||
|
||||
// Destroy service when completed or in case of an error.
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Timber.e(exception)
|
||||
stopSelf(startId)
|
||||
}
|
||||
updateJob = ioScope.launch(handler) {
|
||||
when (target) {
|
||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||
Target.COVERS -> updateCovers(mangaList)
|
||||
Target.TRACKING -> updateTrackings(mangaList)
|
||||
Target.CHAPTERS -> updateChapterList()
|
||||
Target.COVERS -> updateCovers()
|
||||
Target.TRACKING -> updateTrackings()
|
||||
// SY -->
|
||||
Target.SYNC_FOLLOWS -> syncFollows()
|
||||
Target.PUSH_FAVORITES -> pushFavorites()
|
||||
@@ -250,36 +264,40 @@ class LibraryUpdateService(
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of manga to be updated.
|
||||
* Adds list of manga to be updated.
|
||||
*
|
||||
* @param intent the update intent.
|
||||
* @param category the ID of the category to update, or -1 if no category specified.
|
||||
* @param target the target to update.
|
||||
* @return a list of manga to update
|
||||
*/
|
||||
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||
fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?, target: Target) {
|
||||
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||
// SY -->
|
||||
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
|
||||
val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get()
|
||||
// SY <--
|
||||
|
||||
var listToUpdate = if (categoryId != -1) {
|
||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||
libraryManga.filter { it.category == categoryId }
|
||||
// SY -->
|
||||
} else if (group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)) {
|
||||
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
||||
if (categoriesToUpdate.isNotEmpty()) {
|
||||
db.getLibraryMangas().executeAsBlocking()
|
||||
.filter { it.category in categoriesToUpdate }
|
||||
.distinctBy { it.id }
|
||||
val listToInclude = if (categoriesToUpdate.isNotEmpty()) {
|
||||
libraryManga.filter { it.category in categoriesToUpdate }
|
||||
} else {
|
||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||
libraryManga
|
||||
}
|
||||
|
||||
val categoriesToExclude = preferences.libraryUpdateCategoriesExclude().get().map(String::toInt)
|
||||
val listToExclude = if (categoriesToExclude.isNotEmpty()) {
|
||||
libraryManga.filter { it.category in categoriesToExclude }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
listToInclude.minus(listToExclude)
|
||||
} else {
|
||||
val libraryManga = db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||
when (group) {
|
||||
LibraryGroup.BY_TRACK_STATUS -> {
|
||||
val trackingExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
|
||||
val trackingExtra = groupExtra?.toIntOrNull() ?: -1
|
||||
libraryManga.filter {
|
||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||
val status: String = run {
|
||||
@@ -298,12 +316,12 @@ class LibraryUpdateService(
|
||||
}
|
||||
}
|
||||
LibraryGroup.BY_SOURCE -> {
|
||||
val sourceExtra = intent.getStringExtra(KEY_GROUP_EXTRA).nullIfBlank()
|
||||
val sourceExtra = groupExtra.nullIfBlank()
|
||||
val source = sourceManager.getCatalogueSources().find { it.name == sourceExtra }
|
||||
if (source != null) libraryManga.filter { it.source == source.id } else emptyList()
|
||||
}
|
||||
LibraryGroup.BY_STATUS -> {
|
||||
val statusExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
|
||||
val statusExtra = groupExtra?.toIntOrNull() ?: -1
|
||||
libraryManga.filter {
|
||||
it.status == statusExtra
|
||||
}
|
||||
@@ -314,10 +332,13 @@ class LibraryUpdateService(
|
||||
// SY <--
|
||||
}
|
||||
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
|
||||
}
|
||||
|
||||
return listToUpdate
|
||||
val selectedScheme = preferences.libraryUpdatePrioritization().get()
|
||||
mangaToUpdate = listToUpdate
|
||||
.distinctBy { it.id }
|
||||
.sortedWith(rankingScheme[selectedScheme])
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,12 +350,13 @@ class LibraryUpdateService(
|
||||
* @param mangaToUpdate the list to update
|
||||
* @return an observable delivering the progress of each update.
|
||||
*/
|
||||
suspend fun updateChapterList(mangaToUpdate: List<LibraryManga>) {
|
||||
suspend fun updateChapterList() {
|
||||
val semaphore = Semaphore(5)
|
||||
val progressCount = AtomicInteger(0)
|
||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
||||
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
||||
var hasDownloads = false
|
||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||
|
||||
withIOContext {
|
||||
mangaToUpdate.groupBy { it.source }
|
||||
@@ -373,6 +395,10 @@ class LibraryUpdateService(
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
}
|
||||
|
||||
if (preferences.autoUpdateTrackers()) {
|
||||
updateTrackings(manga, loggedServices)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -442,7 +468,7 @@ class LibraryUpdateService(
|
||||
Timber.e(exception)
|
||||
}
|
||||
ioScope.launch(handler) {
|
||||
if (source is MangaDex && trackManager.mdList.isLogged) {
|
||||
if (source.isMdBasedSource() && trackManager.mdList.isLogged) {
|
||||
val tracks = db.getTracks(manga).executeOnIO()
|
||||
if (tracks.isEmpty() || tracks.none { it.sync_id == TrackManager.MDLIST }) {
|
||||
var track = trackManager.mdList.createInitialTracker(manga)
|
||||
@@ -463,7 +489,7 @@ class LibraryUpdateService(
|
||||
return syncChaptersWithSource(db, chapters, manga, source)
|
||||
}
|
||||
|
||||
private suspend fun updateCovers(mangaToUpdate: List<LibraryManga>) {
|
||||
private suspend fun updateCovers() {
|
||||
var progressCount = 0
|
||||
|
||||
mangaToUpdate.forEach { manga ->
|
||||
@@ -489,6 +515,7 @@ class LibraryUpdateService(
|
||||
}
|
||||
}
|
||||
|
||||
coverCache.clearMemoryCache()
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
|
||||
@@ -496,7 +523,7 @@ class LibraryUpdateService(
|
||||
* Method that updates the metadata of the connected tracking services. It's called in a
|
||||
* background thread, so it's safe to do heavy operations or network calls here.
|
||||
*/
|
||||
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
|
||||
private suspend fun updateTrackings() {
|
||||
var progressCount = 0
|
||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||
|
||||
@@ -509,27 +536,35 @@ class LibraryUpdateService(
|
||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
||||
|
||||
// Update the tracking details.
|
||||
db.getTracks(manga).executeAsBlocking()
|
||||
.map { track ->
|
||||
supervisorScope {
|
||||
async {
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service in loggedServices) {
|
||||
try {
|
||||
val updatedTrack = service.refresh(track)
|
||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
Timber.e(e)
|
||||
updateTrackings(manga, loggedServices)
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
|
||||
private suspend fun updateTrackings(manga: LibraryManga, loggedServices: List<TrackService>) {
|
||||
db.getTracks(manga).executeAsBlocking()
|
||||
.map { track ->
|
||||
supervisorScope {
|
||||
async {
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service in loggedServices) {
|
||||
try {
|
||||
val updatedTrack = service.refresh(track)
|
||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||
|
||||
if (service is UnattendedTrackService) {
|
||||
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@@ -539,11 +574,12 @@ class LibraryUpdateService(
|
||||
private suspend fun syncFollows() {
|
||||
val count = AtomicInteger(0)
|
||||
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return
|
||||
val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() }
|
||||
|
||||
val size: Int
|
||||
mangaDex.fetchAllFollows(true)
|
||||
mangaDex.fetchAllFollows()
|
||||
.filter { (_, metadata) ->
|
||||
metadata.follow_status == FollowStatus.RE_READING.int || metadata.follow_status == FollowStatus.READING.int
|
||||
syncFollowStatusInts.contains(metadata.followStatus)
|
||||
}
|
||||
.also { size = it.size }
|
||||
.forEach { (networkManga, metadata) ->
|
||||
@@ -569,7 +605,7 @@ class LibraryUpdateService(
|
||||
val id = db.insertManga(dbManga).executeOnIO().insertedId()
|
||||
if (id != null) {
|
||||
metadata.mangaId = id
|
||||
db.insertFlatMetadata(metadata.flatten()).await()
|
||||
db.insertFlatMetadataAsync(metadata.flatten()).await()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.md.similar.SimilarUpdateService
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -59,22 +58,22 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
ACTION_SHARE_IMAGE ->
|
||||
shareImage(
|
||||
context,
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
// Delete image from path and dismiss notification
|
||||
ACTION_DELETE_IMAGE ->
|
||||
deleteImage(
|
||||
context,
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
// Share backup file
|
||||
ACTION_SHARE_BACKUP ->
|
||||
shareFile(
|
||||
context,
|
||||
intent.getParcelableExtra(EXTRA_URI),
|
||||
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/octet-stream+gzip",
|
||||
intent.getParcelableExtra(EXTRA_URI)!!,
|
||||
"application/x-protobuf+gzip",
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
ACTION_CANCEL_RESTORE -> cancelRestore(
|
||||
@@ -107,13 +106,10 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
ACTION_SHARE_CRASH_LOG ->
|
||||
shareFile(
|
||||
context,
|
||||
intent.getParcelableExtra(EXTRA_URI),
|
||||
intent.getParcelableExtra(EXTRA_URI)!!,
|
||||
"text/plain",
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
// SY -->
|
||||
ACTION_CANCEL_SIMILAR_UPDATE -> cancelSimilarUpdate(context)
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,18 +251,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* Method called when user wants to stop a similar manga update
|
||||
*
|
||||
* @param context context of application
|
||||
*/
|
||||
private fun cancelSimilarUpdate(context: Context) {
|
||||
SimilarUpdateService.stop(context)
|
||||
Handler().post { dismissNotification(context, Notifications.ID_SIMILAR_PROGRESS) }
|
||||
}
|
||||
// SY <--
|
||||
|
||||
companion object {
|
||||
private const val NAME = "NotificationReceiver"
|
||||
|
||||
@@ -297,12 +281,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
||||
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
||||
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
|
||||
private const val EXTRA_IS_LEGACY_BACKUP = "$ID.$NAME.EXTRA_IS_LEGACY_BACKUP"
|
||||
|
||||
// Sy -->
|
||||
// Called to cancel similar manga update.
|
||||
private const val ACTION_CANCEL_SIMILAR_UPDATE = "$ID.$NAME.CANCEL_SIMILAR_UPDATE"
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Returns a [PendingIntent] that resumes the download of a chapter
|
||||
@@ -515,11 +493,10 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* @param notificationId id of notification
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, isLegacyFormat: Boolean, notificationId: Int): PendingIntent {
|
||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_SHARE_BACKUP
|
||||
putExtra(EXTRA_URI, uri)
|
||||
putExtra(EXTRA_IS_LEGACY_BACKUP, isLegacyFormat)
|
||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
@@ -572,20 +549,5 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* Returns [PendingIntent] that starts a service which stops the similar update
|
||||
*
|
||||
* @param context context of application
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun cancelSimilarUpdatePendingBroadcast(context: Context): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_CANCEL_SIMILAR_UPDATE
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,14 +68,11 @@ object Notifications {
|
||||
const val CHANNEL_CRASH_LOGS = "crash_logs_channel"
|
||||
const val ID_CRASH_LOGS = -601
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* Notification channel and ids used for backup and restore.
|
||||
* Notification channel used for Incognito Mode
|
||||
*/
|
||||
const val CHANNEL_SIMILAR = "similar_channel"
|
||||
const val ID_SIMILAR_PROGRESS = -901
|
||||
const val ID_SIMILAR_COMPLETE = -902
|
||||
// SY <--
|
||||
const val CHANNEL_INCOGNITO_MODE = "incognito_mode_channel"
|
||||
const val ID_INCOGNITO_MODE = -701
|
||||
|
||||
private val deprecatedChannels = listOf(
|
||||
"downloader_channel",
|
||||
@@ -165,12 +162,10 @@ object Notifications {
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
),
|
||||
NotificationChannel(
|
||||
CHANNEL_SIMILAR,
|
||||
context.getString(R.string.similar_manga),
|
||||
CHANNEL_INCOGNITO_MODE,
|
||||
context.getString(R.string.pref_incognito_mode),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
setShowBadge(false)
|
||||
}
|
||||
)
|
||||
).forEach(context.notificationManager::createNotificationChannel)
|
||||
|
||||
// Delete old notification channels
|
||||
|
||||
@@ -13,17 +13,25 @@ object PreferenceKeys {
|
||||
|
||||
const val confirmExit = "pref_confirm_exit"
|
||||
|
||||
const val hideBottomBar = "pref_hide_bottom_bar_on_scroll"
|
||||
const val hideBottomBarOnScroll = "pref_hide_bottom_bar_on_scroll"
|
||||
|
||||
const val rotation = "pref_rotation_type_key"
|
||||
const val showSideNavOnBottom = "pref_show_side_nav_on_bottom"
|
||||
|
||||
const val enableTransitions = "pref_enable_transitions_key"
|
||||
const val enableTransitionsPager = "pref_enable_transitions_pager_key"
|
||||
|
||||
const val enableTransitionsWebtoon = "pref_enable_transitions_webtoon_key"
|
||||
|
||||
const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed"
|
||||
|
||||
const val showPageNumber = "pref_show_page_number_key"
|
||||
|
||||
const val dualPageSplit = "pref_dual_page_split"
|
||||
const val dualPageSplitPaged = "pref_dual_page_split"
|
||||
|
||||
const val dualPageSplitWebtoon = "pref_dual_page_split_webtoon"
|
||||
|
||||
const val dualPageInvertPaged = "pref_dual_page_invert"
|
||||
|
||||
const val dualPageInvertWebtoon = "pref_dual_page_invert_webtoon"
|
||||
|
||||
const val showReadingMode = "pref_show_reading_mode"
|
||||
|
||||
@@ -45,7 +53,11 @@ object PreferenceKeys {
|
||||
|
||||
const val colorFilterMode = "color_filter_mode"
|
||||
|
||||
const val defaultViewer = "pref_default_viewer_key"
|
||||
const val grayscale = "pref_grayscale"
|
||||
|
||||
const val defaultReadingMode = "pref_default_reading_mode_key"
|
||||
|
||||
const val defaultOrientationType = "pref_default_orientation_type_key"
|
||||
|
||||
const val imageScaleType = "pref_image_scale_type_key"
|
||||
|
||||
@@ -73,6 +85,10 @@ object PreferenceKeys {
|
||||
|
||||
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
|
||||
|
||||
const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user"
|
||||
|
||||
const val showNavigationOverlayOnStart = "reader_navigation_overlay_on_start"
|
||||
|
||||
const val webtoonSidePadding = "webtoon_side_padding"
|
||||
|
||||
const val portraitColumns = "pref_library_columns_portrait_key"
|
||||
@@ -85,6 +101,8 @@ object PreferenceKeys {
|
||||
|
||||
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||
|
||||
const val autoAddTrack = "pref_auto_add_track_key"
|
||||
|
||||
const val lastUsedSource = "last_catalogue_source"
|
||||
|
||||
const val lastUsedCategory = "last_used_category"
|
||||
@@ -99,6 +117,8 @@ object PreferenceKeys {
|
||||
|
||||
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
||||
|
||||
const val folderPerManga = "create_folder_per_manga"
|
||||
|
||||
const val numberOfBackups = "backup_slots"
|
||||
|
||||
const val backupInterval = "backup_interval"
|
||||
@@ -114,6 +134,7 @@ object PreferenceKeys {
|
||||
const val libraryUpdateRestriction = "library_update_restriction"
|
||||
|
||||
const val libraryUpdateCategories = "library_update_categories"
|
||||
const val libraryUpdateCategoriesExclude = "library_update_categories_exclude"
|
||||
|
||||
const val libraryUpdatePrioritization = "library_update_prioritization"
|
||||
|
||||
@@ -141,7 +162,7 @@ object PreferenceKeys {
|
||||
|
||||
const val startScreen = "start_screen"
|
||||
|
||||
const val useBiometricLock = "use_biometric_lock"
|
||||
const val useAuthenticator = "use_biometric_lock"
|
||||
|
||||
const val lockAppAfter = "lock_app_after"
|
||||
|
||||
@@ -153,11 +174,14 @@ object PreferenceKeys {
|
||||
|
||||
const val autoUpdateMetadata = "auto_update_metadata"
|
||||
|
||||
const val autoUpdateTrackers = "auto_update_trackers"
|
||||
|
||||
const val showLibraryUpdateErrors = "show_library_update_errors"
|
||||
|
||||
const val downloadNew = "download_new"
|
||||
|
||||
const val downloadNewCategories = "download_new_categories"
|
||||
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
|
||||
|
||||
const val libraryDisplayMode = "pref_display_mode_library"
|
||||
|
||||
@@ -175,6 +199,8 @@ object PreferenceKeys {
|
||||
|
||||
const val unreadBadge = "display_unread_badge"
|
||||
|
||||
const val localBadge = "display_local_badge"
|
||||
|
||||
const val categoryTabs = "display_category_tabs"
|
||||
|
||||
const val categoryNumberOfItems = "display_number_of_items"
|
||||
@@ -183,7 +209,7 @@ object PreferenceKeys {
|
||||
|
||||
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
||||
|
||||
const val enableDoh = "enable_doh"
|
||||
const val dohProvider = "doh_provider"
|
||||
|
||||
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
|
||||
|
||||
@@ -307,11 +333,7 @@ object PreferenceKeys {
|
||||
|
||||
const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers"
|
||||
|
||||
const val mangadexSimilarEnabled = "pref_related_show_tab_key"
|
||||
|
||||
const val mangadexSimilarUpdateInterval = "related_update_interval"
|
||||
|
||||
const val mangadexSimilarOnlyOverWifi = "pref_simular_only_over_wifi_key"
|
||||
const val mangadexSyncToLibraryIndexes = "pref_mangadex_sync_to_library_indexes"
|
||||
|
||||
const val preferredMangaDexId = "preferred_mangaDex_id"
|
||||
|
||||
@@ -335,15 +357,31 @@ object PreferenceKeys {
|
||||
|
||||
const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders"
|
||||
|
||||
const val biometricTimeRanges = "biometric_time_ranges"
|
||||
const val authenticatorTimeRanges = "biometric_time_ranges"
|
||||
|
||||
const val sortTagsForLibrary = "sort_tags_for_library"
|
||||
|
||||
const val createLegacyBackup = "create_legacy_backup"
|
||||
|
||||
const val dontDeleteFromCategories = "dont_delete_from_categories"
|
||||
|
||||
const val extensionRepos = "extension_repos"
|
||||
|
||||
const val cropBordersContinuesVertical = "crop_borders_continues_vertical"
|
||||
const val cropBordersContinuousVertical = "crop_borders_continues_vertical"
|
||||
|
||||
const val landscapeVerticalSeekbar = "pref_show_vert_seekbar_landscape"
|
||||
|
||||
const val leftVerticalSeekbar = "pref_left_handed_vertical_seekbar"
|
||||
|
||||
const val forceHorizontalSeekbar = "pref_force_horz_seekbar"
|
||||
|
||||
const val readerBottomButtons = "reader_bottom_buttons"
|
||||
|
||||
const val bottomBarLabels = "pref_show_bottom_bar_labels"
|
||||
|
||||
const val hideUpdatesButton = "pref_hide_updates_button"
|
||||
|
||||
const val hideHistoryButton = "pref_hide_history_button"
|
||||
|
||||
const val pageLayout = "page_layout"
|
||||
|
||||
const val invertDoublePages = "invert_double_pages"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package eu.kanade.tachiyomi.data.preference
|
||||
|
||||
const val UNMETERED_NETWORK = "wifi"
|
||||
const val CHARGING = "ac"
|
||||
|
||||
/**
|
||||
* This class stores the values for the preferences in the application.
|
||||
*/
|
||||
object PreferenceValues {
|
||||
|
||||
/* ktlint-disable experimental:enum-entry-name-case */
|
||||
|
||||
// Keys are lowercase to match legacy string values
|
||||
enum class ThemeMode {
|
||||
light,
|
||||
@@ -16,17 +21,23 @@ object PreferenceValues {
|
||||
enum class LightThemeVariant {
|
||||
default,
|
||||
blue,
|
||||
strawberrydaiquiri,
|
||||
}
|
||||
|
||||
// Keys are lowercase to match legacy string values
|
||||
enum class DarkThemeVariant {
|
||||
default,
|
||||
blue,
|
||||
amoled,
|
||||
red,
|
||||
greenapple,
|
||||
midnightdusk,
|
||||
amoled,
|
||||
hotpink,
|
||||
amoledblue,
|
||||
red,
|
||||
}
|
||||
|
||||
/* ktlint-enable experimental:enum-entry-name-case */
|
||||
|
||||
enum class DisplayMode {
|
||||
COMPACT_GRID,
|
||||
COMFORTABLE_GRID,
|
||||
|
||||
@@ -12,6 +12,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -22,7 +26,7 @@ import java.util.Locale
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||
|
||||
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
||||
fun <T> Preference<T>.asImmediateFlow(block: (T) -> Unit): Flow<T> {
|
||||
block(get())
|
||||
return asFlow()
|
||||
.onEach { block(it) }
|
||||
@@ -36,6 +40,11 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
|
||||
set(get() - item)
|
||||
}
|
||||
|
||||
fun Preference<Boolean>.toggle(): Boolean {
|
||||
set(!get())
|
||||
return get()
|
||||
}
|
||||
|
||||
class PreferencesHelper(val context: Context) {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
@@ -57,9 +66,11 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
|
||||
|
||||
fun hideBottomBar() = flowPrefs.getBoolean(Keys.hideBottomBar, true)
|
||||
fun hideBottomBarOnScroll() = flowPrefs.getBoolean(Keys.hideBottomBarOnScroll, true)
|
||||
|
||||
fun useBiometricLock() = flowPrefs.getBoolean(Keys.useBiometricLock, false)
|
||||
fun showSideNavOnBottom() = flowPrefs.getBoolean(Keys.showSideNavOnBottom, false)
|
||||
|
||||
fun useAuthenticator() = flowPrefs.getBoolean(Keys.useAuthenticator, false)
|
||||
|
||||
fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0)
|
||||
|
||||
@@ -71,9 +82,9 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun autoUpdateMetadata() = prefs.getBoolean(Keys.autoUpdateMetadata, false)
|
||||
|
||||
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
|
||||
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
||||
|
||||
fun clear() = prefs.edit { clear() }
|
||||
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
|
||||
|
||||
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system)
|
||||
|
||||
@@ -81,15 +92,21 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun themeDark() = flowPrefs.getEnum(Keys.themeDark, Values.DarkThemeVariant.default)
|
||||
|
||||
fun rotation() = flowPrefs.getInt(Keys.rotation, 1)
|
||||
fun pageTransitionsPager() = flowPrefs.getBoolean(Keys.enableTransitionsPager, true)
|
||||
|
||||
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
||||
fun pageTransitionsWebtoon() = flowPrefs.getBoolean(Keys.enableTransitionsWebtoon, true)
|
||||
|
||||
fun doubleTapAnimSpeed() = flowPrefs.getInt(Keys.doubleTapAnimationSpeed, 500)
|
||||
|
||||
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
||||
|
||||
fun dualPageSplit() = flowPrefs.getBoolean(Keys.dualPageSplit, false)
|
||||
fun dualPageSplitPaged() = flowPrefs.getBoolean(Keys.dualPageSplitPaged, false)
|
||||
|
||||
fun dualPageSplitWebtoon() = flowPrefs.getBoolean(Keys.dualPageSplitWebtoon, false)
|
||||
|
||||
fun dualPageInvertPaged() = flowPrefs.getBoolean(Keys.dualPageInvertPaged, false)
|
||||
|
||||
fun dualPageInvertWebtoon() = flowPrefs.getBoolean(Keys.dualPageInvertWebtoon, false)
|
||||
|
||||
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
||||
|
||||
@@ -111,7 +128,11 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
|
||||
|
||||
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 2)
|
||||
fun grayscale() = flowPrefs.getBoolean(Keys.grayscale, false)
|
||||
|
||||
fun defaultReadingMode() = prefs.getInt(Keys.defaultReadingMode, ReadingModeType.RIGHT_TO_LEFT.flagValue)
|
||||
|
||||
fun defaultOrientationType() = prefs.getInt(Keys.defaultOrientationType, OrientationType.FREE.flagValue)
|
||||
|
||||
fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
|
||||
|
||||
@@ -143,6 +164,10 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
|
||||
|
||||
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true)
|
||||
|
||||
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean(Keys.showNavigationOverlayOnStart, false)
|
||||
|
||||
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
|
||||
|
||||
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
||||
@@ -153,6 +178,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||
|
||||
fun autoAddTrack() = prefs.getBoolean(Keys.autoAddTrack, true)
|
||||
|
||||
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
|
||||
|
||||
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
|
||||
@@ -189,6 +216,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||
|
||||
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
||||
|
||||
fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
|
||||
|
||||
fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)
|
||||
@@ -201,9 +230,10 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
||||
|
||||
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
|
||||
fun libraryUpdateRestriction() = flowPrefs.getStringSet(Keys.libraryUpdateRestriction, setOf(UNMETERED_NETWORK))
|
||||
|
||||
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
||||
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
|
||||
|
||||
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
||||
|
||||
@@ -211,6 +241,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
||||
|
||||
fun localBadge() = flowPrefs.getBoolean(Keys.localBadge, true)
|
||||
|
||||
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
|
||||
|
||||
fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true)
|
||||
@@ -254,6 +286,7 @@ class PreferencesHelper(val context: Context) {
|
||||
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
|
||||
|
||||
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
||||
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
|
||||
|
||||
fun lang() = prefs.getString(Keys.lang, "")
|
||||
|
||||
@@ -267,7 +300,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
|
||||
|
||||
fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false)
|
||||
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
|
||||
|
||||
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
|
||||
|
||||
@@ -277,16 +310,14 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun filterChapterByBookmarked() = prefs.getInt(Keys.defaultChapterFilterByBookmarked, Manga.SHOW_ALL)
|
||||
|
||||
fun sortChapterBySourceOrNumber() = prefs.getInt(Keys.defaultChapterSortBySourceOrNumber, Manga.SORTING_SOURCE)
|
||||
fun sortChapterBySourceOrNumber() = prefs.getInt(Keys.defaultChapterSortBySourceOrNumber, Manga.CHAPTER_SORTING_SOURCE)
|
||||
|
||||
fun displayChapterByNameOrNumber() = prefs.getInt(Keys.defaultChapterDisplayByNameOrNumber, Manga.DISPLAY_NAME)
|
||||
fun displayChapterByNameOrNumber() = prefs.getInt(Keys.defaultChapterDisplayByNameOrNumber, Manga.CHAPTER_DISPLAY_NAME)
|
||||
|
||||
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.SORT_DESC)
|
||||
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.CHAPTER_SORT_DESC)
|
||||
|
||||
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
|
||||
|
||||
fun createLegacyBackup() = flowPrefs.getBoolean(Keys.createLegacyBackup, true)
|
||||
|
||||
fun setChapterSettingsDefault(manga: Manga) {
|
||||
prefs.edit {
|
||||
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
|
||||
@@ -294,7 +325,7 @@ class PreferencesHelper(val context: Context) {
|
||||
putInt(Keys.defaultChapterFilterByBookmarked, manga.bookmarkedFilter)
|
||||
putInt(Keys.defaultChapterSortBySourceOrNumber, manga.sorting)
|
||||
putInt(Keys.defaultChapterDisplayByNameOrNumber, manga.displayMode)
|
||||
putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.SORT_DESC else Manga.SORT_ASC)
|
||||
putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,13 +450,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0")
|
||||
|
||||
fun mangadexSimilarEnabled() = flowPrefs.getBoolean(Keys.mangadexSimilarEnabled, false)
|
||||
|
||||
fun shownMangaDexSimilarAskDialog() = flowPrefs.getBoolean("shown_similar_ask_dialog", false)
|
||||
|
||||
fun mangadexSimilarOnlyOverWifi() = flowPrefs.getBoolean(Keys.mangadexSimilarOnlyOverWifi, true)
|
||||
|
||||
fun mangadexSimilarUpdateInterval() = flowPrefs.getInt(Keys.mangadexSimilarUpdateInterval, 2)
|
||||
fun mangadexSyncToLibraryIndexes() = flowPrefs.getStringSet(Keys.mangadexSyncToLibraryIndexes, emptySet())
|
||||
|
||||
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
|
||||
|
||||
@@ -447,7 +472,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false)
|
||||
|
||||
fun biometricTimeRanges() = flowPrefs.getStringSet(Keys.biometricTimeRanges, mutableSetOf())
|
||||
fun authenticatorTimeRanges() = flowPrefs.getStringSet(Keys.authenticatorTimeRanges, mutableSetOf())
|
||||
|
||||
fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf())
|
||||
|
||||
@@ -455,5 +480,23 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun extensionRepos() = flowPrefs.getStringSet(Keys.extensionRepos, emptySet())
|
||||
|
||||
fun cropBordersContinuesVertical() = flowPrefs.getBoolean(Keys.cropBordersContinuesVertical, false)
|
||||
fun cropBordersContinuousVertical() = flowPrefs.getBoolean(Keys.cropBordersContinuousVertical, false)
|
||||
|
||||
fun forceHorizontalSeekbar() = flowPrefs.getBoolean(Keys.forceHorizontalSeekbar, false)
|
||||
|
||||
fun landscapeVerticalSeekbar() = flowPrefs.getBoolean(Keys.landscapeVerticalSeekbar, false)
|
||||
|
||||
fun leftVerticalSeekbar() = flowPrefs.getBoolean(Keys.leftVerticalSeekbar, false)
|
||||
|
||||
fun readerBottomButtons() = flowPrefs.getStringSet(Keys.readerBottomButtons, ReaderBottomButton.BUTTONS_DEFAULTS)
|
||||
|
||||
fun bottomBarLabels() = flowPrefs.getBoolean(Keys.bottomBarLabels, true)
|
||||
|
||||
fun hideUpdatesButton() = flowPrefs.getBoolean(Keys.hideUpdatesButton, false)
|
||||
|
||||
fun hideHistoryButton() = flowPrefs.getBoolean(Keys.hideHistoryButton, false)
|
||||
|
||||
fun pageLayout() = flowPrefs.getInt(Keys.pageLayout, PagerConfig.PageLayout.SINGLE_PAGE)
|
||||
|
||||
fun invertDoublePages() = flowPrefs.getBoolean(Keys.invertDoublePages, false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
/**
|
||||
* A TrackService that doesn't need explicit login.
|
||||
*/
|
||||
interface NoLoginTrackService {
|
||||
fun loginNoop()
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||
import eu.kanade.tachiyomi.data.track.komga.Komga
|
||||
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||
@@ -16,9 +17,10 @@ class TrackManager(context: Context) {
|
||||
const val KITSU = 3
|
||||
const val SHIKIMORI = 4
|
||||
const val BANGUMI = 5
|
||||
const val KOMGA = 6
|
||||
|
||||
// SY --> Mangadex from Neko
|
||||
const val MDLIST = 6
|
||||
const val MDLIST = 60
|
||||
// SY <--
|
||||
|
||||
// SY -->
|
||||
@@ -44,7 +46,9 @@ class TrackManager(context: Context) {
|
||||
|
||||
val bangumi = Bangumi(context, BANGUMI)
|
||||
|
||||
val services = listOf(mdList, myAnimeList, aniList, kitsu, shikimori, bangumi)
|
||||
val komga = Komga(context, KOMGA)
|
||||
|
||||
val services = listOf(mdList, myAnimeList, aniList, kitsu, shikimori, bangumi, komga)
|
||||
|
||||
fun getService(id: Int) = services.find { it.id == id }
|
||||
|
||||
|
||||
@@ -46,8 +46,6 @@ abstract class TrackService(val id: Int) {
|
||||
|
||||
abstract fun displayScore(track: Track): String
|
||||
|
||||
abstract suspend fun add(track: Track): Track
|
||||
|
||||
abstract suspend fun update(track: Track): Track
|
||||
|
||||
abstract suspend fun bind(track: Track): Track
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
||||
/**
|
||||
* An Unattended Track Service will never prompt the user to match a manga with the remote.
|
||||
* It is expected that such Track Sercice can only work with specific sources and unique IDs.
|
||||
*/
|
||||
interface UnattendedTrackService {
|
||||
/**
|
||||
* This TrackService will only work with the sources that are accepted by this filter function.
|
||||
*/
|
||||
fun accept(source: Source): Boolean
|
||||
|
||||
/**
|
||||
* match is similar to TrackService.search, but only return zero or one match.
|
||||
*/
|
||||
suspend fun match(manga: Manga): TrackSearch?
|
||||
}
|
||||
@@ -35,6 +35,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
private val api by lazy { AnilistApi(client, interceptor) }
|
||||
|
||||
override val supportsReadingDates: Boolean = true
|
||||
|
||||
private val scorePreference = preferences.anilistScoreType()
|
||||
|
||||
init {
|
||||
@@ -128,7 +130,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun add(track: Track): Track {
|
||||
private suspend fun add(track: Track): Track {
|
||||
return api.addLibManga(track)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,18 @@ package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.afollestad.date.dayOfMonth
|
||||
import com.afollestad.date.month
|
||||
import com.afollestad.date.year
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptor
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
@@ -23,15 +28,18 @@ import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
|
||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
private val authClient = client.newBuilder()
|
||||
.addInterceptor(interceptor)
|
||||
.addInterceptor(RateLimitInterceptor(85, 1, MINUTES))
|
||||
.build()
|
||||
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
val query = """
|
||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
||||
| id
|
||||
@@ -65,10 +73,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
||||
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
||||
val query = """
|
||||
|mutation UpdateManga(
|
||||
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus,
|
||||
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|
||||
|) {
|
||||
|SaveMediaListEntry(
|
||||
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status,
|
||||
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|
||||
|) {
|
||||
|id
|
||||
|status
|
||||
|progress
|
||||
@@ -82,6 +95,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
put("progress", track.last_chapter_read)
|
||||
put("status", track.toAnilistStatus())
|
||||
put("score", track.score.toInt())
|
||||
put("startedAt", createDate(track.started_reading_date))
|
||||
put("completedAt", createDate(track.finished_reading_date))
|
||||
}
|
||||
}
|
||||
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
@@ -92,8 +107,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
val query = """
|
||||
|query Search(${'$'}query: String) {
|
||||
|Page (perPage: 50) {
|
||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||
@@ -143,8 +157,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
val query = """
|
||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||
|Page {
|
||||
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||
@@ -152,6 +165,16 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|status
|
||||
|scoreRaw: score(format: POINT_100)
|
||||
|progress
|
||||
|startedAt {
|
||||
|year
|
||||
|month
|
||||
|day
|
||||
|}
|
||||
|completedAt {
|
||||
|year
|
||||
|month
|
||||
|day
|
||||
|}
|
||||
|media {
|
||||
|id
|
||||
|title {
|
||||
@@ -209,8 +232,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun getCurrentUser(): Pair<Int, String> {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
val query = """
|
||||
|query User {
|
||||
|Viewer {
|
||||
|id
|
||||
@@ -243,21 +265,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
}
|
||||
|
||||
private fun jsonToALManga(struct: JsonObject): ALManga {
|
||||
val date = try {
|
||||
val date = Calendar.getInstance()
|
||||
date.set(
|
||||
struct["startDate"]!!.jsonObject["year"]!!.jsonPrimitive.intOrNull ?: 0,
|
||||
(
|
||||
struct["startDate"]!!.jsonObject["month"]!!.jsonPrimitive.intOrNull
|
||||
?: 0
|
||||
) - 1,
|
||||
struct["startDate"]!!.jsonObject["day"]!!.jsonPrimitive.intOrNull ?: 0
|
||||
)
|
||||
date.timeInMillis
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
|
||||
return ALManga(
|
||||
struct["id"]!!.jsonPrimitive.int,
|
||||
struct["title"]!!.jsonObject["romaji"]!!.jsonPrimitive.content,
|
||||
@@ -265,7 +272,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
struct["description"]!!.jsonPrimitive.contentOrNull,
|
||||
struct["type"]!!.jsonPrimitive.content,
|
||||
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
|
||||
date,
|
||||
parseDate(struct, "startDate"),
|
||||
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0
|
||||
)
|
||||
}
|
||||
@@ -276,10 +283,44 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
struct["status"]!!.jsonPrimitive.content,
|
||||
struct["scoreRaw"]!!.jsonPrimitive.int,
|
||||
struct["progress"]!!.jsonPrimitive.int,
|
||||
parseDate(struct, "startedAt"),
|
||||
parseDate(struct, "completedAt"),
|
||||
jsonToALManga(struct["media"]!!.jsonObject)
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseDate(struct: JsonObject, dateKey: String): Long {
|
||||
return try {
|
||||
val date = Calendar.getInstance()
|
||||
date.set(
|
||||
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
|
||||
struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int - 1,
|
||||
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int
|
||||
)
|
||||
date.timeInMillis
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDate(dateValue: Long): JsonObject {
|
||||
if (dateValue == 0L) {
|
||||
return buildJsonObject {
|
||||
put("year", JsonNull)
|
||||
put("month", JsonNull)
|
||||
put("day", JsonNull)
|
||||
}
|
||||
}
|
||||
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = dateValue
|
||||
return buildJsonObject {
|
||||
put("year", calendar.year)
|
||||
put("month", calendar.month + 1)
|
||||
put("day", calendar.dayOfMonth)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val clientId = "385"
|
||||
private const val apiUrl = "https://graphql.anilist.co/"
|
||||
|
||||
@@ -44,6 +44,8 @@ data class ALUserManga(
|
||||
val list_status: String,
|
||||
val score_raw: Int,
|
||||
val chapters_read: Int,
|
||||
val start_date_fuzzy: Long,
|
||||
val completed_date_fuzzy: Long,
|
||||
val manga: ALManga
|
||||
) {
|
||||
|
||||
@@ -51,6 +53,8 @@ data class ALUserManga(
|
||||
media_id = manga.media_id
|
||||
status = toTrackStatus()
|
||||
score = score_raw.toFloat()
|
||||
started_reading_date = start_date_fuzzy
|
||||
finished_reading_date = completed_date_fuzzy
|
||||
last_chapter_read = chapters_read
|
||||
library_id = this@ALUserManga.library_id
|
||||
total_chapters = manga.total_chapters
|
||||
|
||||
@@ -31,7 +31,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override suspend fun add(track: Track): Track {
|
||||
private suspend fun add(track: Track): Track {
|
||||
return api.addLibManga(track)
|
||||
}
|
||||
|
||||
@@ -45,8 +45,10 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
return if (remoteTrack != null && statusTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
track.status = remoteTrack.status
|
||||
track.last_chapter_read = remoteTrack.last_chapter_read
|
||||
track.status = statusTrack.status
|
||||
track.score = statusTrack.score
|
||||
track.last_chapter_read = statusTrack.last_chapter_read
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
refresh(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
@@ -66,7 +68,6 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
track.copyPersonalFrom(remoteStatusTrack!!)
|
||||
api.findLibManga(track)?.let { remoteTrack ->
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track.status = remoteTrack.status
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@@ -46,6 +47,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
return withIOContext {
|
||||
// read status update
|
||||
val sbody = FormBody.Builder()
|
||||
.add("rating", track.score.toInt().toString())
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
|
||||
@@ -91,12 +93,24 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
}
|
||||
|
||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||
val coverUrl = if (obj["images"] is JsonObject) {
|
||||
obj["images"]?.jsonObject?.get("common")?.jsonPrimitive?.contentOrNull ?: ""
|
||||
} else {
|
||||
// Sometimes JsonNull
|
||||
""
|
||||
}
|
||||
val totalChapters = if (obj["eps_count"] != null) {
|
||||
obj["eps_count"]!!.jsonPrimitive.int
|
||||
} else {
|
||||
0
|
||||
}
|
||||
return TrackSearch.create(TrackManager.BANGUMI).apply {
|
||||
media_id = obj["id"]!!.jsonPrimitive.int
|
||||
title = obj["name_cn"]!!.jsonPrimitive.content
|
||||
cover_url = obj["images"]!!.jsonObject["common"]!!.jsonPrimitive.content
|
||||
cover_url = coverUrl
|
||||
summary = obj["name"]!!.jsonPrimitive.content
|
||||
tracking_url = obj["url"]!!.jsonPrimitive.content
|
||||
total_chapters = totalChapters
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,14 +133,21 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
.build()
|
||||
|
||||
// TODO: get user readed chapter here
|
||||
authClient.newCall(requestUserRead)
|
||||
.await()
|
||||
.parseAs<Collection>()
|
||||
.let {
|
||||
var response = authClient.newCall(requestUserRead).await()
|
||||
var responseBody = response.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
if (responseBody.contains("\"code\":400")) {
|
||||
null
|
||||
} else {
|
||||
json.decodeFromString<Collection>(responseBody).let {
|
||||
track.status = it.status?.id!!
|
||||
track.last_chapter_read = it.ep_status!!
|
||||
track.score = it.rating!!
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ data class Collection(
|
||||
val comment: String? = "",
|
||||
val ep_status: Int? = 0,
|
||||
val lasttouch: Int? = 0,
|
||||
val rating: Int? = 0,
|
||||
val rating: Float? = 0f,
|
||||
val status: Status? = Status(),
|
||||
val tag: List<String?>? = listOf(),
|
||||
val user: User? = User(),
|
||||
|
||||
@@ -67,7 +67,7 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
return df.format(track.score)
|
||||
}
|
||||
|
||||
override suspend fun add(track: Track): Track {
|
||||
private suspend fun add(track: Track): Track {
|
||||
return api.addLibManga(track, getUserId())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package eu.kanade.tachiyomi.data.track.komga
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.NoLoginTrackService
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import okhttp3.Dns
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class Komga(private val context: Context, id: Int) : TrackService(id), UnattendedTrackService, NoLoginTrackService {
|
||||
|
||||
companion object {
|
||||
const val UNREAD = 1
|
||||
const val READING = 2
|
||||
const val COMPLETED = 3
|
||||
|
||||
const val ACCEPTED_SOURCE = "eu.kanade.tachiyomi.extension.all.komga.Komga"
|
||||
}
|
||||
|
||||
override val client: OkHttpClient =
|
||||
networkService.client.newBuilder()
|
||||
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
|
||||
.build()
|
||||
|
||||
val api by lazy { KomgaApi(client) }
|
||||
|
||||
@StringRes
|
||||
override fun nameRes() = R.string.tracker_komga
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_komga
|
||||
|
||||
override fun getLogoColor() = Color.rgb(51, 37, 50)
|
||||
|
||||
override fun getStatusList() = listOf(UNREAD, READING, COMPLETED)
|
||||
|
||||
override fun getStatus(status: Int): String = with(context) {
|
||||
when (status) {
|
||||
UNREAD -> getString(R.string.unread)
|
||||
READING -> getString(R.string.currently_reading)
|
||||
COMPLETED -> getString(R.string.completed)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCompletionStatus(): Int = COMPLETED
|
||||
|
||||
override fun getScoreList(): List<String> = emptyList()
|
||||
|
||||
override fun displayScore(track: Track): String = ""
|
||||
|
||||
override suspend fun update(track: Track): Track {
|
||||
return api.updateProgress(track)
|
||||
}
|
||||
|
||||
override suspend fun bind(track: Track): Track {
|
||||
return track
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
TODO("Not yet implemented: search")
|
||||
}
|
||||
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteTrack = api.getTrackSearch(track.tracking_url)!!
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
return track
|
||||
}
|
||||
|
||||
override suspend fun login(username: String, password: String) {
|
||||
saveCredentials("user", "pass")
|
||||
}
|
||||
|
||||
// TrackService.isLogged works by checking that credentials are saved.
|
||||
// By saving dummy, unused credentials, we can activate the tracker simply by login/logout
|
||||
override fun loginNoop() {
|
||||
saveCredentials("user", "pass")
|
||||
}
|
||||
|
||||
override fun accept(source: Source): Boolean = source::class.qualifiedName == ACCEPTED_SOURCE
|
||||
|
||||
override suspend fun match(manga: Manga): TrackSearch? =
|
||||
try {
|
||||
api.getTrackSearch(manga.url)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package eu.kanade.tachiyomi.data.track.komga
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
const val READLIST_API = "/api/v1/readlists"
|
||||
|
||||
class KomgaApi(private val client: OkHttpClient) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
suspend fun getTrackSearch(url: String): TrackSearch =
|
||||
withIOContext {
|
||||
try {
|
||||
val track = if (url.contains(READLIST_API)) {
|
||||
client.newCall(GET(url))
|
||||
.await()
|
||||
.parseAs<ReadListDto>()
|
||||
.toTrack()
|
||||
} else {
|
||||
client.newCall(GET(url))
|
||||
.await()
|
||||
.parseAs<SeriesDto>()
|
||||
.toTrack()
|
||||
}
|
||||
|
||||
val progress = client
|
||||
.newCall(GET("$url/read-progress/tachiyomi"))
|
||||
.await()
|
||||
.parseAs<ReadProgressDto>()
|
||||
|
||||
track.apply {
|
||||
cover_url = "$url/thumbnail"
|
||||
tracking_url = url
|
||||
total_chapters = progress.booksCount
|
||||
status = when (progress.booksCount) {
|
||||
progress.booksUnreadCount -> Komga.UNREAD
|
||||
progress.booksReadCount -> Komga.COMPLETED
|
||||
else -> Komga.READING
|
||||
}
|
||||
last_chapter_read = progress.lastReadContinuousIndex
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Could not get item: $url")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateProgress(track: Track): Track {
|
||||
val progress = ReadProgressUpdateDto(track.last_chapter_read)
|
||||
val payload = json.encodeToString(progress)
|
||||
client.newCall(
|
||||
Request.Builder()
|
||||
.url("${track.tracking_url}/read-progress/tachiyomi")
|
||||
.put(payload.toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
)
|
||||
.await()
|
||||
return getTrackSearch(track.tracking_url)
|
||||
}
|
||||
|
||||
private fun SeriesDto.toTrack(): TrackSearch = TrackSearch.create(TrackManager.KOMGA).also {
|
||||
it.title = metadata.title
|
||||
it.summary = metadata.summary
|
||||
it.publishing_status = metadata.status
|
||||
}
|
||||
|
||||
private fun ReadListDto.toTrack(): TrackSearch = TrackSearch.create(TrackManager.KOMGA).also {
|
||||
it.title = name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package eu.kanade.tachiyomi.data.track.komga
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SeriesDto(
|
||||
val id: String,
|
||||
val libraryId: String,
|
||||
val name: String,
|
||||
val created: String?,
|
||||
val lastModified: String?,
|
||||
val fileLastModified: String,
|
||||
val booksCount: Int,
|
||||
val booksReadCount: Int,
|
||||
val booksUnreadCount: Int,
|
||||
val booksInProgressCount: Int,
|
||||
val metadata: SeriesMetadataDto,
|
||||
val booksMetadata: BookMetadataAggregationDto
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SeriesMetadataDto(
|
||||
val status: String,
|
||||
val created: String?,
|
||||
val lastModified: String?,
|
||||
val title: String,
|
||||
val titleSort: String,
|
||||
val summary: String,
|
||||
val summaryLock: Boolean,
|
||||
val readingDirection: String,
|
||||
val readingDirectionLock: Boolean,
|
||||
val publisher: String,
|
||||
val publisherLock: Boolean,
|
||||
val ageRating: Int?,
|
||||
val ageRatingLock: Boolean,
|
||||
val language: String,
|
||||
val languageLock: Boolean,
|
||||
val genres: Set<String>,
|
||||
val genresLock: Boolean,
|
||||
val tags: Set<String>,
|
||||
val tagsLock: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BookMetadataAggregationDto(
|
||||
val authors: List<AuthorDto> = emptyList(),
|
||||
val releaseDate: String?,
|
||||
val summary: String,
|
||||
val summaryNumber: String,
|
||||
|
||||
val created: String,
|
||||
val lastModified: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuthorDto(
|
||||
val name: String,
|
||||
val role: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReadProgressUpdateDto(
|
||||
val lastBookRead: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReadListDto(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val bookIds: List<String>,
|
||||
val createdDate: String,
|
||||
val lastModifiedDate: String,
|
||||
val filtered: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReadProgressDto(
|
||||
val booksCount: Int,
|
||||
val booksReadCount: Int,
|
||||
val booksUnreadCount: Int,
|
||||
val booksInProgressCount: Int,
|
||||
val lastReadContinuousIndex: Int,
|
||||
)
|
||||
@@ -9,17 +9,19 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.toMangaInfo
|
||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MdList(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
private val mdex by lazy { MdUtil.getEnabledMangaDex() }
|
||||
private val mdex by lazy { MdUtil.getEnabledMangaDex(Injekt.get()) }
|
||||
|
||||
@StringRes
|
||||
override fun nameRes(): Int = R.string.mdlist
|
||||
@@ -43,83 +45,88 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
override fun displayScore(track: Track) = track.score.toInt().toString()
|
||||
|
||||
override suspend fun add(track: Track): Track = update(track)
|
||||
|
||||
override suspend fun update(track: Track): Track {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
return withIOContext {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
|
||||
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
|
||||
val followStatus = FollowStatus.fromInt(track.status)
|
||||
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
|
||||
val followStatus = FollowStatus.fromInt(track.status)
|
||||
|
||||
// this updates the follow status in the metadata
|
||||
// allow follow status to update
|
||||
if (remoteTrack.status != followStatus.int) {
|
||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
|
||||
remoteTrack.status = followStatus.int
|
||||
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
|
||||
}
|
||||
|
||||
if (track.score.toInt() > 0) {
|
||||
mdex.updateRating(track)
|
||||
}
|
||||
|
||||
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
|
||||
|
||||
if (followStatus != FollowStatus.UNFOLLOWED) {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = FollowStatus.COMPLETED.int
|
||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), FollowStatus.COMPLETED)
|
||||
}
|
||||
if (followStatus == FollowStatus.PLAN_TO_READ && track.last_chapter_read > 0) {
|
||||
val newFollowStatus = FollowStatus.READING
|
||||
track.status = FollowStatus.READING.int
|
||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus)
|
||||
remoteTrack.status = newFollowStatus.int
|
||||
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
|
||||
// this updates the follow status in the metadata
|
||||
// allow follow status to update
|
||||
if (remoteTrack.status != followStatus.int) {
|
||||
if (mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)) {
|
||||
remoteTrack.status = followStatus.int
|
||||
} else {
|
||||
track.status = remoteTrack.status
|
||||
}
|
||||
}
|
||||
|
||||
mdex.updateReadingProgress(track)
|
||||
} else if (track.last_chapter_read != 0) {
|
||||
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
|
||||
track.last_chapter_read = 0
|
||||
/*if (track.score.toInt() > 0) {
|
||||
mdex.updateRating(track)
|
||||
}
|
||||
|
||||
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
|
||||
|
||||
if (followStatus != FollowStatus.UNFOLLOWED) {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = FollowStatus.COMPLETED.int
|
||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), FollowStatus.COMPLETED)
|
||||
}
|
||||
if (followStatus == FollowStatus.PLAN_TO_READ && track.last_chapter_read > 0) {
|
||||
val newFollowStatus = FollowStatus.READING
|
||||
track.status = FollowStatus.READING.int
|
||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus)
|
||||
remoteTrack.status = newFollowStatus.int
|
||||
}
|
||||
|
||||
mdex.updateReadingProgress(track)
|
||||
} else if (track.last_chapter_read != 0) {
|
||||
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
|
||||
track.last_chapter_read = 0
|
||||
}*/
|
||||
track
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int
|
||||
|
||||
override suspend fun bind(track: Track): Track = update(refresh(track))
|
||||
override suspend fun bind(track: Track): Track = update(refresh(track).also { if (it.status == FollowStatus.UNFOLLOWED.int) it.status = FollowStatus.READING.int })
|
||||
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
|
||||
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
|
||||
return withIOContext {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
/*if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
|
||||
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
|
||||
}*/
|
||||
track
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track {
|
||||
val track = Track.create(TrackManager.MDLIST)
|
||||
track.manga_id = dbManga.id!!
|
||||
track.status = FollowStatus.UNFOLLOWED.int
|
||||
track.tracking_url = MdUtil.baseUrl + mdManga.url
|
||||
track.title = mdManga.title
|
||||
return track
|
||||
return Track.create(TrackManager.MDLIST).apply {
|
||||
manga_id = dbManga.id!!
|
||||
status = FollowStatus.UNFOLLOWED.int
|
||||
tracking_url = MdUtil.baseUrl + mdManga.url
|
||||
title = mdManga.title
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
return mdex.fetchSearchManga(0, query, mdex.getFilterList())
|
||||
.flatMap { page ->
|
||||
runAsObservable({
|
||||
page.mangas.map {
|
||||
toTrackSearch(mdex.getMangaDetails(it.toMangaInfo()))
|
||||
}
|
||||
})
|
||||
}
|
||||
.awaitSingle()
|
||||
return withIOContext {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
mdex.fetchSearchManga(0, query, mdex.getFilterList())
|
||||
.flatMap { page ->
|
||||
runAsObservable({
|
||||
page.mangas.map {
|
||||
toTrackSearch(mdex.getMangaDetails(it.toMangaInfo()))
|
||||
}
|
||||
})
|
||||
}
|
||||
.awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toTrackSearch(mangaInfo: MangaInfo): TrackSearch = TrackSearch.create(TrackManager.MDLIST).apply {
|
||||
|
||||
@@ -66,7 +66,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override suspend fun add(track: Track): Track {
|
||||
private suspend fun add(track: Track): Track {
|
||||
track.status = READING
|
||||
track.score = 0F
|
||||
return api.updateItem(track)
|
||||
|
||||
+3
-8
@@ -11,9 +11,6 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private var oauth: OAuth? = null
|
||||
set(value) {
|
||||
field = value?.copy(expires_in = System.currentTimeMillis() + (value.expires_in * 1000))
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
@@ -24,21 +21,19 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
||||
if (oauth == null) {
|
||||
oauth = myanimelist.loadOAuth()
|
||||
}
|
||||
// Refresh access token if null or expired.
|
||||
if (oauth!!.isExpired()) {
|
||||
// Refresh access token if expired
|
||||
if (oauth != null && oauth!!.isExpired()) {
|
||||
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
|
||||
if (it.isSuccessful) {
|
||||
setAuth(json.decodeFromString(it.body!!.string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Throw on null auth.
|
||||
if (oauth == null) {
|
||||
throw Exception("No authentication token")
|
||||
}
|
||||
|
||||
// Add the authorization header to the original request.
|
||||
// Add the authorization header to the original request
|
||||
val authRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||
.build()
|
||||
|
||||
@@ -7,8 +7,9 @@ data class OAuth(
|
||||
val refresh_token: String,
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val created_at: Long = System.currentTimeMillis(),
|
||||
val expires_in: Long
|
||||
) {
|
||||
|
||||
fun isExpired() = System.currentTimeMillis() > expires_in
|
||||
fun isExpired() = System.currentTimeMillis() > created_at + (expires_in * 1000)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override suspend fun add(track: Track): Track {
|
||||
private suspend fun add(track: Track): Track {
|
||||
return api.addLibManga(track, getUsername())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
@@ -11,52 +8,26 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
Worker(context, workerParams) {
|
||||
|
||||
override fun doWork(): Result {
|
||||
return runBlocking {
|
||||
try {
|
||||
val result = GithubUpdateChecker().checkForUpdate()
|
||||
override fun doWork() = runBlocking {
|
||||
try {
|
||||
val result = GithubUpdateChecker().checkForUpdate()
|
||||
|
||||
if (result is UpdateResult.NewUpdate<*>) {
|
||||
val url = result.release.downloadLink
|
||||
|
||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
|
||||
}
|
||||
|
||||
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
setContentText(context.getString(R.string.update_check_notification_update_available))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
// Download action
|
||||
addAction(
|
||||
android.R.drawable.stat_sys_download_done,
|
||||
context.getString(R.string.action_download),
|
||||
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
}
|
||||
}
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Result.failure()
|
||||
if (result is UpdateResult.NewUpdate<*>) {
|
||||
UpdaterNotifier(context).promptUpdate(result.release.downloadLink)
|
||||
}
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
|
||||
block()
|
||||
context.notificationManager.notify(Notifications.ID_UPDATER, build())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "UpdateChecker"
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
@@ -28,6 +30,27 @@ internal class UpdaterNotifier(private val context: Context) {
|
||||
context.notificationManager.notify(id, build())
|
||||
}
|
||||
|
||||
fun promptUpdate(url: String) {
|
||||
val intent = Intent(context, UpdaterService::class.java).apply {
|
||||
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
with(notificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
setContentText(context.getString(R.string.update_check_notification_update_available))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setContentIntent(pendingIntent)
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
android.R.drawable.stat_sys_download_done,
|
||||
context.getString(R.string.action_download),
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
notificationBuilder.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when apk download starts.
|
||||
*
|
||||
@@ -63,19 +86,20 @@ internal class UpdaterNotifier(private val context: Context) {
|
||||
* @param uri path location of apk.
|
||||
*/
|
||||
fun onDownloadFinished(uri: Uri) {
|
||||
val installIntent = NotificationHandler.installApkPendingActivity(context, uri)
|
||||
with(notificationBuilder) {
|
||||
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setOnlyAlertOnce(false)
|
||||
setProgress(0, 0, false)
|
||||
// Install action
|
||||
setContentIntent(NotificationHandler.installApkPendingActivity(context, uri))
|
||||
setContentIntent(installIntent)
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
R.drawable.ic_system_update_alt_white_24dp,
|
||||
context.getString(R.string.action_install),
|
||||
NotificationHandler.installApkPendingActivity(context, uri)
|
||||
installIntent
|
||||
)
|
||||
// Cancel action
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.getString(R.string.action_cancel),
|
||||
@@ -96,13 +120,13 @@ internal class UpdaterNotifier(private val context: Context) {
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
setOnlyAlertOnce(false)
|
||||
setProgress(0, 0, false)
|
||||
// Retry action
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
R.drawable.ic_refresh_24dp,
|
||||
context.getString(R.string.action_retry),
|
||||
UpdaterService.downloadApkPendingService(context, url)
|
||||
)
|
||||
// Cancel action
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.getString(R.string.action_cancel),
|
||||
|
||||
@@ -32,8 +32,8 @@ class GithubUpdateChecker {
|
||||
.parseAs<GithubRelease>()
|
||||
.let {
|
||||
// Check if latest version is different from current version
|
||||
if (/* SY --> */ isNewVersionSY(it.version) /* SY <-- */) {
|
||||
GithubUpdateResult.NewUpdate(it)
|
||||
if (/* SY --> */ isNewVersionSY(it.version) /* SY <-- */) {
|
||||
GithubUpdateResult.NewUpdate(it)
|
||||
} else {
|
||||
GithubUpdateResult.NoNewUpdate()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.extension
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.elvishew.xlog.XLog
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@@ -19,6 +18,7 @@ import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.log.xLogD
|
||||
import exh.source.BlacklistedSources
|
||||
import exh.source.EH_SOURCE_ID
|
||||
import exh.source.EXH_SOURCE_ID
|
||||
@@ -156,15 +156,15 @@ class ExtensionManager(
|
||||
// EXH -->
|
||||
private fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> {
|
||||
val blacklistEnabled = preferences.enableSourceBlacklist().get()
|
||||
return filter {
|
||||
if (it.isBlacklisted(blacklistEnabled)) {
|
||||
XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: %s, pkgName: %s)!", it.name, it.pkgName)
|
||||
false
|
||||
} else true
|
||||
return filterNot { extension ->
|
||||
extension.isBlacklisted(blacklistEnabled)
|
||||
.also {
|
||||
if (it) this@ExtensionManager.xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Extension.isBlacklisted(blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()): Boolean {
|
||||
private fun Extension.isBlacklisted(blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()): Boolean {
|
||||
return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled
|
||||
}
|
||||
// EXH <--
|
||||
@@ -333,7 +333,7 @@ class ExtensionManager(
|
||||
private fun registerNewExtension(extension: Extension.Installed) {
|
||||
// SY -->
|
||||
if (extension.isBlacklisted()) {
|
||||
XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName)
|
||||
xLogD("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName)
|
||||
return
|
||||
}
|
||||
// SY <--
|
||||
@@ -351,7 +351,7 @@ class ExtensionManager(
|
||||
private fun registerUpdatedExtension(extension: Extension.Installed) {
|
||||
// SY -->
|
||||
if (extension.isBlacklisted()) {
|
||||
XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName)
|
||||
xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName)
|
||||
return
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@@ -32,7 +32,7 @@ internal class ExtensionGithubApi {
|
||||
.let { parseResponse(it) }
|
||||
} /* SY --> */ + preferences.extensionRepos().get().flatMap { repoPath ->
|
||||
val url = "$BASE_URL$repoPath/repo/"
|
||||
networkService.client
|
||||
networkService.client
|
||||
.newCall(GET("${url}index.min.json"))
|
||||
.await()
|
||||
.parseAs<JsonArray>()
|
||||
|
||||
@@ -163,7 +163,7 @@ internal object ExtensionLoader {
|
||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Extension load error: $extName.")
|
||||
Timber.e(e, "Extension load error: $extName ($it)")
|
||||
return LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
import java.net.InetAddress
|
||||
|
||||
/**
|
||||
* Based on https://github.com/square/okhttp/blob/ef5d0c83f7bbd3a0c0534e7ca23cbc4ee7550f3b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.java
|
||||
*/
|
||||
|
||||
const val PREF_DOH_CLOUDFLARE = 1
|
||||
const val PREF_DOH_GOOGLE = 2
|
||||
|
||||
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
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()
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.dohGoogle() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://dns.google/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("8.8.4.4"),
|
||||
InetAddress.getByName("8.8.8.8")
|
||||
)
|
||||
.build()
|
||||
)
|
||||
@@ -1,16 +1,16 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import coil.util.CoilUtils
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||
import okhttp3.Cache
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) {
|
||||
@@ -21,46 +21,34 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||
|
||||
/* SY --> */ open /* SY <-- */ val cookieManager = AndroidCookieJar()
|
||||
/* SY --> */ open /* SY <-- */val cookieManager = AndroidCookieJar()
|
||||
|
||||
/* SY --> */ open /* SY <-- */ val client by lazy {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.cookieJar(cookieManager)
|
||||
.cache(Cache(cacheDir, cacheSize))
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(UserAgentInterceptor())
|
||||
private val baseClientBuilder: OkHttpClient.Builder
|
||||
get() {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.cookieJar(cookieManager)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(UserAgentInterceptor())
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
if (BuildConfig.DEBUG) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
}
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
|
||||
when (preferences.dohProvider()) {
|
||||
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
/* SY --> */ open /* SY <-- */val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
|
||||
|
||||
builder.build()
|
||||
}
|
||||
val coilClient by lazy { baseClientBuilder.cache(CoilUtils.createDefaultCache(context)).build() }
|
||||
|
||||
/* SY --> */ open /* SY <-- */val cloudflareClient by lazy {
|
||||
client.newBuilder()
|
||||
|
||||
@@ -9,6 +9,7 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import rx.Observable
|
||||
import rx.Producer
|
||||
import rx.Subscription
|
||||
@@ -70,7 +71,9 @@ suspend fun Call.await(): Response {
|
||||
return
|
||||
}
|
||||
|
||||
continuation.resume(response)
|
||||
continuation.resume(response) {
|
||||
response.body?.closeQuietly()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
|
||||
+5
-7
@@ -1,14 +1,14 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
@@ -114,10 +114,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
// HTTP error codes are only received since M
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
url == origRequestUrl && !challengeFound
|
||||
) {
|
||||
if (url == origRequestUrl && !challengeFound) {
|
||||
// The first request didn't return the challenge, abort.
|
||||
latch.countDown()
|
||||
}
|
||||
@@ -156,6 +153,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
|
||||
webView?.stopLoading()
|
||||
webView?.destroy()
|
||||
webView = null
|
||||
}
|
||||
|
||||
// Throw exception if we failed to bypass Cloudflare
|
||||
@@ -171,6 +169,6 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
|
||||
companion object {
|
||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||
private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance")
|
||||
private val COOKIE_NAMES = listOf("cf_clearance")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.os.SystemClock
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* An OkHttp interceptor that handles rate limiting.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* permits = 5, period = 1, unit = seconds => 5 requests per second
|
||||
* permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes
|
||||
*
|
||||
* @param permits {Int} Number of requests allowed within a period of units.
|
||||
* @param period {Long} The limiting duration. Defaults to 1.
|
||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||
*/
|
||||
class RateLimitInterceptor(
|
||||
private val permits: Int,
|
||||
private val period: Long = 1,
|
||||
private val unit: TimeUnit = TimeUnit.SECONDS
|
||||
) : Interceptor {
|
||||
|
||||
private val requestQueue = ArrayList<Long>(permits)
|
||||
private val rateLimitMillis = unit.toMillis(period)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
synchronized(requestQueue) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val waitTime = if (requestQueue.size < permits) {
|
||||
0
|
||||
} else {
|
||||
val oldestReq = requestQueue[0]
|
||||
val newestReq = requestQueue[permits - 1]
|
||||
|
||||
if (newestReq - oldestReq > rateLimitMillis) {
|
||||
0
|
||||
} else {
|
||||
oldestReq + rateLimitMillis - now // Remaining time
|
||||
}
|
||||
}
|
||||
|
||||
if (requestQueue.size == permits) {
|
||||
requestQueue.removeAt(0)
|
||||
}
|
||||
if (waitTime > 0) {
|
||||
requestQueue.add(now + waitTime)
|
||||
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
|
||||
} else {
|
||||
requestQueue.add(now)
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.os.SystemClock
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* An OkHttp interceptor that handles given url host's rate limiting.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
|
||||
* httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
|
||||
*
|
||||
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
|
||||
* @param permits {Int} Number of requests allowed within a period of units.
|
||||
* @param period {Long} The limiting duration. Defaults to 1.
|
||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||
*/
|
||||
class SpecificHostRateLimitInterceptor(
|
||||
private val httpUrl: HttpUrl,
|
||||
private val permits: Int,
|
||||
private val period: Long = 1,
|
||||
private val unit: TimeUnit = TimeUnit.SECONDS
|
||||
) : Interceptor {
|
||||
|
||||
private val requestQueue = ArrayList<Long>(permits)
|
||||
private val rateLimitMillis = unit.toMillis(period)
|
||||
private val host = httpUrl.host
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
if (chain.request().url.host != host) {
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
synchronized(requestQueue) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val waitTime = if (requestQueue.size < permits) {
|
||||
0
|
||||
} else {
|
||||
val oldestReq = requestQueue[0]
|
||||
val newestReq = requestQueue[permits - 1]
|
||||
|
||||
if (newestReq - oldestReq > rateLimitMillis) {
|
||||
0
|
||||
} else {
|
||||
oldestReq + rateLimitMillis - now // Remaining time
|
||||
}
|
||||
}
|
||||
|
||||
if (requestQueue.size == permits) {
|
||||
requestQueue.removeAt(0)
|
||||
}
|
||||
if (waitTime > 0) {
|
||||
requestQueue.add(now + waitTime)
|
||||
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
|
||||
} else {
|
||||
requestQueue.add(now)
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import okhttp3.Interceptor
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user