Compare commits
464 Commits
Author | SHA1 | Date | |
---|---|---|---|
f590378761 | |||
f5f592be91 | |||
7a373fb43a | |||
aded11e599 | |||
41d7cee020 | |||
f2ef6a20e6 | |||
a398c3fb81 | |||
2a454b44cc | |||
7b66ece895 | |||
b5017eebbf | |||
aa67229daf | |||
5af68186d6 | |||
545bc0e605 | |||
291168f4de | |||
9facb51f22 | |||
5b7d8c5e37 | |||
5945937e4b | |||
9f9f9872eb | |||
3566072f4a | |||
b85cd86b24 | |||
79c3767fff | |||
cf1609a429 | |||
3aeac7e7b5 | |||
1557f713f4 | |||
b63d24ac1a | |||
348c1ff29d | |||
717e55497f | |||
d84b5e8b46 | |||
5f9ddf9ff5 | |||
bbee093c63 | |||
e8c35ae4e1 | |||
1607658c30 | |||
2e9ef373f3 | |||
ec6eef6d37 | |||
45a19d15ec | |||
7191552126 | |||
cfa07490e5 | |||
ae40990eb9 | |||
9f2fe33ce0 | |||
33660de6b1 | |||
13d25e0849 | |||
6662e2002f | |||
d4081dc899 | |||
62dffb8226 | |||
cb6aa18480 | |||
d5cfbef42b | |||
535abcbb8b | |||
c34b548a3e | |||
9bf452856c | |||
17109ab760 | |||
6bc6e1a1d1 | |||
7eef4f7fbf | |||
75bec6a8e3 | |||
0a10f66053 | |||
58860b51a2 | |||
3ee652b61a | |||
426ed7308b | |||
0ecfef3f70 | |||
5f7e34b6a1 | |||
34cb24fe34 | |||
1490112135 | |||
c4716a3f4c | |||
0a54901eb0 | |||
fea2e0a265 | |||
d3c087375b | |||
a93c0577ac | |||
e4dc35674d | |||
8a668ba7b9 | |||
ee9a68b040 | |||
78e8d40649 | |||
660e13b701 | |||
0685382083 | |||
04a993c997 | |||
7cae3095c4 | |||
e288bf902b | |||
a083e1f71a | |||
86b9d7e843 | |||
628bd5d6b4 | |||
00285a782c | |||
16be469ecb | |||
fdcbc4cffa | |||
fc548304cf | |||
7c7ff8165e | |||
496a476c13 | |||
441fc6e45b | |||
cf7ec6aa76 | |||
db2dd4b6c6 | |||
a68417a0b0 | |||
2a5102a457 | |||
837d8f5f30 | |||
1a5858e99b | |||
4044427d93 | |||
f667f85fa5 | |||
5cddc0c387 | |||
cbc01dd6f1 | |||
b820c7debf | |||
2bee072cba | |||
80710b0b94 | |||
3319ccfd41 | |||
878008e93b | |||
0cd551d4fd | |||
f85194ec46 | |||
271489bdfd | |||
bd5f22a049 | |||
189f18b112 | |||
df166184ea | |||
ce42cba096 | |||
9670863a41 | |||
1ae52bd33f | |||
c9cf9cfff0 | |||
2ffbee3db2 | |||
96b8beb9cd | |||
365b849046 | |||
8e613d03e3 | |||
b18a794eca | |||
c620c924f9 | |||
9db81a5a49 | |||
6fb7a85e8a | |||
36f81b4a62 | |||
2caecc01b2 | |||
dedb8d2d68 | |||
7192b26402 | |||
762f5bdc33 | |||
bebb52b4e8 | |||
2c9f8bb9ce | |||
efbefabb01 | |||
990fb22d3e | |||
9b2c22b2d9 | |||
df7e0d2f2f | |||
5cfda1b1bf | |||
ac9bf1f3ff | |||
7eb0868791 | |||
8a792e6d76 | |||
d8a3692d92 | |||
95ce0e39ef | |||
17b70ab38c | |||
07e76f35fa | |||
a4cab9876a | |||
c06a932c95 | |||
7d713b87b1 | |||
b1167146c5 | |||
2d0a5eb02c | |||
8d68859c2a | |||
444cefc9a2 | |||
d0deceabbd | |||
175c1df0b8 | |||
9cc6491c2a | |||
710179f4b4 | |||
d11c72fd48 | |||
0af505828e | |||
135cf9960f | |||
3bf7c74f93 | |||
cea4911c4d | |||
54dc01253d | |||
4db9a90da2 | |||
d69e9034ab | |||
71ece73d99 | |||
3bb2102eb4 | |||
b7914909d0 | |||
63398fe491 | |||
bf32bf28da | |||
dcb6bfb18d | |||
8f605dc0f6 | |||
47e770948b | |||
9ab29f5b7f | |||
10bf430ce6 | |||
67eb4e8180 | |||
141f9b7730 | |||
139a589ad6 | |||
591873a185 | |||
97a308b114 | |||
430714e67f | |||
a49adbd09c | |||
3df98d576e | |||
8135136c86 | |||
cef1c4b8a1 | |||
2e8791a101 | |||
0e2b8b10d1 | |||
3cb64669e4 | |||
bc0d32f330 | |||
0db17beacc | |||
931efed784 | |||
6378a41b6d | |||
23bf7faf9f | |||
01ff3af63f | |||
8f98055e9e | |||
84ae61f72c | |||
6dd280205b | |||
1365d553a4 | |||
61a594493c | |||
62ab70f889 | |||
eaccfdde59 | |||
a8e536478c | |||
e94d5626dd | |||
be3e31ddc4 | |||
b92b6520cb | |||
ea33179a95 | |||
6fcf6ae1f5 | |||
f2a9247b68 | |||
dc3ed7fffc | |||
271de31d51 | |||
1268caf3e0 | |||
c0cef58e39 | |||
d363d205c3 | |||
2fd5a9e883 | |||
e7ef974a39 | |||
0b62fa8b76 | |||
2d28750782 | |||
e2054a0ab7 | |||
7ae5c3b2e7 | |||
6e7fefb8b2 | |||
450bef278b | |||
0affc0d58b | |||
3d153b6c8e | |||
04fff91e23 | |||
28a23452f2 | |||
6d403851cf | |||
395a749bce | |||
2cc2a90941 | |||
c87ba6231d | |||
c5ca739b49 | |||
00fe4cdf2d | |||
69be3e1e87 | |||
2cb3984d68 | |||
5901978889 | |||
8bf1cf3cc5 | |||
f6af1184bc | |||
4880741b8b | |||
e8627800fe | |||
907fbb94a2 | |||
fd2028557e | |||
91fa1ec6b2 | |||
628c525599 | |||
bbc00768f0 | |||
5b09461ccf | |||
1a439ecece | |||
836aec4396 | |||
0b5dec9bab | |||
fd56123267 | |||
45ca470789 | |||
a3b1690d38 | |||
a3bad75899 | |||
d1aaafbfff | |||
93d4af99bf | |||
c950595fe3 | |||
8ffd3a8ed2 | |||
b6e246c6b2 | |||
59859e124f | |||
2bb7a33bc3 | |||
c2b8fea291 | |||
08ab7f6aa0 | |||
560f0bba5c | |||
722437a022 | |||
8a44b1dabe | |||
b39191ff50 | |||
9814d20404 | |||
6664dfb048 | |||
3133a63cf8 | |||
c9c0f3d014 | |||
ff66f307dd | |||
e048d66f74 | |||
66e3fa7df8 | |||
019a0f31c7 | |||
749c2071af | |||
322d66d282 | |||
aa98cd0da0 | |||
c8316c7254 | |||
6b9180844d | |||
c0e4863229 | |||
2be9871d05 | |||
776f6a9a16 | |||
10163aab21 | |||
60b2a4ea9d | |||
56e1e3e205 | |||
0f805cd45e | |||
1d7c692e89 | |||
38bc8ec6b4 | |||
2154e3aa2d | |||
56c19e57a9 | |||
d548c690d6 | |||
3fa70dade3 | |||
368c30a2cc | |||
5539e4591f | |||
781971ee81 | |||
1140316d1b | |||
cf6c48744a | |||
eed6db8e92 | |||
858664bfd7 | |||
eceac4d6e3 | |||
47a172df1f | |||
f2c0732c40 | |||
682fae12b6 | |||
a150762c63 | |||
2695bdddf8 | |||
c9b1a425a7 | |||
122b2b1a8e | |||
2351c1b426 | |||
558c4ada06 | |||
779fd9c61a | |||
0b5128dfd1 | |||
c0519e8670 | |||
fd545db1bd | |||
6991c224b2 | |||
7dc70c9eab | |||
e32445f2cf | |||
8aa6486bf7 | |||
d21c147203 | |||
9b10e851d1 | |||
6675508b24 | |||
7310ec4fe4 | |||
b1ce3693ed | |||
deb1ed5623 | |||
0902d7cca9 | |||
95ec903862 | |||
2ab6af6471 | |||
9493577de2 | |||
837ce62844 | |||
2860bbfb12 | |||
a2b1acd70f | |||
f1350bc33e | |||
6af0eb4068 | |||
538c168641 | |||
832a4fa68e | |||
ccb727529d | |||
ed41604f56 | |||
9f05d563f9 | |||
72d114d46a | |||
99b96d80d0 | |||
67418ba853 | |||
60755d0c26 | |||
a689e4e041 | |||
f5aa36c787 | |||
f8d82cb052 | |||
980feb6c96 | |||
e7d6605490 | |||
7a476abb53 | |||
19581792fc | |||
aadc6a56cd | |||
b88e444cbc | |||
6c792d2821 | |||
9afb445620 | |||
efc951191d | |||
a249373bf5 | |||
e1eb030b18 | |||
6aea0f48ed | |||
e637f22540 | |||
842295348e | |||
2992a0f4d8 | |||
e8f5963a57 | |||
4cbe497770 | |||
76a53097b1 | |||
0904692f15 | |||
65bacd288b | |||
11ab3b2c2e | |||
812368e332 | |||
cf39ae0000 | |||
7194f65203 | |||
4b78ff324d | |||
79ccfcd553 | |||
2df6a4dde8 | |||
e88cbc2769 | |||
25d1c40cda | |||
969b57ade9 | |||
bddeb86223 | |||
b5986b509e | |||
9d2adcd512 | |||
fb3756420b | |||
2eab43a669 | |||
caeab0a63b | |||
7c69b1b649 | |||
3784d1a8f2 | |||
458e761b45 | |||
371b0b2132 | |||
5d1ca64768 | |||
972a595c74 | |||
a3c598a3e1 | |||
9ce8c5c160 | |||
79bbc99882 | |||
31867362dc | |||
3bce07e873 | |||
766f9e37b5 | |||
a9bed90d02 | |||
01ad405dd2 | |||
8cef35f4a0 | |||
274f0edd76 | |||
88aea311f8 | |||
477aedbffa | |||
b898442fe3 | |||
528c1b90c2 | |||
205c1e5170 | |||
004e1c98ee | |||
7641bb4d0d | |||
687f3d48ea | |||
da5f10a2f1 | |||
64050e8266 | |||
791a7d5a01 | |||
13930d3706 | |||
2769e27a2a | |||
76c795d0d0 | |||
b20bced3ca | |||
4f2da9a78f | |||
8e0ba3650b | |||
76f6fe4601 | |||
ca1373f36b | |||
fc6c2e083d | |||
c0789cd6ba | |||
af47103707 | |||
c466baaa25 | |||
670294a427 | |||
21ddae6a86 | |||
9f260c3513 | |||
18061d1077 | |||
381c061ebc | |||
d37341d7d0 | |||
a5098e5b5b | |||
b55d394a1f | |||
5e2e177aa9 | |||
86e59977de | |||
66baf01e43 | |||
7a33e198dc | |||
4b493ebbaf | |||
565e8cf00b | |||
738a3999b4 | |||
6f4b84c8fb | |||
29ab99aa1f | |||
d53719b79e | |||
f10fe8bf02 | |||
d7cfe1990c | |||
8bedc8f456 | |||
d9000f6fd1 | |||
50c7b32b00 | |||
78072ad285 | |||
437a34b5dc | |||
3ebea4c305 | |||
8fe315c354 | |||
b10b13a339 | |||
5b5ea5ab8a | |||
1a230b3900 | |||
e90b0aaf8b | |||
fe7c7e72f5 | |||
9ba11a585f | |||
9920ff617b | |||
3f1355c413 | |||
4929e66ecc | |||
02e370c2d8 | |||
4c31e3fc5f | |||
15f49b39b8 | |||
8c82b766e3 | |||
4c8665c9f0 | |||
ba67781431 | |||
4ef25c75b7 | |||
3aafc671f8 | |||
967df6f7a3 | |||
22518f173f | |||
64bdfabbd8 | |||
c8c65ab7b1 | |||
19cd28b66b | |||
4a136ef2aa | |||
9e7a53cb90 | |||
19a7f37efa | |||
c3084ac43a | |||
159146e197 | |||
67ddf4a5b8 | |||
4b9b53a9b8 |
33
.github/CONTRIBUTING.md
vendored
@ -1,33 +0,0 @@
|
|||||||
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
|
||||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
|
||||||
3. What is your type of issue?
|
|
||||||
* [Catalogue request](#catalogue-requests)
|
|
||||||
* [Bugs](#bugs)
|
|
||||||
* [Feature requests](#feature-requests)
|
|
||||||
* [Translations](https://tachiyomi.org/help/contribution/#translation)
|
|
||||||
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
# Catalogue requests
|
|
||||||
|
|
||||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
|
||||||
|
|
||||||
# Bugs
|
|
||||||
* Include version (More > About > Version)
|
|
||||||
* If not latest, try updating, it may have already been solved
|
|
||||||
* Preview version is equal to the number of commits as seen in the main page
|
|
||||||
* Include steps to reproduce (if not obvious from description)
|
|
||||||
* Include screenshot (if needed)
|
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
|
||||||
* For large logs use http://pastebin.com/ (or similar)
|
|
||||||
* Don't group unrelated requests into one issue
|
|
||||||
|
|
||||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
|
||||||
|
|
||||||
DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
|
||||||
|
|
||||||
# Feature requests
|
|
||||||
|
|
||||||
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
|
||||||
* Include screenshot (if needed)
|
|
4
.github/ISSUE_TEMPLATE.md
vendored
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.10.4)
|
- I have updated to the latest version of the app (stable is v0.10.9)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
|
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -9,9 +9,9 @@ labels: "bug"
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.10.4)
|
- I have updated to the latest version of the app (stable is v0.10.9)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -4,5 +4,5 @@ contact_links:
|
|||||||
url: https://tachiyomi.org/help/
|
url: https://tachiyomi.org/help/
|
||||||
about: Common questions are answered here.
|
about: Common questions are answered here.
|
||||||
- name: Tachiyomi extensions GitHub repository
|
- name: Tachiyomi extensions GitHub repository
|
||||||
url: https://github.com/inorichi/tachiyomi-extensions
|
url: https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
about: Issues about an extension/source/catalogue should be opened here instead.
|
about: Issues about an extension/source/catalogue should be opened here instead.
|
||||||
|
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -9,9 +9,9 @@ labels: "feature"
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.10.4)
|
- I have updated to the latest version of the app (stable is v0.10.9)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
|
6
.github/ISSUE_TEMPLATE/source_issue.md
vendored
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: "Extension/source/catalogue issue"
|
name: "Extension/source/catalogue issue"
|
||||||
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
|
about: "Do not open an issue here. See https://github.com/tachiyomiorg/tachiyomi-extensions"
|
||||||
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
|
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/tachiyomiorg/tachiyomi-extensions"
|
||||||
labels: "catalog, invalid"
|
labels: "catalog, invalid"
|
||||||
---
|
---
|
||||||
|
|
||||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
|
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
BIN
.github/readme-images/app-icon.png
vendored
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
6
.github/runner-files/ci-gradle.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
org.gradle.daemon=false
|
||||||
|
org.gradle.jvmargs=-Xmx5120m
|
||||||
|
org.gradle.workers.max=2
|
||||||
|
|
||||||
|
kotlin.incremental=false
|
||||||
|
kotlin.compiler.execution.strategy=in-process
|
95
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_wrapper:
|
||||||
|
name: Validate Gradle Wrapper
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Clone repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Validate Gradle Wrapper
|
||||||
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build app
|
||||||
|
needs: check_wrapper
|
||||||
|
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Cancel previous runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.5.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Clone repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up JDK 11
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 11
|
||||||
|
|
||||||
|
- name: Copy CI gradle.properties
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.gradle
|
||||||
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
|
- name: Build app
|
||||||
|
uses: eskatos/gradle-command-action@v1
|
||||||
|
with:
|
||||||
|
arguments: assembleStandardRelease
|
||||||
|
wrapper-cache-enabled: true
|
||||||
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
|
# Sign APK and create release for tags
|
||||||
|
|
||||||
|
- name: Get tag name
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
|
id: get_tag_name
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Sign APK
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
|
uses: r0adkll/sign-android-release@v1
|
||||||
|
with:
|
||||||
|
releaseDirectory: app/build/outputs/apk/standard/release
|
||||||
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.ALIAS }}
|
||||||
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ env.VERSION_TAG }}
|
||||||
|
release_name: Tachiyomi ${{ env.VERSION_TAG }}
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: Upload APK to release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ${{ env.SIGNED_RELEASE_FILE }}
|
||||||
|
asset_name: tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||||
|
asset_content_type: application/vnd.android.package-archive
|
7
.github/workflows/issue_closer.yml
vendored
@ -1,5 +1,8 @@
|
|||||||
name: Issue closer
|
name: Issue closer
|
||||||
on: [issues]
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, edited, reopened]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
autoclose:
|
autoclose:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -31,4 +34,4 @@ jobs:
|
|||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
type: body
|
type: body
|
||||||
regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*"
|
regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*"
|
||||||
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
|
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
|
||||||
|
81
.travis.yml
@ -1,81 +0,0 @@
|
|||||||
dist: trusty
|
|
||||||
language: android
|
|
||||||
|
|
||||||
android:
|
|
||||||
components:
|
|
||||||
- tools
|
|
||||||
- platform-tools
|
|
||||||
- build-tools-29.0.3
|
|
||||||
- android-29
|
|
||||||
- extra-android-m2repository
|
|
||||||
- extra-google-m2repository
|
|
||||||
- extra-android-support
|
|
||||||
- extra-google-google_play_services
|
|
||||||
|
|
||||||
licenses:
|
|
||||||
- 'android-sdk-license-.+'
|
|
||||||
- 'android-sdk-preview-license-.+'
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- yes | sdkmanager "platforms;android-29" # workaround for accepting the license
|
|
||||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
|
||||||
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
|
|
||||||
tar xf secrets.tar;
|
|
||||||
mv debug.keystore "$HOME/.android";
|
|
||||||
fi
|
|
||||||
- mkdir "$ANDROID_HOME/licenses" || true
|
|
||||||
- echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license"
|
|
||||||
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
|
||||||
|
|
||||||
install:
|
|
||||||
- echo y | sdkmanager "ndk-bundle"
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle
|
|
||||||
|
|
||||||
script: ".travis/build.sh"
|
|
||||||
|
|
||||||
before_cache:
|
|
||||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
|
||||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
|
||||||
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- "$HOME/.gradle/caches/"
|
|
||||||
- "$HOME/.gradle/wrapper/"
|
|
||||||
- "$HOME/.android/build-cache"
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
- provider: releases
|
|
||||||
api_key:
|
|
||||||
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA=
|
|
||||||
file: tachiyomi-v*.apk
|
|
||||||
file_glob: true
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
repo: inorichi/tachiyomi
|
|
||||||
- provider: script
|
|
||||||
script: ".travis/deploy.sh"
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
branch: master
|
|
||||||
condition: "-z $TRAVIS_TAG"
|
|
||||||
repo: inorichi/tachiyomi
|
|
||||||
- provider: script
|
|
||||||
script: ".travis/deploy.sh"
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
branch: dev
|
|
||||||
condition: "-z $TRAVIS_TAG"
|
|
||||||
repo: inorichi/tachiyomi
|
|
||||||
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU=
|
|
||||||
- secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI=
|
|
||||||
- secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E=
|
|
||||||
- secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c=
|
|
||||||
- secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI=
|
|
||||||
- secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY=
|
|
||||||
- secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04=
|
|
@ -1,20 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
git fetch --unshallow #required for commit count
|
|
||||||
|
|
||||||
if [ -z "$TRAVIS_TAG" ]; then
|
|
||||||
./gradlew clean assembleStandardDebug
|
|
||||||
|
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
|
||||||
export ARTIFACT="tachiyomi-r${COMMIT_COUNT}.apk"
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/standard/debug/app-standard-debug.apk $ARTIFACT
|
|
||||||
else
|
|
||||||
./gradlew clean assembleStandardRelease
|
|
||||||
|
|
||||||
TOOLS="$(ls -d ${ANDROID_HOME}/build-tools/* | tail -1)"
|
|
||||||
export ARTIFACT="tachiyomi-${TRAVIS_TAG}.apk"
|
|
||||||
|
|
||||||
${TOOLS}/zipalign -v -p 4 app/build/outputs/apk/standard/release/app-standard-release-unsigned.apk app-aligned.apk
|
|
||||||
${TOOLS}/apksigner sign --ks $STORE_PATH --ks-key-alias $STORE_ALIAS --ks-pass env:STORE_PASS --key-pass env:KEY_PASS --out $ARTIFACT app-aligned.apk
|
|
||||||
fi
|
|
@ -1,15 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
pattern="tachiyomi-r*"
|
|
||||||
files=( $pattern )
|
|
||||||
export ARTIFACT="${files[0]}"
|
|
||||||
|
|
||||||
if [ -z "$ARTIFACT" ]; then
|
|
||||||
echo "Artifact not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export SSHOPTIONS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${DEPLOY_KEY}"
|
|
||||||
|
|
||||||
scp $SSHOPTIONS $ARTIFACT $DEPLOY_USER@$DEPLOY_HOST:builds/
|
|
||||||
ssh $SSHOPTIONS $DEPLOY_USER@$DEPLOY_HOST ln -sf $ARTIFACT builds/latest
|
|
34
CONTRIBUTING.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/tachiyomiorg/tachiyomi#issues-feature-requests-and-contributing).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Thanks for your interest in contributing to Tachiyomi!
|
||||||
|
|
||||||
|
|
||||||
|
# Code contributions
|
||||||
|
|
||||||
|
Pull requests are welcome!
|
||||||
|
|
||||||
|
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
||||||
|
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
|
||||||
|
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/help/contribution/#translation) for more details.
|
||||||
|
|
||||||
|
|
||||||
|
# Forks
|
||||||
|
|
||||||
|
Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/tachiyomiorg/tachiyomi/blob/master/LICENSE).
|
||||||
|
|
||||||
|
When creating a fork, remember to:
|
||||||
|
|
||||||
|
- To avoid confusion with the main app:
|
||||||
|
- Change the app name
|
||||||
|
- Change the app icon
|
||||||
|
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt)
|
||||||
|
- To avoid installation conflicts:
|
||||||
|
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||||
|
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||||
|
- If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/standard/google-services.json) with your own
|
||||||
|
- If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) with your own
|
26
LICENSE
@ -174,29 +174,3 @@
|
|||||||
of your accepting any such warranty or additional liability.
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright {yyyy} {name of copyright owner}
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
### r2903
|
||||||
|
- The MyAnimeList tracker was rewritten. You will need to log out and log in again.
|
||||||
|
|
||||||
### r1810
|
### r1810
|
||||||
- Background jobs were migrated to a new system. You may need to toggle the settings to ensure they
|
- Background jobs were migrated to a new system. You may need to toggle the settings to ensure they
|
||||||
run properly. This includes app updates, library updates, and automatic backups.
|
run properly. This includes app updates, library updates, and automatic backups.
|
||||||
|
21
README.md
@ -1,6 +1,6 @@
|
|||||||
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
||||||
|-------|----------|---------|------------|---------|
|
|-------|----------|---------|------------|---------|
|
||||||
| [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) | [](http://tachiyomi.kanade.eu/latest) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
|  | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
|
|
||||||
# Tachiyomi
|
# Tachiyomi
|
||||||
@ -11,7 +11,7 @@ Tachiyomi is a free and open source manga reader for Android 5.0 and above.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
Features include:
|
Features include:
|
||||||
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
||||||
* Local reading of downloaded manga
|
* Local reading of downloaded manga
|
||||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
||||||
@ -21,9 +21,9 @@ Features include:
|
|||||||
* Create backups locally to read offline or to your desired cloud service
|
* Create backups locally to read offline or to your desired cloud service
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases).
|
Get the app from our [releases page](https://github.com/tachiyomiorg/tachiyomi/releases).
|
||||||
|
|
||||||
If you want to try new features before they get to the stable release, you can download the preview version [here](http://tachiyomi.kanade.eu/latest).
|
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/tachiyomiorg/android-app-preview/releases).
|
||||||
|
|
||||||
## Issues, Feature Requests and Contributing
|
## Issues, Feature Requests and Contributing
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
|
|
||||||
<details><summary>Issues</summary>
|
<details><summary>Issues</summary>
|
||||||
|
|
||||||
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/tachiyomiorg/tachiyomi/releases) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
||||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@ -47,9 +47,9 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
* For large logs use http://pastebin.com/ (or similar)
|
* For large logs use http://pastebin.com/ (or similar)
|
||||||
* Don't group unrelated requests into one issue
|
* Don't group unrelated requests into one issue
|
||||||
|
|
||||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
||||||
|
|
||||||
DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@ -58,7 +58,12 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
|||||||
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
|
|
||||||
Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions, they do not belong in this repository.
|
Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions, they do not belong in this repository.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Contributing</summary>
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](https://github.com/tachiyomiorg/tachiyomi/blob/master/CONTRIBUTING.md).
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
321
app/build.gradle
@ -1,321 +0,0 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
|
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
apply plugin: 'kotlin-kapt'
|
|
||||||
apply plugin: 'com.github.zellius.shortcut-helper'
|
|
||||||
|
|
||||||
shortcutHelper.filePath = './shortcuts.xml'
|
|
||||||
|
|
||||||
ext {
|
|
||||||
// Git is needed in your system PATH for these commands to work.
|
|
||||||
// If it's not installed, you can return a random value as a workaround
|
|
||||||
getCommitCount = {
|
|
||||||
return 'git rev-list --count HEAD'.execute().text.trim()
|
|
||||||
// return "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
getGitSha = {
|
|
||||||
return 'git rev-parse --short HEAD'.execute().text.trim()
|
|
||||||
// return "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
getBuildTime = {
|
|
||||||
def df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
|
||||||
df.setTimeZone(TimeZone.getTimeZone("UTC"))
|
|
||||||
return df.format(new Date())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion AndroidConfig.compileSdk
|
|
||||||
buildToolsVersion AndroidConfig.buildTools
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "eu.kanade.tachiyomi"
|
|
||||||
minSdkVersion AndroidConfig.minSdk
|
|
||||||
targetSdkVersion AndroidConfig.targetSdk
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
versionCode 50
|
|
||||||
versionName "0.10.4"
|
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
|
||||||
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
|
||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "false"
|
|
||||||
|
|
||||||
multiDexEnabled true
|
|
||||||
|
|
||||||
ndk {
|
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding = true
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
versionNameSuffix "-${getCommitCount()}"
|
|
||||||
applicationIdSuffix ".debug"
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
postprocessing {
|
|
||||||
obfuscate false
|
|
||||||
optimizeCode true
|
|
||||||
removeUnusedCode false
|
|
||||||
removeUnusedResources true
|
|
||||||
proguardFiles 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions "default"
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
standard {
|
|
||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
|
||||||
dimension "default"
|
|
||||||
}
|
|
||||||
fdroid {
|
|
||||||
dimension "default"
|
|
||||||
}
|
|
||||||
dev {
|
|
||||||
resConfigs "en", "xxhdpi"
|
|
||||||
dimension "default"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
exclude 'META-INF/DEPENDENCIES'
|
|
||||||
exclude 'LICENSE.txt'
|
|
||||||
exclude 'META-INF/LICENSE'
|
|
||||||
exclude 'META-INF/LICENSE.txt'
|
|
||||||
exclude 'META-INF/NOTICE'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependenciesInfo {
|
|
||||||
includeInApk = false
|
|
||||||
}
|
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
disable 'MissingTranslation'
|
|
||||||
disable 'ExtraTranslation'
|
|
||||||
|
|
||||||
abortOnError false
|
|
||||||
checkReleaseBuilds false
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
androidExtensions {
|
|
||||||
experimental = true
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
|
|
||||||
// AndroidX libraries
|
|
||||||
implementation 'androidx.annotation:annotation:1.1.0'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
|
|
||||||
implementation 'androidx.biometric:biometric:1.0.1'
|
|
||||||
implementation 'androidx.browser:browser:1.2.0'
|
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
|
|
||||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
|
||||||
implementation 'androidx.core:core-ktx:1.4.0-alpha01'
|
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
|
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
|
||||||
|
|
||||||
final lifecycle_version = '2.3.0-alpha06'
|
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
|
||||||
|
|
||||||
// Job scheduling
|
|
||||||
final work_version = '2.4.0'
|
|
||||||
implementation "androidx.work:work-runtime:$work_version"
|
|
||||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
|
||||||
|
|
||||||
// UI library
|
|
||||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
|
||||||
|
|
||||||
standardImplementation 'com.google.firebase:firebase-core:17.4.4'
|
|
||||||
|
|
||||||
// ReactiveX
|
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
|
||||||
implementation 'io.reactivex:rxjava:1.3.8'
|
|
||||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
|
||||||
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
|
||||||
|
|
||||||
// Network client
|
|
||||||
final okhttp_version = '4.8.1'
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
|
||||||
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
|
|
||||||
implementation 'com.squareup.okio:okio:2.7.0'
|
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.4.0'
|
|
||||||
|
|
||||||
// REST
|
|
||||||
final retrofit_version = '2.9.0'
|
|
||||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
|
||||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
|
||||||
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
|
||||||
|
|
||||||
// JSON
|
|
||||||
implementation 'com.google.code.gson:gson:2.8.6'
|
|
||||||
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
|
||||||
|
|
||||||
// JavaScript engine
|
|
||||||
implementation 'com.squareup.duktape:duktape-android:1.3.0'
|
|
||||||
|
|
||||||
// Disk
|
|
||||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
|
||||||
implementation 'com.github.inorichi:unifile:e9ee588'
|
|
||||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
|
||||||
|
|
||||||
// HTML parser
|
|
||||||
implementation 'org.jsoup:jsoup:1.13.1'
|
|
||||||
|
|
||||||
// Database
|
|
||||||
implementation 'androidx.sqlite:sqlite: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.32.2'
|
|
||||||
|
|
||||||
// Preferences
|
|
||||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
|
|
||||||
|
|
||||||
// Model View Presenter
|
|
||||||
final nucleus_version = '3.0.0'
|
|
||||||
implementation "info.android15.nucleus:nucleus:$nucleus_version"
|
|
||||||
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
|
||||||
|
|
||||||
// Dependency injection
|
|
||||||
implementation "com.github.inorichi.injekt:injekt-core:65b0440"
|
|
||||||
|
|
||||||
// Image library
|
|
||||||
final glide_version = '4.11.0'
|
|
||||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
|
||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
|
||||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
|
||||||
|
|
||||||
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:bff2806'
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
|
||||||
|
|
||||||
// Crash reports
|
|
||||||
implementation 'ch.acra:acra-http:5.7.0'
|
|
||||||
|
|
||||||
// Sort
|
|
||||||
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.nononsenseapps:filepicker:2.5.2'
|
|
||||||
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
|
|
||||||
implementation 'com.github.mthli:Slice:v1.3'
|
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
|
||||||
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
|
|
||||||
|
|
||||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
|
||||||
final material_dialogs_version = '3.1.1'
|
|
||||||
implementation "com.afollestad.material-dialogs:core:$material_dialogs_version"
|
|
||||||
implementation "com.afollestad.material-dialogs:input:$material_dialogs_version"
|
|
||||||
implementation "com.afollestad.material-dialogs:datetime:$material_dialogs_version"
|
|
||||||
|
|
||||||
// Conductor
|
|
||||||
implementation 'com.bluelinelabs:conductor:2.1.5'
|
|
||||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
|
||||||
exclude group: "com.android.support"
|
|
||||||
}
|
|
||||||
implementation 'com.github.tachiyomiorg:conductor-support-preference:1.1.1'
|
|
||||||
|
|
||||||
// FlowBinding
|
|
||||||
final flowbinding_version = '0.12.0'
|
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
|
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
|
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
|
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version"
|
|
||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
|
||||||
|
|
||||||
// Licenses
|
|
||||||
final aboutlibraries_version = '8.3.0'
|
|
||||||
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
|
|
||||||
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
testImplementation 'junit:junit:4.13'
|
|
||||||
testImplementation 'org.assertj:assertj-core:3.12.2'
|
|
||||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
|
||||||
|
|
||||||
final robolectric_version = '3.1.4'
|
|
||||||
testImplementation "org.robolectric:robolectric:$robolectric_version"
|
|
||||||
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
|
||||||
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
|
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
|
||||||
|
|
||||||
final coroutines_version = '1.3.8'
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
|
||||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
|
||||||
|
|
||||||
// Debug tool; see https://fbflipper.com/
|
|
||||||
// debugImplementation 'com.facebook.flipper:flipper:0.50.0'
|
|
||||||
// debugImplementation 'com.facebook.soloader:soloader:0.9.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
ext.kotlin_version = '1.3.72'
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
|
||||||
tasks.withType(AbstractKotlinCompile).all {
|
|
||||||
kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.Experimental"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
|
||||||
task copyResources(type: Copy) {
|
|
||||||
from './src/main/res/values-he'
|
|
||||||
into './src/main/res/values-iw'
|
|
||||||
include '**/*'
|
|
||||||
}
|
|
||||||
|
|
||||||
preBuild.dependsOn(ktlintFormat, copyResources)
|
|
||||||
|
|
||||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
||||||
}
|
|
333
app/build.gradle.kts
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("com.mikepenz.aboutlibraries.plugin")
|
||||||
|
kotlin("android")
|
||||||
|
kotlin("kapt")
|
||||||
|
kotlin("plugin.serialization")
|
||||||
|
id("com.github.zellius.shortcut-helper")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||||
|
apply(plugin = "com.google.gms.google-services")
|
||||||
|
}
|
||||||
|
|
||||||
|
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion(AndroidConfig.compileSdk)
|
||||||
|
buildToolsVersion(AndroidConfig.buildTools)
|
||||||
|
ndkVersion = AndroidConfig.ndk
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
|
minSdkVersion(AndroidConfig.minSdk)
|
||||||
|
targetSdkVersion(AndroidConfig.targetSdk)
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
versionCode = 56
|
||||||
|
versionName = "0.10.9"
|
||||||
|
|
||||||
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
|
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
||||||
|
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||||
|
|
||||||
|
// Please disable ACRA or use your own instance in forked versions of the project
|
||||||
|
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
||||||
|
|
||||||
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
named("debug") {
|
||||||
|
versionNameSuffix = "-${getCommitCount()}"
|
||||||
|
applicationIdSuffix = ".debug"
|
||||||
|
}
|
||||||
|
named("release") {
|
||||||
|
/*named("postprocessing") {
|
||||||
|
postprocessing {
|
||||||
|
isObfuscate = false
|
||||||
|
isOptimizeCode = true
|
||||||
|
isRemoveUnusedCode = false
|
||||||
|
isRemoveUnusedResources = true
|
||||||
|
}
|
||||||
|
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions("default")
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
create("standard") {
|
||||||
|
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
|
||||||
|
dimension = "default"
|
||||||
|
}
|
||||||
|
create("fdroid") {
|
||||||
|
dimension = "default"
|
||||||
|
}
|
||||||
|
create("dev") {
|
||||||
|
resConfigs("en", "xxhdpi")
|
||||||
|
dimension = "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
exclude("META-INF/DEPENDENCIES")
|
||||||
|
exclude("LICENSE.txt")
|
||||||
|
exclude("META-INF/LICENSE")
|
||||||
|
exclude("META-INF/LICENSE.txt")
|
||||||
|
exclude("META-INF/NOTICE")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependenciesInfo {
|
||||||
|
includeInApk = false
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable("MissingTranslation", "ExtraTranslation")
|
||||||
|
isAbortOnError = false
|
||||||
|
isCheckReleaseBuilds = false
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
|
implementation("tachiyomi.sourceapi: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.browser:browser:1.3.0")
|
||||||
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
|
implementation("androidx.constraintlayout:constraintlayout:2.1.0-alpha2")
|
||||||
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||||
|
implementation("androidx.core:core-ktx:1.5.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.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||||
|
|
||||||
|
val lifecycleVersion = "2.3.0-rc01"
|
||||||
|
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")
|
||||||
|
|
||||||
|
// UI library
|
||||||
|
implementation("com.google.android.material:material:1.3.0")
|
||||||
|
|
||||||
|
"standardImplementation"("com.google.firebase:firebase-core:18.0.2")
|
||||||
|
|
||||||
|
// ReactiveX
|
||||||
|
implementation("io.reactivex:rxandroid:1.2.1")
|
||||||
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
|
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||||
|
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||||
|
|
||||||
|
// Network client
|
||||||
|
val okhttpVersion = "4.10.0-RC1"
|
||||||
|
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||||
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||||
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||||
|
implementation("com.squareup.okio:okio:2.10.0")
|
||||||
|
|
||||||
|
// TLS 1.3 support for Android < 10
|
||||||
|
implementation("org.conscrypt:conscrypt-android:2.5.1")
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
val kotlinSerializationVersion = "1.0.1"
|
||||||
|
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")
|
||||||
|
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||||
|
|
||||||
|
// JavaScript engine
|
||||||
|
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
||||||
|
|
||||||
|
// Disk
|
||||||
|
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||||
|
implementation("com.github.inorichi:unifile:e9ee588")
|
||||||
|
implementation("com.github.junrar:junrar:7.4.0")
|
||||||
|
|
||||||
|
// HTML parser
|
||||||
|
implementation("org.jsoup:jsoup:1.13.1")
|
||||||
|
|
||||||
|
// Database
|
||||||
|
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")
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3")
|
||||||
|
|
||||||
|
// Model View Presenter
|
||||||
|
val nucleusVersion = "3.0.0"
|
||||||
|
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
|
||||||
|
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
|
||||||
|
|
||||||
|
// Dependency injection
|
||||||
|
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")
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||||
|
|
||||||
|
// Crash reports
|
||||||
|
implementation("ch.acra:acra-http:5.7.0")
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
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")
|
||||||
|
|
||||||
|
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
||||||
|
val materialDialogsVersion = "3.1.1"
|
||||||
|
implementation("com.afollestad.material-dialogs:core:$materialDialogsVersion")
|
||||||
|
implementation("com.afollestad.material-dialogs:input:$materialDialogsVersion")
|
||||||
|
implementation("com.afollestad.material-dialogs:datetime:$materialDialogsVersion")
|
||||||
|
|
||||||
|
// Conductor
|
||||||
|
implementation("com.bluelinelabs:conductor:2.1.5")
|
||||||
|
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
||||||
|
exclude(group = "com.android.support")
|
||||||
|
}
|
||||||
|
implementation("com.github.tachiyomiorg:conductor-support-preference:1.1.1")
|
||||||
|
|
||||||
|
// FlowBinding
|
||||||
|
val flowbindingVersion = "0.12.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")
|
||||||
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
||||||
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
||||||
|
|
||||||
|
// Licenses
|
||||||
|
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
testImplementation("junit:junit:4.13.1")
|
||||||
|
testImplementation("org.assertj:assertj-core:3.16.1")
|
||||||
|
testImplementation("org.mockito:mockito-core:1.10.19")
|
||||||
|
|
||||||
|
val robolectricVersion = "3.1.4"
|
||||||
|
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
||||||
|
testImplementation("org.robolectric:shadows-multidex:$robolectricVersion")
|
||||||
|
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
||||||
|
|
||||||
|
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||||
|
|
||||||
|
val coroutinesVersion = "1.4.2"
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||||
|
withType<KotlinCompile> {
|
||||||
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
|
"-Xopt-in=kotlin.Experimental",
|
||||||
|
"-Xopt-in=kotlin.RequiresOptIn",
|
||||||
|
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
|
||||||
|
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
||||||
|
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
|
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||||
|
val copyHebrewStrings = task("copyHebrewStrings", type = Copy::class) {
|
||||||
|
from("./src/main/res/values-he")
|
||||||
|
into("./src/main/res/values-iw")
|
||||||
|
include("**/*")
|
||||||
|
}
|
||||||
|
|
||||||
|
preBuild {
|
||||||
|
dependsOn(formatKotlin, copyHebrewStrings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath(kotlin("gradle-plugin", version = BuildPluginsVersion.KOTLIN))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Git is needed in your system PATH for these commands to work.
|
||||||
|
// If it's not installed, you can return a random value as a workaround
|
||||||
|
fun getCommitCount(): String {
|
||||||
|
return runCommand("git rev-list --count HEAD")
|
||||||
|
// return "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getGitSha(): String {
|
||||||
|
return runCommand("git rev-parse --short HEAD")
|
||||||
|
// return "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBuildTime(): String {
|
||||||
|
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
||||||
|
df.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
return df.format(Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runCommand(command: String): String {
|
||||||
|
val byteOut = ByteArrayOutputStream()
|
||||||
|
project.exec {
|
||||||
|
commandLine = command.split(" ")
|
||||||
|
standardOutput = byteOut
|
||||||
|
}
|
||||||
|
return String(byteOut.toByteArray()).trim()
|
||||||
|
}
|
36
app/proguard-rules.pro
vendored
@ -23,21 +23,6 @@
|
|||||||
<init>();
|
<init>();
|
||||||
}
|
}
|
||||||
|
|
||||||
# OkHttp
|
|
||||||
-dontwarn okhttp3.**
|
|
||||||
-dontwarn okio.**
|
|
||||||
-dontwarn javax.annotation.**
|
|
||||||
-dontwarn retrofit2.Platform$Java8
|
|
||||||
|
|
||||||
# Glide specific rules #
|
|
||||||
# https://github.com/bumptech/glide
|
|
||||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
|
||||||
-keep public class * extends com.bumptech.glide.AppGlideModule
|
|
||||||
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
|
|
||||||
**[] $VALUES;
|
|
||||||
public *;
|
|
||||||
}
|
|
||||||
|
|
||||||
# RxJava 1.1.0
|
# RxJava 1.1.0
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
@ -71,3 +56,24 @@
|
|||||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||||
-keep class * implements com.google.gson.JsonSerializer
|
-keep class * implements com.google.gson.JsonSerializer
|
||||||
-keep class * implements com.google.gson.JsonDeserializer
|
-keep class * implements com.google.gson.JsonDeserializer
|
||||||
|
|
||||||
|
|
||||||
|
## kotlinx.serialization ##
|
||||||
|
|
||||||
|
-keepattributes *Annotation*, InnerClasses
|
||||||
|
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||||
|
|
||||||
|
-keepclassmembers class kotlinx.serialization.json.** {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keep,includedescriptorclasses class eu.kanade.tachiyomi.**$$serializer { *; }
|
||||||
|
-keepclassmembers class eu.kanade.tachiyomi.** {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class eu.kanade.tachiyomi.** {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 13 KiB |
@ -2,16 +2,24 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="eu.kanade.tachiyomi">
|
package="eu.kanade.tachiyomi">
|
||||||
|
|
||||||
|
<!-- Internet -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
|
<!-- Storage -->
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<!-- For background jobs -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
|
||||||
|
<!-- For managing extensions -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<!-- To view extension packages in API 30+ -->
|
||||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
@ -25,7 +33,7 @@
|
|||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.Tachiyomi.Light"
|
android:theme="@style/Theme.Tachiyomi.Light"
|
||||||
android:usesCleartextTraffic="true">
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
@ -42,7 +50,8 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.DeepLinkActivity"
|
android:name=".ui.main.DeepLinkActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
|
android:label="@string/action_global_search">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||||
@ -53,6 +62,11 @@
|
|||||||
<action android:name="eu.kanade.tachiyomi.SEARCH" />
|
<action android:name="eu.kanade.tachiyomi.SEARCH" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.searchable"
|
android:name="android.app.searchable"
|
||||||
@ -60,17 +74,20 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:launchMode="singleTask" />
|
android:launchMode="singleTask">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
|
android:resource="@xml/s_pen_actions"/>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.security.BiometricUnlockActivity"
|
android:name=".ui.security.BiometricUnlockActivity"
|
||||||
android:theme="@style/Theme.Splash" />
|
android:theme="@style/Theme.Splash" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.webview.WebViewActivity"
|
android:name=".ui.webview.WebViewActivity"
|
||||||
android:configChanges="uiMode|orientation|screenSize" />
|
android:configChanges="uiMode|orientation|screenSize" />
|
||||||
<activity
|
|
||||||
android:name=".widget.CustomLayoutPickerActivity"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/FilePickerTheme" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||||
android:label="Anilist">
|
android:label="Anilist">
|
||||||
@ -85,6 +102,20 @@
|
|||||||
android:scheme="tachiyomi" />
|
android:scheme="tachiyomi" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||||
|
android:label="MyAnimeList">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="myanimelist-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||||
android:label="Shikimori">
|
android:label="Shikimori">
|
||||||
|
@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import java.security.Security
|
|
||||||
import org.acra.ACRA
|
import org.acra.ACRA
|
||||||
import org.acra.annotation.AcraCore
|
import org.acra.annotation.AcraCore
|
||||||
import org.acra.annotation.AcraHttpSender
|
import org.acra.annotation.AcraHttpSender
|
||||||
@ -21,39 +20,30 @@ import org.acra.sender.HttpSender
|
|||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.InjektScope
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
import java.security.Security
|
||||||
|
|
||||||
@AcraCore(
|
@AcraCore(
|
||||||
buildConfigClass = BuildConfig::class,
|
buildConfigClass = BuildConfig::class,
|
||||||
excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"]
|
excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"]
|
||||||
)
|
)
|
||||||
@AcraHttpSender(
|
@AcraHttpSender(
|
||||||
uri = "https://tachiyomi.kanade.eu/crash_report",
|
uri = BuildConfig.ACRA_URI,
|
||||||
httpMethod = HttpSender.Method.PUT
|
httpMethod = HttpSender.Method.PUT
|
||||||
)
|
)
|
||||||
open class App : Application(), LifecycleObserver {
|
open class App : Application(), LifecycleObserver {
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
// Debug tool; see https://fbflipper.com/
|
|
||||||
// SoLoader.init(this, false)
|
|
||||||
// if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) {
|
|
||||||
// val client = AndroidFlipperClient.getInstance(this)
|
|
||||||
// client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
|
|
||||||
// client.addPlugin(DatabasesFlipperPlugin(this))
|
|
||||||
// client.start()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
Injekt = InjektScope(DefaultRegistrar())
|
|
||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
setupAcra()
|
setupAcra()
|
||||||
@ -77,14 +67,15 @@ open class App : Application(), LifecycleObserver {
|
|||||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun onAppBackgrounded() {
|
fun onAppBackgrounded() {
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
|
||||||
if (preferences.lockAppAfter().get() >= 0) {
|
if (preferences.lockAppAfter().get() >= 0) {
|
||||||
SecureActivityDelegate.locked = true
|
SecureActivityDelegate.locked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupAcra() {
|
protected open fun setupAcra() {
|
||||||
ACRA.init(this)
|
if (BuildConfig.FLAVOR != "dev") {
|
||||||
|
ACRA.init(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupNotificationChannels() {
|
protected open fun setupNotificationChannels() {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.os.Handler
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
@ -11,8 +12,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addSingleton
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
@ -44,16 +44,19 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { Gson() }
|
addSingletonFactory { Gson() }
|
||||||
|
|
||||||
|
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
|
Handler().post {
|
||||||
|
get<PreferencesHelper>()
|
||||||
|
|
||||||
GlobalScope.launch { get<PreferencesHelper>() }
|
get<NetworkHelper>()
|
||||||
|
|
||||||
GlobalScope.launch { get<NetworkHelper>() }
|
get<SourceManager>()
|
||||||
|
|
||||||
GlobalScope.launch { get<SourceManager>() }
|
get<DatabaseHelper>()
|
||||||
|
|
||||||
GlobalScope.launch { get<DatabaseHelper>() }
|
get<DownloadManager>()
|
||||||
|
}
|
||||||
GlobalScope.launch { get<DownloadManager>() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object Migrations {
|
object Migrations {
|
||||||
@ -18,13 +26,13 @@ object Migrations {
|
|||||||
*/
|
*/
|
||||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||||
val context = preferences.context
|
val context = preferences.context
|
||||||
val oldVersion = preferences.lastVersionCode().get()
|
|
||||||
|
|
||||||
// Cancel app updater job for debug builds that don't include it
|
// Cancel app updater job for debug builds that don't include it
|
||||||
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
||||||
UpdaterJob.cancelTask(context)
|
UpdaterJob.cancelTask(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val oldVersion = preferences.lastVersionCode().get()
|
||||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
@ -85,12 +93,43 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 44) {
|
if (oldVersion < 44) {
|
||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
||||||
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 52) {
|
||||||
|
// Migrate library filters to tri-state versions
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
fun convertBooleanPrefToTriState(key: String): Int {
|
||||||
|
val oldPrefValue = prefs.getBoolean(key, false)
|
||||||
|
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE.value
|
||||||
|
else ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value
|
||||||
|
}
|
||||||
|
prefs.edit {
|
||||||
|
putInt(PreferenceKeys.filterDownloaded, convertBooleanPrefToTriState("pref_filter_downloaded_key"))
|
||||||
|
remove("pref_filter_downloaded_key")
|
||||||
|
|
||||||
|
putInt(PreferenceKeys.filterUnread, convertBooleanPrefToTriState("pref_filter_unread_key"))
|
||||||
|
remove("pref_filter_unread_key")
|
||||||
|
|
||||||
|
putInt(PreferenceKeys.filterCompleted, convertBooleanPrefToTriState("pref_filter_completed_key"))
|
||||||
|
remove("pref_filter_completed_key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 54) {
|
||||||
|
// Force MAL log out due to login flow change
|
||||||
|
// v52: switched from scraping to WebView
|
||||||
|
// v53: switched from WebView to OAuth
|
||||||
|
val trackManager = Injekt.get<TrackManager>()
|
||||||
|
if (trackManager.myAnimeList.isLogged) {
|
||||||
|
trackManager.myAnimeList.logout()
|
||||||
|
context.toast(R.string.myanimelist_relogin)
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.annoations
|
package eu.kanade.tachiyomi.annotations
|
||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
@ -0,0 +1,96 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
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.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||||
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class AbstractBackupManager(protected val context: Context) {
|
||||||
|
|
||||||
|
internal val databaseHelper: DatabaseHelper by injectLazy()
|
||||||
|
internal val sourceManager: SourceManager by injectLazy()
|
||||||
|
internal val trackManager: TrackManager by injectLazy()
|
||||||
|
protected val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns manga
|
||||||
|
*
|
||||||
|
* @return [Manga], null if not found
|
||||||
|
*/
|
||||||
|
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
||||||
|
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches chapter information.
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @param chapters list of chapters in the backup
|
||||||
|
* @return Updated manga chapters.
|
||||||
|
*/
|
||||||
|
internal suspend fun restoreChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
|
||||||
|
val fetchedChapters = source.getChapterList(manga.toMangaInfo())
|
||||||
|
.map { it.toSChapter() }
|
||||||
|
val syncedChapters = syncChaptersWithSource(databaseHelper, fetchedChapters, manga, source)
|
||||||
|
if (syncedChapters.first.isNotEmpty()) {
|
||||||
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
|
updateChapters(chapters)
|
||||||
|
}
|
||||||
|
return syncedChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list containing manga from library
|
||||||
|
*
|
||||||
|
* @return [Manga] from library
|
||||||
|
*/
|
||||||
|
protected fun getFavoriteManga(): List<Manga> =
|
||||||
|
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts manga and returns id
|
||||||
|
*
|
||||||
|
* @return id of [Manga], null if not found
|
||||||
|
*/
|
||||||
|
internal fun insertManga(manga: Manga): Long? =
|
||||||
|
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts list of chapters
|
||||||
|
*/
|
||||||
|
protected fun insertChapters(chapters: List<Chapter>) {
|
||||||
|
databaseHelper.insertChapters(chapters).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a list of chapters
|
||||||
|
*/
|
||||||
|
protected fun updateChapters(chapters: List<Chapter>) {
|
||||||
|
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a list of chapters with known database ids
|
||||||
|
*/
|
||||||
|
protected fun updateKnownChapters(chapters: List<Chapter>) {
|
||||||
|
databaseHelper.updateKnownChaptersBackup(chapters).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return number of backups.
|
||||||
|
*
|
||||||
|
* @return number of backups selected by user
|
||||||
|
*/
|
||||||
|
protected fun numberOfBackups(): Int = preferences.numberOfBackups().get()
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
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.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
|
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val context: Context, protected val notifier: BackupNotifier) {
|
||||||
|
|
||||||
|
protected val db: DatabaseHelper by injectLazy()
|
||||||
|
protected val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
var job: Job? = null
|
||||||
|
|
||||||
|
protected lateinit var backupManager: T
|
||||||
|
|
||||||
|
protected var restoreAmount = 0
|
||||||
|
protected var restoreProgress = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of source ID to source name from backup data
|
||||||
|
*/
|
||||||
|
protected var sourceMapping: Map<Long, String> = emptyMap()
|
||||||
|
|
||||||
|
protected val errors = mutableListOf<Pair<Date, String>>()
|
||||||
|
|
||||||
|
abstract suspend fun performRestore(uri: Uri): Boolean
|
||||||
|
|
||||||
|
suspend fun restoreBackup(uri: Uri): Boolean {
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
restoreProgress = 0
|
||||||
|
errors.clear()
|
||||||
|
|
||||||
|
if (!performRestore(uri)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
val time = endTime - startTime
|
||||||
|
|
||||||
|
val logFile = writeErrorLog()
|
||||||
|
|
||||||
|
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches chapter information.
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return Updated manga chapters.
|
||||||
|
*/
|
||||||
|
internal suspend fun updateChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
|
||||||
|
return try {
|
||||||
|
backupManager.restoreChapters(source, manga, chapters)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If there's any error, return empty update and continue.
|
||||||
|
val errorMessage = if (e is NoChaptersException) {
|
||||||
|
context.getString(R.string.no_chapters_error)
|
||||||
|
} else {
|
||||||
|
e.message
|
||||||
|
}
|
||||||
|
errors.add(Date() to "${manga.title} - $errorMessage")
|
||||||
|
Pair(emptyList(), emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes tracking information.
|
||||||
|
*
|
||||||
|
* @param manga manga that needs updating.
|
||||||
|
* @param tracks list containing tracks from restore file.
|
||||||
|
*/
|
||||||
|
internal suspend fun updateTracking(manga: Manga, tracks: List<Track>) {
|
||||||
|
tracks.forEach { track ->
|
||||||
|
val service = trackManager.getService(track.sync_id)
|
||||||
|
if (service != null && service.isLogged) {
|
||||||
|
try {
|
||||||
|
val updatedTrack = service.refresh(track)
|
||||||
|
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val serviceName = service?.nameRes()?.let { context.getString(it) }
|
||||||
|
errors.add(Date() to "${manga.title} - ${context.getString(R.string.tracker_not_logged_in, serviceName)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to update dialog in [BackupConst]
|
||||||
|
*
|
||||||
|
* @param progress restore progress
|
||||||
|
* @param amount total restoreAmount of manga
|
||||||
|
* @param title title of restored manga
|
||||||
|
*/
|
||||||
|
internal fun showRestoreProgress(
|
||||||
|
progress: Int,
|
||||||
|
amount: Int,
|
||||||
|
title: String
|
||||||
|
) {
|
||||||
|
notifier.showRestoreProgress(title, progress, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun writeErrorLog(): File {
|
||||||
|
try {
|
||||||
|
if (errors.isNotEmpty()) {
|
||||||
|
val file = context.createFileInCacheDir("tachiyomi_restore.txt")
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
|
||||||
|
file.bufferedWriter().use { out ->
|
||||||
|
errors.forEach { (date, message) ->
|
||||||
|
out.write("[${sdf.format(date)}] $message\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Empty
|
||||||
|
}
|
||||||
|
return File("")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class AbstractBackupRestoreValidator {
|
||||||
|
protected val sourceManager: SourceManager by injectLazy()
|
||||||
|
protected val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
abstract fun validate(context: Context, uri: Uri): Results
|
||||||
|
|
||||||
|
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||||
|
}
|
@ -7,4 +7,9 @@ object BackupConst {
|
|||||||
private const val NAME = "BackupRestoreServices"
|
private const val NAME = "BackupRestoreServices"
|
||||||
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||||
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
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
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,13 @@ import android.app.Service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
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.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
@ -46,17 +48,14 @@ class BackupCreateService : Service() {
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param flags determines what to backup
|
* @param flags determines what to backup
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, uri: Uri, flags: Int) {
|
fun start(context: Context, uri: Uri, flags: Int, type: Int) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
||||||
|
putExtra(BackupConst.EXTRA_TYPE, type)
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
ContextCompat.startForegroundService(context, intent)
|
||||||
context.startService(intent)
|
|
||||||
} else {
|
|
||||||
context.startForegroundService(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,7 +65,6 @@ class BackupCreateService : Service() {
|
|||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
private lateinit var backupManager: BackupManager
|
|
||||||
private lateinit var notifier: BackupNotifier
|
private lateinit var notifier: BackupNotifier
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@ -105,11 +103,15 @@ class BackupCreateService : Service() {
|
|||||||
try {
|
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 backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||||
backupManager = BackupManager(this)
|
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 = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
||||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||||
notifier.showBackupComplete(unifile)
|
notifier.showBackupComplete(unifile, backupType == BackupConst.BACKUP_TYPE_LEGACY)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
notifier.showBackupError(e.message)
|
notifier.showBackupError(e.message)
|
||||||
}
|
}
|
||||||
|
@ -7,21 +7,25 @@ import androidx.work.PeriodicWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
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 eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
Worker(context, workerParams) {
|
Worker(context, workerParams) {
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val backupManager = BackupManager(context)
|
|
||||||
val uri = preferences.backupsDirectory().get().toUri()
|
val uri = preferences.backupsDirectory().get().toUri()
|
||||||
val flags = BackupCreateService.BACKUP_ALL
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
return try {
|
return try {
|
||||||
backupManager.createBackup(uri, flags, true)
|
FullBackupManager(context).createBackup(uri, flags, true)
|
||||||
|
if (preferences.createLegacyBackup().get()) {
|
||||||
|
LegacyBackupManager(context).createBackup(uri, flags, true)
|
||||||
|
}
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure()
|
Result.failure()
|
||||||
@ -36,8 +40,10 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val interval = prefInterval ?: preferences.backupInterval().get()
|
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||||
interval.toLong(), TimeUnit.HOURS,
|
interval.toLong(),
|
||||||
10, TimeUnit.MINUTES
|
TimeUnit.HOURS,
|
||||||
|
10,
|
||||||
|
TimeUnit.MINUTES
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.build()
|
.build()
|
||||||
|
@ -11,11 +11,11 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
internal class BackupNotifier(private val context: Context) {
|
class BackupNotifier(private val context: Context) {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
@ -60,25 +60,20 @@ internal class BackupNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showBackupComplete(unifile: UniFile) {
|
fun showBackupComplete(unifile: UniFile, isLegacyFormat: Boolean) {
|
||||||
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||||
|
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.backup_created))
|
setContentTitle(context.getString(R.string.backup_created))
|
||||||
|
setContentText(unifile.filePath ?: unifile.name)
|
||||||
if (unifile.filePath != null) {
|
|
||||||
setContentText(unifile.filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear old actions if they exist
|
// Clear old actions if they exist
|
||||||
if (mActions.isNotEmpty()) {
|
clearActions()
|
||||||
mActions.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_share_24dp,
|
R.drawable.ic_share_24dp,
|
||||||
context.getString(R.string.action_share),
|
context.getString(R.string.action_share),
|
||||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, isLegacyFormat, Notifications.ID_BACKUP_COMPLETE)
|
||||||
)
|
)
|
||||||
|
|
||||||
show(Notifications.ID_BACKUP_COMPLETE)
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
@ -97,9 +92,7 @@ internal class BackupNotifier(private val context: Context) {
|
|||||||
setOnlyAlertOnce(true)
|
setOnlyAlertOnce(true)
|
||||||
|
|
||||||
// Clear old actions if they exist
|
// Clear old actions if they exist
|
||||||
if (mActions.isNotEmpty()) {
|
clearActions()
|
||||||
mActions.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_close_24dp,
|
R.drawable.ic_close_24dp,
|
||||||
@ -140,16 +133,14 @@ internal class BackupNotifier(private val context: Context) {
|
|||||||
setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount))
|
setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount))
|
||||||
|
|
||||||
// Clear old actions if they exist
|
// Clear old actions if they exist
|
||||||
if (mActions.isNotEmpty()) {
|
clearActions()
|
||||||
mActions.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
||||||
val destFile = File(path, file)
|
val destFile = File(path, file)
|
||||||
val uri = destFile.getUriCompat(context)
|
val uri = destFile.getUriCompat(context)
|
||||||
|
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.nnf_ic_file_folder,
|
R.drawable.ic_folder_24dp,
|
||||||
context.getString(R.string.action_open_log),
|
context.getString(R.string.action_open_log),
|
||||||
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||||
)
|
)
|
||||||
|
@ -4,50 +4,25 @@ import android.app.Service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupRestore
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestore
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
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.Track
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import rx.Observable
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores backup from a JSON file.
|
* Restores backup.
|
||||||
*/
|
*/
|
||||||
class BackupRestoreService : Service() {
|
class BackupRestoreService : Service() {
|
||||||
|
|
||||||
@ -68,16 +43,14 @@ class BackupRestoreService : Service() {
|
|||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, uri: Uri) {
|
fun start(context: Context, uri: Uri, mode: Int, online: Boolean?) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
|
putExtra(BackupConst.EXTRA_MODE, mode)
|
||||||
|
online?.let { putExtra(BackupConst.EXTRA_TYPE, it) }
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
ContextCompat.startForegroundService(context, intent)
|
||||||
context.startService(intent)
|
|
||||||
} else {
|
|
||||||
context.startForegroundService(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,38 +71,14 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
private var job: Job? = null
|
private lateinit var ioScope: CoroutineScope
|
||||||
|
private var backupRestore: AbstractBackupRestore<*>? = null
|
||||||
/**
|
|
||||||
* The progress of a backup restore
|
|
||||||
*/
|
|
||||||
private var restoreProgress = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount of manga in Json file (needed for restore)
|
|
||||||
*/
|
|
||||||
private var restoreAmount = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping of source ID to source name from backup data
|
|
||||||
*/
|
|
||||||
private var sourceMapping: Map<Long, String> = emptyMap()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List containing errors
|
|
||||||
*/
|
|
||||||
private val errors = mutableListOf<Pair<Date, String>>()
|
|
||||||
|
|
||||||
private lateinit var backupManager: BackupManager
|
|
||||||
private lateinit var notifier: BackupNotifier
|
private lateinit var notifier: BackupNotifier
|
||||||
|
|
||||||
private val db: DatabaseHelper by injectLazy()
|
|
||||||
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
notifier = BackupNotifier(this)
|
notifier = BackupNotifier(this)
|
||||||
wakeLock = acquireWakeLock(javaClass.name)
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
|
|
||||||
@ -147,7 +96,8 @@ class BackupRestoreService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun destroyJob() {
|
private fun destroyJob() {
|
||||||
job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
|
ioScope?.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
@ -168,299 +118,34 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
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.
|
// Cancel any previous job if needed.
|
||||||
job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
|
|
||||||
|
backupRestore = when (mode) {
|
||||||
|
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
|
||||||
|
else -> LegacyBackupRestore(this, notifier)
|
||||||
|
}
|
||||||
|
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
Timber.e(exception)
|
||||||
writeErrorLog()
|
backupRestore?.writeErrorLog()
|
||||||
|
|
||||||
notifier.showRestoreError(exception.message)
|
notifier.showRestoreError(exception.message)
|
||||||
|
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
job = GlobalScope.launch(handler) {
|
val job = ioScope.launch(handler) {
|
||||||
if (!restoreBackup(uri)) {
|
if (backupRestore?.restoreBackup(uri) == false) {
|
||||||
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
job?.invokeOnCompletion {
|
job.invokeOnCompletion {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
|
backupRestore?.job = job
|
||||||
|
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores data from backup file.
|
|
||||||
*
|
|
||||||
* @param uri backup file to restore
|
|
||||||
*/
|
|
||||||
private fun restoreBackup(uri: Uri): Boolean {
|
|
||||||
val startTime = System.currentTimeMillis()
|
|
||||||
|
|
||||||
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
|
||||||
|
|
||||||
// Get parser version
|
|
||||||
val version = json.get(VERSION)?.asInt ?: 1
|
|
||||||
|
|
||||||
// Initialize manager
|
|
||||||
backupManager = BackupManager(this, version)
|
|
||||||
|
|
||||||
val mangasJson = json.get(MANGAS).asJsonArray
|
|
||||||
|
|
||||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
|
||||||
restoreProgress = 0
|
|
||||||
errors.clear()
|
|
||||||
|
|
||||||
// Restore categories
|
|
||||||
json.get(CATEGORIES)?.let { restoreCategories(it) }
|
|
||||||
|
|
||||||
// Store source mapping for error messages
|
|
||||||
sourceMapping = BackupRestoreValidator.getSourceMapping(json)
|
|
||||||
|
|
||||||
// Restore individual manga
|
|
||||||
mangasJson.forEach {
|
|
||||||
if (job?.isActive != true) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreManga(it.asJsonObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
val endTime = System.currentTimeMillis()
|
|
||||||
val time = endTime - startTime
|
|
||||||
|
|
||||||
val logFile = writeErrorLog()
|
|
||||||
|
|
||||||
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreCategories(categoriesJson: JsonElement) {
|
|
||||||
db.inTransaction {
|
|
||||||
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreManga(mangaJson: JsonObject) {
|
|
||||||
val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
|
|
||||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
|
||||||
mangaJson.get(CHAPTERS)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val categories = backupManager.parser.fromJson<List<String>>(
|
|
||||||
mangaJson.get(CATEGORIES)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val history = backupManager.parser.fromJson<List<DHistory>>(
|
|
||||||
mangaJson.get(HISTORY)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
|
||||||
mangaJson.get(TRACK)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
val source = backupManager.sourceManager.get(manga.source)
|
|
||||||
if (source != null) {
|
|
||||||
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
|
||||||
} else {
|
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
|
||||||
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found_name, sourceName)}")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 fun restoreMangaData(
|
|
||||||
manga: Manga,
|
|
||||||
source: Source,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<String>,
|
|
||||||
history: List<DHistory>,
|
|
||||||
tracks: List<Track>
|
|
||||||
) {
|
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
|
||||||
|
|
||||||
db.inTransaction {
|
|
||||||
if (dbManga == null) {
|
|
||||||
// Manga not in database
|
|
||||||
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
|
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Observable] that fetches manga information
|
|
||||||
*
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @param chapters chapters of manga that needs updating
|
|
||||||
* @param categories categories that need updating
|
|
||||||
*/
|
|
||||||
private fun restoreMangaFetch(
|
|
||||||
source: Source,
|
|
||||||
manga: Manga,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<String>,
|
|
||||||
history: List<DHistory>,
|
|
||||||
tracks: List<Track>
|
|
||||||
) {
|
|
||||||
backupManager.restoreMangaFetchObservable(source, manga)
|
|
||||||
.onErrorReturn {
|
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
.filter { it.id != null }
|
|
||||||
.flatMap {
|
|
||||||
chapterFetchObservable(source, it, chapters)
|
|
||||||
// Convert to the manga that contains new chapters.
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
.doOnNext {
|
|
||||||
restoreExtraForManga(it, categories, history, tracks)
|
|
||||||
}
|
|
||||||
.flatMap {
|
|
||||||
trackingFetchObservable(it, tracks)
|
|
||||||
}
|
|
||||||
.subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreMangaNoFetch(
|
|
||||||
source: Source,
|
|
||||||
backupManga: Manga,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<String>,
|
|
||||||
history: List<DHistory>,
|
|
||||||
tracks: List<Track>
|
|
||||||
) {
|
|
||||||
Observable.just(backupManga)
|
|
||||||
.flatMap { manga ->
|
|
||||||
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
|
||||||
chapterFetchObservable(source, manga, chapters)
|
|
||||||
.map { manga }
|
|
||||||
} else {
|
|
||||||
Observable.just(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.doOnNext {
|
|
||||||
restoreExtraForManga(it, categories, history, tracks)
|
|
||||||
}
|
|
||||||
.flatMap { manga ->
|
|
||||||
trackingFetchObservable(manga, tracks)
|
|
||||||
}
|
|
||||||
.subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
|
||||||
// Restore categories
|
|
||||||
backupManager.restoreCategoriesForManga(manga, categories)
|
|
||||||
|
|
||||||
// Restore history
|
|
||||||
backupManager.restoreHistoryForManga(history)
|
|
||||||
|
|
||||||
// Restore tracking
|
|
||||||
backupManager.restoreTrackForManga(manga, tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Observable] that fetches chapter information
|
|
||||||
*
|
|
||||||
* @param source source of manga
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @return [Observable] that contains manga
|
|
||||||
*/
|
|
||||||
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
|
||||||
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
|
|
||||||
// If there's any error, return empty update and continue.
|
|
||||||
.onErrorReturn {
|
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
|
||||||
Pair(emptyList(), emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Observable] that refreshes tracking information
|
|
||||||
* @param manga manga that needs updating.
|
|
||||||
* @param tracks list containing tracks from restore file.
|
|
||||||
* @return [Observable] that contains updated track item
|
|
||||||
*/
|
|
||||||
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
|
||||||
return Observable.from(tracks)
|
|
||||||
.flatMap { track ->
|
|
||||||
val service = trackManager.getService(track.sync_id)
|
|
||||||
if (service != null && service.isLogged) {
|
|
||||||
service.refresh(track)
|
|
||||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
|
||||||
.onErrorReturn {
|
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
|
||||||
track
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errors.add(Date() to "${manga.title} - ${getString(R.string.tracker_not_logged_in, service?.name)}")
|
|
||||||
Observable.empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to update dialog in [BackupConst]
|
|
||||||
*
|
|
||||||
* @param progress restore progress
|
|
||||||
* @param amount total restoreAmount of manga
|
|
||||||
* @param title title of restored manga
|
|
||||||
*/
|
|
||||||
private fun showRestoreProgress(
|
|
||||||
progress: Int,
|
|
||||||
amount: Int,
|
|
||||||
title: String
|
|
||||||
) {
|
|
||||||
notifier.showRestoreProgress(title, progress, amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write errors to error log
|
|
||||||
*/
|
|
||||||
private fun writeErrorLog(): File {
|
|
||||||
try {
|
|
||||||
if (errors.isNotEmpty()) {
|
|
||||||
val destFile = File(externalCacheDir, "tachiyomi_restore.txt")
|
|
||||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
|
||||||
|
|
||||||
destFile.bufferedWriter().use { out ->
|
|
||||||
errors.forEach { (date, message) ->
|
|
||||||
out.write("[${sdf.format(date)}] $message\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return destFile
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Empty
|
|
||||||
}
|
|
||||||
return File("")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,409 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
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.full.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupFull
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupTracking
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
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 kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.sink
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
|
|
||||||
|
val parser = ProtoBuf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create backup Json file from database
|
||||||
|
*
|
||||||
|
* @param uri path of Uri
|
||||||
|
* @param isJob backup called from job
|
||||||
|
*/
|
||||||
|
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||||
|
// Create root object
|
||||||
|
var backup: Backup? = null
|
||||||
|
|
||||||
|
databaseHelper.inTransaction {
|
||||||
|
val databaseManga = getFavoriteManga()
|
||||||
|
|
||||||
|
backup = Backup(
|
||||||
|
backupManga(databaseManga, flags),
|
||||||
|
backupCategories(),
|
||||||
|
backupExtensionInfo(databaseManga)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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+.proto.gz""")
|
||||||
|
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||||
|
.orEmpty()
|
||||||
|
.sortedByDescending { it.name }
|
||||||
|
.drop(numberOfBackups - 1)
|
||||||
|
.forEach { it.delete() }
|
||||||
|
|
||||||
|
// Create new file to place backup
|
||||||
|
dir.createFile(BackupFull.getDefaultFilename())
|
||||||
|
} else {
|
||||||
|
UniFile.fromUri(context, uri)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
|
||||||
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||||
|
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
||||||
|
return file.uri.toString()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupManga(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
||||||
|
return mangas.map {
|
||||||
|
backupMangaObject(it, flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupExtensionInfo(mangas: List<Manga>): List<BackupSource> {
|
||||||
|
return mangas
|
||||||
|
.asSequence()
|
||||||
|
.map { it.source }
|
||||||
|
.distinct()
|
||||||
|
.map { sourceManager.getOrStub(it) }
|
||||||
|
.map { BackupSource.copyFrom(it) }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup the categories of library
|
||||||
|
*
|
||||||
|
* @return list of [BackupCategory] to be backed up
|
||||||
|
*/
|
||||||
|
private fun backupCategories(): List<BackupCategory> {
|
||||||
|
return databaseHelper.getCategories()
|
||||||
|
.executeAsBlocking()
|
||||||
|
.map { BackupCategory.copyFrom(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a manga to Json
|
||||||
|
*
|
||||||
|
* @param manga manga that gets converted
|
||||||
|
* @param options options for the backup
|
||||||
|
* @return [BackupManga] containing manga in a serializable form
|
||||||
|
*/
|
||||||
|
private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
|
||||||
|
// Entry for this manga
|
||||||
|
val mangaObject = BackupManga.copyFrom(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()) {
|
||||||
|
mangaObject.chapters = chapters.map { BackupChapter.copyFrom(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()) {
|
||||||
|
mangaObject.categories = categoriesForManga.mapNotNull { it.order }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()) {
|
||||||
|
mangaObject.tracking = tracks.map { BackupTracking.copyFrom(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 history = historyForManga.mapNotNull { history ->
|
||||||
|
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||||
|
url?.let { BackupHistory(url, history.last_read) }
|
||||||
|
}
|
||||||
|
if (history.isNotEmpty()) {
|
||||||
|
mangaObject.history = history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mangaObject
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||||
|
manga.id = dbManga.id
|
||||||
|
manga.copyFrom(dbManga)
|
||||||
|
insertManga(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the categories from Json
|
||||||
|
*
|
||||||
|
* @param backupCategories list containing categories
|
||||||
|
*/
|
||||||
|
internal fun restoreCategories(backupCategories: List<BackupCategory>) {
|
||||||
|
// Get categories from file and from db
|
||||||
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
|
||||||
|
// Iterate over them
|
||||||
|
backupCategories.map { it.getCategoryImpl() }.forEach { category ->
|
||||||
|
// Used to know if the category is already in the db
|
||||||
|
var found = false
|
||||||
|
for (dbCategory in dbCategories) {
|
||||||
|
// If the category is already in the db, assign the id to the file's category
|
||||||
|
// and do nothing
|
||||||
|
if (category.name == dbCategory.name) {
|
||||||
|
category.id = dbCategory.id
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the category isn't in the db, remove the id and insert a new category
|
||||||
|
// Store the inserted id in the category
|
||||||
|
if (!found) {
|
||||||
|
// Let the db assign the id
|
||||||
|
category.id = null
|
||||||
|
val result = databaseHelper.insertCategory(category).executeAsBlocking()
|
||||||
|
category.id = result.insertedId()?.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the categories a manga is in.
|
||||||
|
*
|
||||||
|
* @param manga the manga whose categories have to be restored.
|
||||||
|
* @param categories the categories to restore.
|
||||||
|
*/
|
||||||
|
internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
|
||||||
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
|
||||||
|
categories.forEach { backupCategoryOrder ->
|
||||||
|
backupCategories.firstOrNull {
|
||||||
|
it.order == backupCategoryOrder
|
||||||
|
}?.let { backupCategory ->
|
||||||
|
dbCategories.firstOrNull { dbCategory ->
|
||||||
|
dbCategory.name == backupCategory.name
|
||||||
|
}?.let { dbCategory ->
|
||||||
|
mangaCategoriesToUpdate += MangaCategory.create(manga, dbCategory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||||
|
databaseHelper.deleteOldMangasCategories(listOf(manga)).executeAsBlocking()
|
||||||
|
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore history from Json
|
||||||
|
*
|
||||||
|
* @param history list containing history to be restored
|
||||||
|
*/
|
||||||
|
internal fun restoreHistoryForManga(history: List<BackupHistory>) {
|
||||||
|
// List containing history to be updated
|
||||||
|
val historyToBeUpdated = ArrayList<History>(history.size)
|
||||||
|
for ((url, lastRead) in history) {
|
||||||
|
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||||
|
// Check if history already in database and update
|
||||||
|
if (dbHistory != null) {
|
||||||
|
dbHistory.apply {
|
||||||
|
last_read = max(lastRead, dbHistory.last_read)
|
||||||
|
}
|
||||||
|
historyToBeUpdated.add(dbHistory)
|
||||||
|
} else {
|
||||||
|
// If not in database create
|
||||||
|
databaseHelper.getChapter(url).executeAsBlocking()?.let {
|
||||||
|
val historyToAdd = History.create(it).apply {
|
||||||
|
last_read = lastRead
|
||||||
|
}
|
||||||
|
historyToBeUpdated.add(historyToAdd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the sync of a manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga whose sync have to be restored.
|
||||||
|
* @param tracks the track list to restore.
|
||||||
|
*/
|
||||||
|
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
||||||
|
// Fix foreign keys with the current manga id
|
||||||
|
tracks.map { it.manga_id = manga.id!! }
|
||||||
|
|
||||||
|
// Get tracks from database
|
||||||
|
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isInDatabase) {
|
||||||
|
// Insert new sync. Let the db assign the id
|
||||||
|
track.id = null
|
||||||
|
trackToUpdate.add(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update database
|
||||||
|
if (trackToUpdate.isNotEmpty()) {
|
||||||
|
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>) {
|
||||||
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
val newChapters = chapters.groupBy { it.id != null }
|
||||||
|
newChapters[true]?.let { updateKnownChapters(it) }
|
||||||
|
newChapters[false]?.let { insertChapters(it) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,187 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
||||||
|
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 okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.source
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
||||||
|
|
||||||
|
override suspend fun performRestore(uri: Uri): Boolean {
|
||||||
|
backupManager = FullBackupManager(context)
|
||||||
|
|
||||||
|
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
||||||
|
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||||
|
|
||||||
|
restoreAmount = backup.backupManga.size + 1 // +1 for categories
|
||||||
|
|
||||||
|
// Restore categories
|
||||||
|
if (backup.backupCategories.isNotEmpty()) {
|
||||||
|
restoreCategories(backup.backupCategories)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store source mapping for error messages
|
||||||
|
sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
||||||
|
|
||||||
|
// Restore individual manga
|
||||||
|
backup.backupManga.forEach {
|
||||||
|
if (job?.isActive != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreManga(it, backup.backupCategories, online)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreCategories(backupCategories: List<BackupCategory>) {
|
||||||
|
db.inTransaction {
|
||||||
|
backupManager.restoreCategories(backupCategories)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
|
||||||
|
val manga = backupManga.getMangaImpl()
|
||||||
|
val chapters = backupManga.getChaptersImpl()
|
||||||
|
val categories = backupManga.categories
|
||||||
|
val history = backupManga.history
|
||||||
|
val tracks = backupManga.getTrackingImpl()
|
||||||
|
|
||||||
|
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, online)
|
||||||
|
} else {
|
||||||
|
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
|
manga: Manga,
|
||||||
|
source: Source?,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<Int>,
|
||||||
|
history: List<BackupHistory>,
|
||||||
|
tracks: List<Track>,
|
||||||
|
backupCategories: List<BackupCategory>,
|
||||||
|
online: Boolean
|
||||||
|
) {
|
||||||
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
|
db.inTransaction {
|
||||||
|
if (dbManga == null) {
|
||||||
|
// Manga not in database
|
||||||
|
restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
|
||||||
|
} 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, online)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches manga information
|
||||||
|
*
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @param chapters chapters of manga that needs updating
|
||||||
|
* @param categories categories that need updating
|
||||||
|
*/
|
||||||
|
private suspend fun restoreMangaFetch(
|
||||||
|
source: Source?,
|
||||||
|
manga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<Int>,
|
||||||
|
history: List<BackupHistory>,
|
||||||
|
tracks: List<Track>,
|
||||||
|
backupCategories: List<BackupCategory>,
|
||||||
|
online: Boolean
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val fetchedManga = backupManager.restoreMangaFetch(source, manga, online)
|
||||||
|
fetchedManga.id ?: return
|
||||||
|
|
||||||
|
if (online && source != null) {
|
||||||
|
updateChapters(source, fetchedManga, chapters)
|
||||||
|
} else {
|
||||||
|
backupManager.restoreChaptersForMangaOffline(fetchedManga, chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories)
|
||||||
|
|
||||||
|
updateTracking(fetchedManga, tracks)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreMangaNoFetch(
|
||||||
|
source: Source?,
|
||||||
|
backupManga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<Int>,
|
||||||
|
history: List<BackupHistory>,
|
||||||
|
tracks: List<Track>,
|
||||||
|
backupCategories: List<BackupCategory>,
|
||||||
|
online: Boolean
|
||||||
|
) {
|
||||||
|
if (online && source != null) {
|
||||||
|
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
||||||
|
updateChapters(source, backupManga, chapters)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
|
||||||
|
|
||||||
|
updateTracking(backupManga, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
||||||
|
// Restore categories
|
||||||
|
backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
|
||||||
|
|
||||||
|
// Restore history
|
||||||
|
backupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// Restore tracking
|
||||||
|
backupManager.restoreTrackForManga(manga, tracks)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.source
|
||||||
|
|
||||||
|
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
|
/**
|
||||||
|
* Checks for critical backup file data.
|
||||||
|
*
|
||||||
|
* @throws Exception if manga cannot be found.
|
||||||
|
* @return List of missing sources or missing trackers.
|
||||||
|
*/
|
||||||
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
|
val backupManager = FullBackupManager(context)
|
||||||
|
|
||||||
|
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
||||||
|
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||||
|
|
||||||
|
if (backup.backupManga.isEmpty()) {
|
||||||
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
|
}
|
||||||
|
|
||||||
|
val sources = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
||||||
|
val missingSources = sources
|
||||||
|
.filter { sourceManager.get(it.key) == null }
|
||||||
|
.values
|
||||||
|
.sorted()
|
||||||
|
|
||||||
|
val trackers = backup.backupManga
|
||||||
|
.flatMap { it.tracking }
|
||||||
|
.map { it.syncId }
|
||||||
|
.distinct()
|
||||||
|
val missingTrackers = trackers
|
||||||
|
.mapNotNull { trackManager.getService(it) }
|
||||||
|
.filter { !it.isLogged }
|
||||||
|
.map { context.getString(it.nameRes()) }
|
||||||
|
.sorted()
|
||||||
|
|
||||||
|
return Results(missingSources, missingTrackers)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Backup(
|
||||||
|
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
||||||
|
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||||
|
// Bump by 100 to specify this is a 0.x value
|
||||||
|
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
|
||||||
|
)
|
@ -0,0 +1,33 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class BackupCategory(
|
||||||
|
@ProtoNumber(1) var name: String,
|
||||||
|
@ProtoNumber(2) var order: Int = 0,
|
||||||
|
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
||||||
|
// Bump by 100 to specify this is a 0.x value
|
||||||
|
@ProtoNumber(100) var flags: Int = 0,
|
||||||
|
) {
|
||||||
|
fun getCategoryImpl(): CategoryImpl {
|
||||||
|
return CategoryImpl().apply {
|
||||||
|
name = this@BackupCategory.name
|
||||||
|
flags = this@BackupCategory.flags
|
||||||
|
order = this@BackupCategory.order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(category: Category): BackupCategory {
|
||||||
|
return BackupCategory(
|
||||||
|
name = category.name,
|
||||||
|
order = category.order,
|
||||||
|
flags = category.flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupChapter(
|
||||||
|
// in 1.x some of these values have different names
|
||||||
|
// url is called key in 1.x
|
||||||
|
@ProtoNumber(1) var url: String,
|
||||||
|
@ProtoNumber(2) var name: String,
|
||||||
|
@ProtoNumber(3) var scanlator: String? = null,
|
||||||
|
@ProtoNumber(4) var read: Boolean = false,
|
||||||
|
@ProtoNumber(5) var bookmark: Boolean = false,
|
||||||
|
// lastPageRead is called progress in 1.x
|
||||||
|
@ProtoNumber(6) var lastPageRead: Int = 0,
|
||||||
|
@ProtoNumber(7) var dateFetch: Long = 0,
|
||||||
|
@ProtoNumber(8) var dateUpload: Long = 0,
|
||||||
|
// chapterNumber is called number is 1.x
|
||||||
|
@ProtoNumber(9) var chapterNumber: Float = 0F,
|
||||||
|
@ProtoNumber(10) var sourceOrder: Int = 0,
|
||||||
|
) {
|
||||||
|
fun toChapterImpl(): ChapterImpl {
|
||||||
|
return ChapterImpl().apply {
|
||||||
|
url = this@BackupChapter.url
|
||||||
|
name = this@BackupChapter.name
|
||||||
|
chapter_number = this@BackupChapter.chapterNumber
|
||||||
|
scanlator = this@BackupChapter.scanlator
|
||||||
|
read = this@BackupChapter.read
|
||||||
|
bookmark = this@BackupChapter.bookmark
|
||||||
|
last_page_read = this@BackupChapter.lastPageRead
|
||||||
|
date_fetch = this@BackupChapter.dateFetch
|
||||||
|
date_upload = this@BackupChapter.dateUpload
|
||||||
|
source_order = this@BackupChapter.sourceOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(chapter: Chapter): BackupChapter {
|
||||||
|
return BackupChapter(
|
||||||
|
url = chapter.url,
|
||||||
|
name = chapter.name,
|
||||||
|
chapterNumber = chapter.chapter_number,
|
||||||
|
scanlator = chapter.scanlator,
|
||||||
|
read = chapter.read,
|
||||||
|
bookmark = chapter.bookmark,
|
||||||
|
lastPageRead = chapter.last_page_read,
|
||||||
|
dateFetch = chapter.date_fetch,
|
||||||
|
dateUpload = chapter.date_upload,
|
||||||
|
sourceOrder = chapter.source_order
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object BackupFull {
|
||||||
|
fun getDefaultFilename(): String {
|
||||||
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
|
return "tachiyomi_$date.proto.gz"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupHistory(
|
||||||
|
@ProtoNumber(0) var url: String,
|
||||||
|
@ProtoNumber(1) var lastRead: Long
|
||||||
|
)
|
@ -0,0 +1,87 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
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 kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupManga(
|
||||||
|
// in 1.x some of these values have different names
|
||||||
|
@ProtoNumber(1) var source: Long,
|
||||||
|
// url is called key in 1.x
|
||||||
|
@ProtoNumber(2) var url: String,
|
||||||
|
@ProtoNumber(3) var title: String = "",
|
||||||
|
@ProtoNumber(4) var artist: String? = null,
|
||||||
|
@ProtoNumber(5) var author: String? = null,
|
||||||
|
@ProtoNumber(6) var description: String? = null,
|
||||||
|
@ProtoNumber(7) var genre: List<String> = emptyList(),
|
||||||
|
@ProtoNumber(8) var status: Int = 0,
|
||||||
|
// thumbnailUrl is called cover in 1.x
|
||||||
|
@ProtoNumber(9) var thumbnailUrl: String? = null,
|
||||||
|
// @ProtoNumber(10) val customCover: String = "", 1.x value, not used in 0.x
|
||||||
|
// @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(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(),
|
||||||
|
@ProtoNumber(18) var tracking: List<BackupTracking> = emptyList(),
|
||||||
|
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
||||||
|
@ProtoNumber(100) var favorite: Boolean = true,
|
||||||
|
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||||
|
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
||||||
|
) {
|
||||||
|
fun getMangaImpl(): MangaImpl {
|
||||||
|
return MangaImpl().apply {
|
||||||
|
url = this@BackupManga.url
|
||||||
|
title = this@BackupManga.title
|
||||||
|
artist = this@BackupManga.artist
|
||||||
|
author = this@BackupManga.author
|
||||||
|
description = this@BackupManga.description
|
||||||
|
genre = this@BackupManga.genre.joinToString()
|
||||||
|
status = this@BackupManga.status
|
||||||
|
thumbnail_url = this@BackupManga.thumbnailUrl
|
||||||
|
favorite = this@BackupManga.favorite
|
||||||
|
source = this@BackupManga.source
|
||||||
|
date_added = this@BackupManga.dateAdded
|
||||||
|
viewer = this@BackupManga.viewer
|
||||||
|
chapter_flags = this@BackupManga.chapterFlags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChaptersImpl(): List<ChapterImpl> {
|
||||||
|
return chapters.map {
|
||||||
|
it.toChapterImpl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTrackingImpl(): List<TrackImpl> {
|
||||||
|
return tracking.map {
|
||||||
|
it.getTrackingImpl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(manga: Manga): 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,
|
||||||
|
thumbnailUrl = manga.thumbnail_url,
|
||||||
|
favorite = manga.favorite,
|
||||||
|
source = manga.source,
|
||||||
|
dateAdded = manga.date_added,
|
||||||
|
viewer = manga.viewer,
|
||||||
|
chapterFlags = manga.chapter_flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializer
|
||||||
|
|
||||||
|
@Serializer(forClass = Backup::class)
|
||||||
|
object BackupSerializer
|
@ -0,0 +1,20 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupSource(
|
||||||
|
@ProtoNumber(0) var name: String = "",
|
||||||
|
@ProtoNumber(1) var sourceId: Long
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(source: Source): BackupSource {
|
||||||
|
return BackupSource(
|
||||||
|
name = source.name,
|
||||||
|
sourceId = source.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupTracking(
|
||||||
|
// in 1.x some of these values have different types or names
|
||||||
|
// syncId is called siteId in 1,x
|
||||||
|
@ProtoNumber(1) var syncId: Int,
|
||||||
|
// LibraryId is not null in 1.x
|
||||||
|
@ProtoNumber(2) var libraryId: Long,
|
||||||
|
@ProtoNumber(3) var mediaId: Int = 0,
|
||||||
|
// trackingUrl is called mediaUrl in 1.x
|
||||||
|
@ProtoNumber(4) var trackingUrl: String = "",
|
||||||
|
@ProtoNumber(5) var title: String = "",
|
||||||
|
// lastChapterRead is called last read, and it has been changed to a float in 1.x
|
||||||
|
@ProtoNumber(6) var lastChapterRead: Float = 0F,
|
||||||
|
@ProtoNumber(7) var totalChapters: Int = 0,
|
||||||
|
@ProtoNumber(8) var score: Float = 0F,
|
||||||
|
@ProtoNumber(9) var status: Int = 0,
|
||||||
|
// startedReadingDate is called startReadTime in 1.x
|
||||||
|
@ProtoNumber(10) var startedReadingDate: Long = 0,
|
||||||
|
// finishedReadingDate is called endReadTime in 1.x
|
||||||
|
@ProtoNumber(11) var finishedReadingDate: Long = 0,
|
||||||
|
) {
|
||||||
|
fun getTrackingImpl(): TrackImpl {
|
||||||
|
return TrackImpl().apply {
|
||||||
|
sync_id = this@BackupTracking.syncId
|
||||||
|
media_id = this@BackupTracking.mediaId
|
||||||
|
library_id = this@BackupTracking.libraryId
|
||||||
|
title = this@BackupTracking.title
|
||||||
|
// convert from float to int because of 1.x types
|
||||||
|
last_chapter_read = this@BackupTracking.lastChapterRead.toInt()
|
||||||
|
total_chapters = this@BackupTracking.totalChapters
|
||||||
|
score = this@BackupTracking.score
|
||||||
|
status = this@BackupTracking.status
|
||||||
|
started_reading_date = this@BackupTracking.startedReadingDate
|
||||||
|
finished_reading_date = this@BackupTracking.finishedReadingDate
|
||||||
|
tracking_url = this@BackupTracking.trackingUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(track: Track): BackupTracking {
|
||||||
|
return BackupTracking(
|
||||||
|
syncId = track.sync_id,
|
||||||
|
mediaId = track.media_id,
|
||||||
|
// forced not null so its compatible with 1.x backup system
|
||||||
|
libraryId = track.library_id!!,
|
||||||
|
title = track.title,
|
||||||
|
// convert to float for 1.x
|
||||||
|
lastChapterRead = track.last_chapter_read.toFloat(),
|
||||||
|
totalChapters = track.total_chapters,
|
||||||
|
score = track.score,
|
||||||
|
status = track.status,
|
||||||
|
startedReadingDate = track.started_reading_date,
|
||||||
|
finishedReadingDate = track.finished_reading_date,
|
||||||
|
trackingUrl = track.tracking_url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup.legacy
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -12,6 +12,7 @@ import com.google.gson.JsonArray
|
|||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonElement
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
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
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
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
|
||||||
@ -20,21 +21,20 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HIST
|
|||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
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
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CATEGORIES
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CHAPTERS
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.EXTENSIONS
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.HISTORY
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGA
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
@ -44,56 +44,24 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
|||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
|
||||||
import kotlin.math.max
|
|
||||||
import rx.Observable
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import kotlin.math.max
|
||||||
|
|
||||||
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||||
|
|
||||||
internal val databaseHelper: DatabaseHelper by injectLazy()
|
val parser: Gson = when (version) {
|
||||||
internal val sourceManager: SourceManager by injectLazy()
|
2 -> GsonBuilder()
|
||||||
internal val trackManager: TrackManager by injectLazy()
|
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||||
|
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||||
/**
|
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||||
* Version of parser
|
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||||
*/
|
.create()
|
||||||
var version: Int = version
|
else -> throw Exception("Unknown backup version")
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Json Parser
|
|
||||||
*/
|
|
||||||
var parser: Gson = initParser()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set version of parser
|
|
||||||
*
|
|
||||||
* @param version version of parser
|
|
||||||
*/
|
|
||||||
internal fun setVersion(version: Int) {
|
|
||||||
this.version = version
|
|
||||||
parser = initParser()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initParser(): Gson = when (version) {
|
|
||||||
1 -> GsonBuilder().create()
|
|
||||||
2 ->
|
|
||||||
GsonBuilder()
|
|
||||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
|
||||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
|
||||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
|
||||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
|
||||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
|
||||||
.create()
|
|
||||||
else -> throw Exception("Json version unknown")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -102,7 +70,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isJob backup called from job
|
||||||
*/
|
*/
|
||||||
fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||||
// Create root object
|
// Create root object
|
||||||
val root = JsonObject()
|
val root = JsonObject()
|
||||||
|
|
||||||
@ -122,7 +90,6 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
root[EXTENSIONS] = extensionEntries
|
root[EXTENSIONS] = extensionEntries
|
||||||
|
|
||||||
databaseHelper.inTransaction {
|
databaseHelper.inTransaction {
|
||||||
// Get manga from database
|
|
||||||
val mangas = getFavoriteManga()
|
val mangas = getFavoriteManga()
|
||||||
|
|
||||||
val extensions: MutableSet<String> = mutableSetOf()
|
val extensions: MutableSet<String> = mutableSetOf()
|
||||||
@ -149,39 +116,33 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// When BackupCreatorJob
|
val file: UniFile = (
|
||||||
if (isJob) {
|
if (isJob) {
|
||||||
// Get dir of file and create
|
// Get dir of file and create
|
||||||
var dir = UniFile.fromUri(context, uri)
|
var dir = UniFile.fromUri(context, uri)
|
||||||
dir = dir.createDirectory("automatic")
|
dir = dir.createDirectory("automatic")
|
||||||
|
|
||||||
// Delete older backups
|
// Delete older backups
|
||||||
val numberOfBackups = numberOfBackups()
|
val numberOfBackups = numberOfBackups()
|
||||||
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
||||||
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.sortedByDescending { it.name }
|
.sortedByDescending { it.name }
|
||||||
.drop(numberOfBackups - 1)
|
.drop(numberOfBackups - 1)
|
||||||
.forEach { it.delete() }
|
.forEach { it.delete() }
|
||||||
|
|
||||||
// Create new file to place backup
|
// Create new file to place backup
|
||||||
val newFile = dir.createFile(Backup.getDefaultFilename())
|
dir.createFile(Backup.getDefaultFilename())
|
||||||
?: throw Exception("Couldn't create backup file")
|
} else {
|
||||||
|
UniFile.fromUri(context, uri)
|
||||||
newFile.openOutputStream().bufferedWriter().use {
|
|
||||||
parser.toJson(root, it)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
|
||||||
return newFile.uri.toString()
|
file.openOutputStream().bufferedWriter().use {
|
||||||
} else {
|
parser.toJson(root, it)
|
||||||
val file = UniFile.fromUri(context, uri)
|
|
||||||
?: throw Exception("Couldn't create backup file")
|
|
||||||
file.openOutputStream().bufferedWriter().use {
|
|
||||||
parser.toJson(root, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
return file.uri.toString()
|
|
||||||
}
|
}
|
||||||
|
return file.uri.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
throw e
|
throw e
|
||||||
@ -273,39 +234,20 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Observable] that fetches manga information
|
* Fetches manga information
|
||||||
*
|
*
|
||||||
* @param source source of manga
|
* @param source source of manga
|
||||||
* @param manga manga that needs updating
|
* @param manga manga that needs updating
|
||||||
* @return [Observable] that contains manga
|
* @return Updated manga.
|
||||||
*/
|
*/
|
||||||
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
|
suspend fun fetchManga(source: Source, manga: Manga): Manga {
|
||||||
return source.fetchMangaDetails(manga)
|
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||||
.map { networkManga ->
|
return manga.also {
|
||||||
manga.copyFrom(networkManga)
|
it.copyFrom(networkManga.toSManga())
|
||||||
manga.favorite = true
|
it.favorite = true
|
||||||
manga.initialized = true
|
it.initialized = true
|
||||||
manga.id = insertManga(manga)
|
it.id = insertManga(manga)
|
||||||
manga
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Observable] that fetches chapter information
|
|
||||||
*
|
|
||||||
* @param source source of manga
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @return [Observable] that contains manga
|
|
||||||
*/
|
|
||||||
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
|
||||||
return source.fetchChapterList(manga)
|
|
||||||
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
|
||||||
.doOnNext { pair ->
|
|
||||||
if (pair.first.isNotEmpty()) {
|
|
||||||
chapters.forEach { it.manga_id = manga.id }
|
|
||||||
insertChapters(chapters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -350,7 +292,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*/
|
*/
|
||||||
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
|
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
|
||||||
for (backupCategoryStr in categories) {
|
for (backupCategoryStr in categories) {
|
||||||
for (dbCategory in dbCategories) {
|
for (dbCategory in dbCategories) {
|
||||||
if (backupCategoryStr == dbCategory.name) {
|
if (backupCategoryStr == dbCategory.name) {
|
||||||
@ -374,7 +316,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*/
|
*/
|
||||||
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
||||||
// List containing history to be updated
|
// List containing history to be updated
|
||||||
val historyToBeUpdated = mutableListOf<History>()
|
val historyToBeUpdated = ArrayList<History>(history.size)
|
||||||
for ((url, lastRead) in history) {
|
for ((url, lastRead) in history) {
|
||||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||||
// Check if history already in database and update
|
// Check if history already in database and update
|
||||||
@ -403,14 +345,14 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
* @param tracks the track list to restore.
|
* @param tracks the track list to restore.
|
||||||
*/
|
*/
|
||||||
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
||||||
// Fix foreign keys with the current manga id
|
|
||||||
tracks.map { it.manga_id = manga.id!! }
|
|
||||||
|
|
||||||
// Get tracks from database
|
// Get tracks from database
|
||||||
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
val trackToUpdate = mutableListOf<Track>()
|
val trackToUpdate = ArrayList<Track>(tracks.size)
|
||||||
|
|
||||||
tracks.forEach { track ->
|
tracks.forEach { track ->
|
||||||
|
// Fix foreign keys with the current manga id
|
||||||
|
track.manga_id = manga.id!!
|
||||||
|
|
||||||
val service = trackManager.getService(track.sync_id)
|
val service = trackManager.getService(track.sync_id)
|
||||||
if (service != null && service.isLogged) {
|
if (service != null && service.isLogged) {
|
||||||
var isInDatabase = false
|
var isInDatabase = false
|
||||||
@ -465,50 +407,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
chapter.copyFrom(dbChapter)
|
chapter.copyFrom(dbChapter)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Filter the chapters that couldn't be found.
|
|
||||||
chapters.filter { it.id != null }
|
|
||||||
chapters.map { it.manga_id = manga.id }
|
|
||||||
|
|
||||||
insertChapters(chapters)
|
chapter.manga_id = manga.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the chapters that couldn't be found.
|
||||||
|
updateChapters(chapters.filter { it.id != null })
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns manga
|
|
||||||
*
|
|
||||||
* @return [Manga], null if not found
|
|
||||||
*/
|
|
||||||
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
|
||||||
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns list containing manga from library
|
|
||||||
*
|
|
||||||
* @return [Manga] from library
|
|
||||||
*/
|
|
||||||
internal fun getFavoriteManga(): List<Manga> =
|
|
||||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts manga and returns id
|
|
||||||
*
|
|
||||||
* @return id of [Manga], null if not found
|
|
||||||
*/
|
|
||||||
internal fun insertManga(manga: Manga): Long? =
|
|
||||||
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts list of chapters
|
|
||||||
*/
|
|
||||||
private fun insertChapters(chapters: List<Chapter>) {
|
|
||||||
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return number of backups.
|
|
||||||
*
|
|
||||||
* @return number of backups selected by user
|
|
||||||
*/
|
|
||||||
fun numberOfBackups(): Int = preferences.numberOfBackups().get()
|
|
||||||
}
|
}
|
@ -0,0 +1,194 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGAS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
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.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
||||||
|
|
||||||
|
override suspend fun performRestore(uri: Uri): Boolean {
|
||||||
|
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
|
val version = json.get(Backup.VERSION)?.asInt ?: 1
|
||||||
|
backupManager = LegacyBackupManager(context, version)
|
||||||
|
|
||||||
|
val mangasJson = json.get(MANGAS).asJsonArray
|
||||||
|
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||||
|
|
||||||
|
// Restore categories
|
||||||
|
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
||||||
|
|
||||||
|
// Store source mapping for error messages
|
||||||
|
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
|
||||||
|
|
||||||
|
// Restore individual manga
|
||||||
|
mangasJson.forEach {
|
||||||
|
if (job?.isActive != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreManga(it.asJsonObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreCategories(categoriesJson: JsonElement) {
|
||||||
|
db.inTransaction {
|
||||||
|
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||||
|
val manga = backupManager.parser.fromJson<MangaImpl>(
|
||||||
|
mangaJson.get(
|
||||||
|
Backup.MANGA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
||||||
|
mangaJson.get(Backup.CHAPTERS)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val categories = backupManager.parser.fromJson<List<String>>(
|
||||||
|
mangaJson.get(Backup.CATEGORIES)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val history = backupManager.parser.fromJson<List<DHistory>>(
|
||||||
|
mangaJson.get(Backup.HISTORY)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
||||||
|
mangaJson.get(Backup.TRACK)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
val source = backupManager.sourceManager.get(manga.source)
|
||||||
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (source != null) {
|
||||||
|
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||||
|
} else {
|
||||||
|
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
|
manga: Manga,
|
||||||
|
source: Source,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
|
db.inTransaction {
|
||||||
|
if (dbManga == null) {
|
||||||
|
// Manga not in database
|
||||||
|
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches manga information.
|
||||||
|
*
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @param chapters chapters of manga that needs updating
|
||||||
|
* @param categories categories that need updating
|
||||||
|
*/
|
||||||
|
private suspend fun restoreMangaFetch(
|
||||||
|
source: Source,
|
||||||
|
manga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val fetchedManga = backupManager.fetchManga(source, manga)
|
||||||
|
fetchedManga.id ?: return
|
||||||
|
|
||||||
|
updateChapters(source, fetchedManga, chapters)
|
||||||
|
|
||||||
|
restoreExtraForManga(fetchedManga, categories, history, tracks)
|
||||||
|
|
||||||
|
updateTracking(fetchedManga, tracks)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreMangaNoFetch(
|
||||||
|
source: Source,
|
||||||
|
backupManga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
|
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
||||||
|
updateChapters(source, backupManga, chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreExtraForManga(backupManga, categories, history, tracks)
|
||||||
|
|
||||||
|
updateTracking(backupManga, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
||||||
|
// Restore categories
|
||||||
|
backupManager.restoreCategoriesForManga(manga, categories)
|
||||||
|
|
||||||
|
// Restore history
|
||||||
|
backupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// Restore tracking
|
||||||
|
backupManager.restoreTrackForManga(manga, tracks)
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup.legacy
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -6,23 +6,17 @@ import com.google.gson.JsonObject
|
|||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
object BackupRestoreValidator {
|
|
||||||
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
|
||||||
|
|
||||||
|
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
* @throws Exception if version or manga cannot be found.
|
* @throws Exception if version or manga cannot be found.
|
||||||
* @return List of missing sources or missing trackers.
|
* @return List of missing sources or missing trackers.
|
||||||
*/
|
*/
|
||||||
fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
@ -51,22 +45,22 @@ object BackupRestoreValidator {
|
|||||||
val missingTrackers = trackers
|
val missingTrackers = trackers
|
||||||
.mapNotNull { trackManager.getService(it) }
|
.mapNotNull { trackManager.getService(it) }
|
||||||
.filter { !it.isLogged }
|
.filter { !it.isLogged }
|
||||||
.map { it.name }
|
.map { context.getString(it.nameRes()) }
|
||||||
.sorted()
|
.sorted()
|
||||||
|
|
||||||
return Results(missingSources, missingTrackers)
|
return Results(missingSources, missingTrackers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
companion object {
|
||||||
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||||
|
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
||||||
|
|
||||||
return extensionsMapping.asJsonArray
|
return extensionsMapping.asJsonArray
|
||||||
.map {
|
.map {
|
||||||
val items = it.asString.split(":")
|
val items = it.asString.split(":")
|
||||||
items[0].toLong() to items[1]
|
items[0].toLong() to items[1]
|
||||||
}
|
}
|
||||||
.toMap()
|
.toMap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.legacy.models
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
@ -1,3 +1,3 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.legacy.models
|
||||||
|
|
||||||
data class DHistory(val url: String, val lastRead: Long)
|
data class DHistory(val url: String, val lastRead: Long)
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
@ -1,8 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON Serializer used to write / read [DHistory] to / from json
|
* JSON Serializer used to write / read [DHistory] to / from json
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
@ -9,13 +9,13 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to create chapter cache
|
* Class used to create chapter cache
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
@ -35,12 +35,13 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
|
override fun mapToContentValues(obj: Category) =
|
||||||
put(COL_ID, obj.id)
|
contentValuesOf(
|
||||||
put(COL_NAME, obj.name)
|
COL_ID to obj.id,
|
||||||
put(COL_ORDER, obj.order)
|
COL_NAME to obj.name,
|
||||||
put(COL_FLAGS, obj.flags)
|
COL_ORDER to obj.order,
|
||||||
}
|
COL_FLAGS to obj.flags
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
@ -43,20 +43,21 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
|
override fun mapToContentValues(obj: Chapter) =
|
||||||
put(COL_ID, obj.id)
|
contentValuesOf(
|
||||||
put(COL_MANGA_ID, obj.manga_id)
|
COL_ID to obj.id,
|
||||||
put(COL_URL, obj.url)
|
COL_MANGA_ID to obj.manga_id,
|
||||||
put(COL_NAME, obj.name)
|
COL_URL to obj.url,
|
||||||
put(COL_READ, obj.read)
|
COL_NAME to obj.name,
|
||||||
put(COL_SCANLATOR, obj.scanlator)
|
COL_READ to obj.read,
|
||||||
put(COL_BOOKMARK, obj.bookmark)
|
COL_SCANLATOR to obj.scanlator,
|
||||||
put(COL_DATE_FETCH, obj.date_fetch)
|
COL_BOOKMARK to obj.bookmark,
|
||||||
put(COL_DATE_UPLOAD, obj.date_upload)
|
COL_DATE_FETCH to obj.date_fetch,
|
||||||
put(COL_LAST_PAGE_READ, obj.last_page_read)
|
COL_DATE_UPLOAD to obj.date_upload,
|
||||||
put(COL_CHAPTER_NUMBER, obj.chapter_number)
|
COL_LAST_PAGE_READ to obj.last_page_read,
|
||||||
put(COL_SOURCE_ORDER, obj.source_order)
|
COL_CHAPTER_NUMBER to obj.chapter_number,
|
||||||
}
|
COL_SOURCE_ORDER to obj.source_order
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
@ -35,12 +35,13 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
|
override fun mapToContentValues(obj: History) =
|
||||||
put(COL_ID, obj.id)
|
contentValuesOf(
|
||||||
put(COL_CHAPTER_ID, obj.chapter_id)
|
COL_ID to obj.id,
|
||||||
put(COL_LAST_READ, obj.last_read)
|
COL_CHAPTER_ID to obj.chapter_id,
|
||||||
put(COL_TIME_READ, obj.time_read)
|
COL_LAST_READ to obj.last_read,
|
||||||
}
|
COL_TIME_READ to obj.time_read
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
@ -33,11 +33,12 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
|
override fun mapToContentValues(obj: MangaCategory) =
|
||||||
put(COL_ID, obj.id)
|
contentValuesOf(
|
||||||
put(COL_MANGA_ID, obj.manga_id)
|
COL_ID to obj.id,
|
||||||
put(COL_CATEGORY_ID, obj.category_id)
|
COL_MANGA_ID to obj.manga_id,
|
||||||
}
|
COL_CATEGORY_ID to obj.category_id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
@ -48,25 +48,26 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Manga) = ContentValues(17).apply {
|
override fun mapToContentValues(obj: Manga) =
|
||||||
put(COL_ID, obj.id)
|
contentValuesOf(
|
||||||
put(COL_SOURCE, obj.source)
|
COL_ID to obj.id,
|
||||||
put(COL_URL, obj.url)
|
COL_SOURCE to obj.source,
|
||||||
put(COL_ARTIST, obj.artist)
|
COL_URL to obj.url,
|
||||||
put(COL_AUTHOR, obj.author)
|
COL_ARTIST to obj.artist,
|
||||||
put(COL_DESCRIPTION, obj.description)
|
COL_AUTHOR to obj.author,
|
||||||
put(COL_GENRE, obj.genre)
|
COL_DESCRIPTION to obj.description,
|
||||||
put(COL_TITLE, obj.title)
|
COL_GENRE to obj.genre,
|
||||||
put(COL_STATUS, obj.status)
|
COL_TITLE to obj.title,
|
||||||
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
|
COL_STATUS to obj.status,
|
||||||
put(COL_FAVORITE, obj.favorite)
|
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
||||||
put(COL_LAST_UPDATE, obj.last_update)
|
COL_FAVORITE to obj.favorite,
|
||||||
put(COL_INITIALIZED, obj.initialized)
|
COL_LAST_UPDATE to obj.last_update,
|
||||||
put(COL_VIEWER, obj.viewer)
|
COL_INITIALIZED to obj.initialized,
|
||||||
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
COL_VIEWER to obj.viewer,
|
||||||
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
|
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||||
put(COL_DATE_ADDED, obj.date_added)
|
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
||||||
}
|
COL_DATE_ADDED to obj.date_added
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseMangaGetResolver {
|
interface BaseMangaGetResolver {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
@ -44,21 +44,22 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Track) = ContentValues(10).apply {
|
override fun mapToContentValues(obj: Track) =
|
||||||
put(COL_ID, obj.id)
|
contentValuesOf(
|
||||||
put(COL_MANGA_ID, obj.manga_id)
|
COL_ID to obj.id,
|
||||||
put(COL_SYNC_ID, obj.sync_id)
|
COL_MANGA_ID to obj.manga_id,
|
||||||
put(COL_MEDIA_ID, obj.media_id)
|
COL_SYNC_ID to obj.sync_id,
|
||||||
put(COL_LIBRARY_ID, obj.library_id)
|
COL_MEDIA_ID to obj.media_id,
|
||||||
put(COL_TITLE, obj.title)
|
COL_LIBRARY_ID to obj.library_id,
|
||||||
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
|
COL_TITLE to obj.title,
|
||||||
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
|
COL_LAST_CHAPTER_READ to obj.last_chapter_read,
|
||||||
put(COL_STATUS, obj.status)
|
COL_TOTAL_CHAPTERS to obj.total_chapters,
|
||||||
put(COL_TRACKING_URL, obj.tracking_url)
|
COL_STATUS to obj.status,
|
||||||
put(COL_SCORE, obj.score)
|
COL_TRACKING_URL to obj.tracking_url,
|
||||||
put(COL_START_DATE, obj.started_reading_date)
|
COL_SCORE to obj.score,
|
||||||
put(COL_FINISH_DATE, obj.finished_reading_date)
|
COL_START_DATE to obj.started_reading_date,
|
||||||
}
|
COL_FINISH_DATE to obj.finished_reading_date
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class TrackGetResolver : DefaultGetResolver<Track>() {
|
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
|
||||||
interface Manga : SManga {
|
interface Manga : SManga {
|
||||||
|
|
||||||
@ -98,3 +99,16 @@ interface Manga : SManga {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Manga.toMangaInfo(): MangaInfo {
|
||||||
|
return MangaInfo(
|
||||||
|
artist = this.artist ?: "",
|
||||||
|
author = this.author ?: "",
|
||||||
|
cover = this.thumbnail_url ?: "",
|
||||||
|
description = this.description ?: "",
|
||||||
|
genres = this.getGenres() ?: emptyList(),
|
||||||
|
key = this.url,
|
||||||
|
status = this.status,
|
||||||
|
title = this.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterKnownBackupPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
||||||
@ -84,6 +85,11 @@ interface ChapterQueries : DbProvider {
|
|||||||
.withPutResolver(ChapterBackupPutResolver())
|
.withPutResolver(ChapterBackupPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateKnownChaptersBackup(chapters: List<Chapter>) = db.put()
|
||||||
|
.objects(chapters)
|
||||||
|
.withPutResolver(ChapterKnownBackupPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun updateChapterProgress(chapter: Chapter) = db.put()
|
fun updateChapterProgress(chapter: Chapter) = db.put()
|
||||||
.`object`(chapter)
|
.`object`(chapter)
|
||||||
.withPutResolver(ChapterProgressPutResolver())
|
.withPutResolver(ChapterProgressPutResolver())
|
||||||
|
@ -21,13 +21,16 @@ interface HistoryQueries : DbProvider {
|
|||||||
/**
|
/**
|
||||||
* Returns history of recent manga containing last read chapter
|
* Returns history of recent manga containing last read chapter
|
||||||
* @param date recent date range
|
* @param date recent date range
|
||||||
|
* @param limit the limit of manga to grab
|
||||||
|
* @param offset offset the db by
|
||||||
|
* @param search what to search in the db history
|
||||||
*/
|
*/
|
||||||
fun getRecentManga(date: Date) = db.get()
|
fun getRecentManga(date: Date, limit: Int = 25, offset: Int = 0, search: String = "") = db.get()
|
||||||
.listOfObjects(MangaChapterHistory::class.java)
|
.listOfObjects(MangaChapterHistory::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
RawQuery.builder()
|
RawQuery.builder()
|
||||||
.query(getRecentMangasQuery())
|
.query(getRecentMangasQuery(search))
|
||||||
.args(date.time)
|
.args(date.time, limit, offset)
|
||||||
.observesTables(HistoryTable.TABLE)
|
.observesTables(HistoryTable.TABLE)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
@ -83,6 +83,11 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaFlagsPutResolver())
|
.withPutResolver(MangaFlagsPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateFlags(mangas: List<Manga>) = db.put()
|
||||||
|
.objects(mangas)
|
||||||
|
.withPutResolver(MangaFlagsPutResolver(true))
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun updateLastUpdated(manga: Manga) = db.put()
|
fun updateLastUpdated(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaLastUpdatedPutResolver())
|
.withPutResolver(MangaLastUpdatedPutResolver())
|
||||||
|
@ -49,9 +49,8 @@ fun getRecentsQuery() =
|
|||||||
* The max_last_read table contains the most recent chapters grouped by manga
|
* The max_last_read table contains the most recent chapters grouped by manga
|
||||||
* The select statement returns all information of chapters that have the same id as the chapter in max_last_read
|
* The select statement returns all information of chapters that have the same id as the chapter in max_last_read
|
||||||
* and are read after the given time period
|
* and are read after the given time period
|
||||||
* @return return limit is 25
|
|
||||||
*/
|
*/
|
||||||
fun getRecentMangasQuery() =
|
fun getRecentMangasQuery(search: String = "") =
|
||||||
"""
|
"""
|
||||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
|
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
@ -65,9 +64,11 @@ fun getRecentMangasQuery() =
|
|||||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
|
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
||||||
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
|
||||||
|
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
|
AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%'
|
||||||
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
|
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
|
||||||
LIMIT 25
|
LIMIT ? OFFSET ?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fun getHistoryByMangaId() =
|
fun getHistoryByMangaId() =
|
||||||
|
@ -10,6 +10,15 @@ import eu.kanade.tachiyomi.data.track.TrackService
|
|||||||
|
|
||||||
interface TrackQueries : DbProvider {
|
interface TrackQueries : DbProvider {
|
||||||
|
|
||||||
|
fun getTracks() = db.get()
|
||||||
|
.listOfObjects(Track::class.java)
|
||||||
|
.withQuery(
|
||||||
|
Query.builder()
|
||||||
|
.table(TrackTable.TABLE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getTracks(manga: Manga) = db.get()
|
fun getTracks(manga: Manga) = db.get()
|
||||||
.listOfObjects(Track::class.java)
|
.listOfObjects(Track::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
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.StorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
@ -25,9 +25,10 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
|||||||
.whereArgs(chapter.url)
|
.whereArgs(chapter.url)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
fun mapToContentValues(chapter: Chapter) =
|
||||||
put(ChapterTable.COL_READ, chapter.read)
|
contentValuesOf(
|
||||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
ChapterTable.COL_READ to chapter.read,
|
||||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||||
}
|
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
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.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
|
|
||||||
|
class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(chapter)
|
||||||
|
val contentValues = mapToContentValues(chapter)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_ID} = ?")
|
||||||
|
.whereArgs(chapter.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(chapter: Chapter) =
|
||||||
|
contentValuesOf(
|
||||||
|
ChapterTable.COL_READ to chapter.read,
|
||||||
|
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||||
|
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
||||||
|
)
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
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.StorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
@ -25,9 +25,10 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
|||||||
.whereArgs(chapter.id)
|
.whereArgs(chapter.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
fun mapToContentValues(chapter: Chapter) =
|
||||||
put(ChapterTable.COL_READ, chapter.read)
|
contentValuesOf(
|
||||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
ChapterTable.COL_READ to chapter.read,
|
||||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||||
}
|
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
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.StorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
@ -25,7 +25,8 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
|
|||||||
.whereArgs(chapter.url, chapter.manga_id)
|
.whereArgs(chapter.url, chapter.manga_id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
|
fun mapToContentValues(chapter: Chapter) =
|
||||||
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
|
contentValuesOf(
|
||||||
}
|
ChapterTable.COL_SOURCE_ORDER to chapter.source_order
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import androidx.annotation.NonNull
|
import androidx.annotation.NonNull
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
@ -57,7 +57,8 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
* Create content query
|
* Create content query
|
||||||
* @param history object
|
* @param history object
|
||||||
*/
|
*/
|
||||||
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
|
fun mapToUpdateContentValues(history: History) =
|
||||||
put(HistoryTable.COL_LAST_READ, history.last_read)
|
contentValuesOf(
|
||||||
}
|
HistoryTable.COL_LAST_READ to history.last_read
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
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.StorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
@ -25,7 +25,8 @@ class MangaCoverLastModifiedPutResolver : PutResolver<Manga>() {
|
|||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) =
|
||||||
put(MangaTable.COL_COVER_LAST_MODIFIED, manga.cover_last_modified)
|
contentValuesOf(
|
||||||
}
|
MangaTable.COL_COVER_LAST_MODIFIED to manga.cover_last_modified
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
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.StorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
@ -25,7 +25,8 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
|
|||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) =
|
||||||
put(MangaTable.COL_FAVORITE, manga.favorite)
|
contentValuesOf(
|
||||||
}
|
MangaTable.COL_FAVORITE to manga.favorite
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
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.StorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
class MangaFlagsPutResolver : PutResolver<Manga>() {
|
class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
@ -19,13 +19,24 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
|
|||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
fun mapToUpdateQuery(manga: Manga): UpdateQuery {
|
||||||
.table(MangaTable.TABLE)
|
val builder = UpdateQuery.builder()
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
return if (updateAll) {
|
||||||
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)
|
builder
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
builder
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun mapToContentValues(manga: Manga) =
|
||||||
|
contentValuesOf(
|
||||||
|
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
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.StorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
@ -25,7 +25,8 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
|
|||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) =
|
||||||
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
|
contentValuesOf(
|
||||||
}
|
MangaTable.COL_LAST_UPDATE to manga.last_update
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
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.StorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
@ -25,7 +25,8 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
|
|||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) =
|
||||||
put(MangaTable.COL_TITLE, manga.title)
|
contentValuesOf(
|
||||||
}
|
MangaTable.COL_TITLE to manga.title
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
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.StorIOSQLite
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
@ -25,7 +25,8 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
|
|||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) =
|
||||||
put(MangaTable.COL_VIEWER, manga.viewer)
|
contentValuesOf(
|
||||||
}
|
MangaTable.COL_VIEWER to manga.viewer
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,10 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache where we dump the downloads directory from the filesystem. This class is needed because
|
* Cache where we dump the downloads directory from the filesystem. This class is needed because
|
||||||
@ -128,7 +128,7 @@ class DownloadCache(
|
|||||||
.orEmpty()
|
.orEmpty()
|
||||||
.associate { it.name to SourceDirectory(it) }
|
.associate { it.name to SourceDirectory(it) }
|
||||||
.mapNotNullKeys { entry ->
|
.mapNotNullKeys { entry ->
|
||||||
onlineSources.find { provider.getSourceDirName(it).toLowerCase() == entry.key?.toLowerCase() }?.id
|
onlineSources.find { provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) }?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
rootDir.files = sourceDirs
|
rootDir.files = sourceDirs
|
||||||
|
@ -173,6 +173,17 @@ class DownloadManager(private val context: Context) {
|
|||||||
return cache.isChapterDownloaded(chapter, manga, skipCache)
|
return cache.isChapterDownloaded(chapter, manga, skipCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the download from queue if the chapter is queued for download
|
||||||
|
* else it will return null which means that the chapter is not queued for download
|
||||||
|
*
|
||||||
|
* @param chapter the chapter to check.
|
||||||
|
*/
|
||||||
|
fun getChapterDownloadOrNull(chapter: Chapter): Download? {
|
||||||
|
return downloader.queue
|
||||||
|
.firstOrNull { it.chapter.id == chapter.id && it.chapter.manga_id == chapter.manga_id }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the amount of downloaded chapters for a manga.
|
* Returns the amount of downloaded chapters for a manga.
|
||||||
*
|
*
|
||||||
@ -198,14 +209,10 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
* @param source the source of the chapters.
|
* @param source the source of the chapters.
|
||||||
*/
|
*/
|
||||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
|
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
||||||
queue.remove(chapters)
|
val filteredChapters = getChaptersToDelete(chapters)
|
||||||
|
|
||||||
val filteredChapters = if (!preferences.removeBookmarkedChapters()) {
|
removeFromDownloadQueue(filteredChapters)
|
||||||
chapters.filterNot { it.bookmark }
|
|
||||||
} else {
|
|
||||||
chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||||
chapterDirs.forEach { it.delete() }
|
chapterDirs.forEach { it.delete() }
|
||||||
@ -213,6 +220,26 @@ class DownloadManager(private val context: Context) {
|
|||||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return filteredChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeFromDownloadQueue(chapters: List<Chapter>) {
|
||||||
|
val wasRunning = downloader.isRunning
|
||||||
|
if (wasRunning) {
|
||||||
|
downloader.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
downloader.queue.remove(chapters)
|
||||||
|
|
||||||
|
if (wasRunning) {
|
||||||
|
if (downloader.queue.isEmpty()) {
|
||||||
|
DownloadService.stop(context)
|
||||||
|
downloader.stop()
|
||||||
|
} else if (downloader.queue.isNotEmpty()) {
|
||||||
|
downloader.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -222,7 +249,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param source the source of the manga.
|
* @param source the source of the manga.
|
||||||
*/
|
*/
|
||||||
fun deleteManga(manga: Manga, source: Source) {
|
fun deleteManga(manga: Manga, source: Source) {
|
||||||
queue.remove(manga)
|
downloader.queue.remove(manga)
|
||||||
provider.findMangaDir(manga, source)?.delete()
|
provider.findMangaDir(manga, source)?.delete()
|
||||||
cache.removeManga(manga)
|
cache.removeManga(manga)
|
||||||
}
|
}
|
||||||
@ -234,7 +261,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
*/
|
*/
|
||||||
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
||||||
pendingDeleter.addChapters(chapters, manga)
|
pendingDeleter.addChapters(getChaptersToDelete(chapters), manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -273,4 +300,12 @@ class DownloadManager(private val context: Context) {
|
|||||||
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getChaptersToDelete(chapters: List<Chapter>): List<Chapter> {
|
||||||
|
return if (!preferences.removeBookmarkedChapters()) {
|
||||||
|
chapters.filterNot { it.bookmark }
|
||||||
|
} else {
|
||||||
|
chapters
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.util.lang.chop
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import java.util.regex.Pattern
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
||||||
@ -66,15 +66,6 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
context.notificationManager.notify(id, build())
|
context.notificationManager.notify(id, build())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear old actions if they exist.
|
|
||||||
*/
|
|
||||||
private fun NotificationCompat.Builder.clearActions() {
|
|
||||||
if (mActions.isNotEmpty()) {
|
|
||||||
mActions.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
|
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
|
||||||
* those can only be dismissed by the user.
|
* those can only be dismissed by the user.
|
||||||
@ -107,7 +98,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val downloadingProgressText = context.getString(
|
val downloadingProgressText = context.getString(
|
||||||
R.string.chapter_downloading_progress, download.downloadedImages, download.pages!!.size
|
R.string.chapter_downloading_progress,
|
||||||
|
download.downloadedImages,
|
||||||
|
download.pages!!.size
|
||||||
)
|
)
|
||||||
|
|
||||||
if (preferences.hideNotificationContent()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
@ -163,6 +156,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* This function shows a notification to inform download tasks are done.
|
* This function shows a notification to inform download tasks are done.
|
||||||
*/
|
*/
|
||||||
fun onComplete() {
|
fun onComplete() {
|
||||||
|
dismissProgress()
|
||||||
|
|
||||||
if (!errorThrown) {
|
if (!errorThrown) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
|
@ -2,10 +2,12 @@ package eu.kanade.tachiyomi.data.download
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,10 +17,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*/
|
*/
|
||||||
class DownloadPendingDeleter(context: Context) {
|
class DownloadPendingDeleter(context: Context) {
|
||||||
|
|
||||||
/**
|
private val json: Json by injectLazy()
|
||||||
* Gson instance to encode and decode chapters.
|
|
||||||
*/
|
|
||||||
private val gson by injectLazy<Gson>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preferences used to store the list of chapters to delete.
|
* Preferences used to store the list of chapters to delete.
|
||||||
@ -53,7 +52,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
val existingEntry = preferences.getString(manga.id!!.toString(), null)
|
val existingEntry = preferences.getString(manga.id!!.toString(), null)
|
||||||
if (existingEntry != null) {
|
if (existingEntry != null) {
|
||||||
// Existing entry found on preferences, decode json and add the new chapter
|
// Existing entry found on preferences, decode json and add the new chapter
|
||||||
val savedEntry = gson.fromJson<Entry>(existingEntry)
|
val savedEntry = json.decodeFromString<Entry>(existingEntry)
|
||||||
|
|
||||||
// Append new chapters
|
// Append new chapters
|
||||||
val newChapters = savedEntry.chapters.addUniqueById(chapters)
|
val newChapters = savedEntry.chapters.addUniqueById(chapters)
|
||||||
@ -69,7 +68,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save current state
|
// Save current state
|
||||||
val json = gson.toJson(newEntry)
|
val json = json.encodeToString(newEntry)
|
||||||
preferences.edit {
|
preferences.edit {
|
||||||
putString(newEntry.manga.id.toString(), json)
|
putString(newEntry.manga.id.toString(), json)
|
||||||
}
|
}
|
||||||
@ -90,8 +89,8 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
}
|
}
|
||||||
lastAddedEntry = null
|
lastAddedEntry = null
|
||||||
|
|
||||||
return entries.associate { entry ->
|
return entries.associate { (chapters, manga) ->
|
||||||
entry.manga.toModel() to entry.chapters.map { it.toModel() }
|
manga.toModel() to chapters.map { it.toModel() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +100,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
private fun decodeAll(): List<Entry> {
|
private fun decodeAll(): List<Entry> {
|
||||||
return preferences.all.values.mapNotNull { rawEntry ->
|
return preferences.all.values.mapNotNull { rawEntry ->
|
||||||
try {
|
try {
|
||||||
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
|
(rawEntry as? String)?.let { json.decodeFromString<Entry>(it) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@ -124,6 +123,7 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Class used to save an entry of chapters with their manga into preferences.
|
* Class used to save an entry of chapters with their manga into preferences.
|
||||||
*/
|
*/
|
||||||
|
@Serializable
|
||||||
private data class Entry(
|
private data class Entry(
|
||||||
val chapters: List<ChapterEntry>,
|
val chapters: List<ChapterEntry>,
|
||||||
val manga: MangaEntry
|
val manga: MangaEntry
|
||||||
@ -132,16 +132,18 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Class used to save an entry for a chapter into preferences.
|
* Class used to save an entry for a chapter into preferences.
|
||||||
*/
|
*/
|
||||||
|
@Serializable
|
||||||
private data class ChapterEntry(
|
private data class ChapterEntry(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val scanlator: String?
|
val scanlator: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to save an entry for a manga into preferences.
|
* Class used to save an entry for a manga into preferences.
|
||||||
*/
|
*/
|
||||||
|
@Serializable
|
||||||
private data class MangaEntry(
|
private data class MangaEntry(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
|
@ -9,11 +9,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,7 +25,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
private val scope = MainScope()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The root directory for downloads.
|
* The root directory for downloads.
|
||||||
@ -55,6 +54,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
.createDirectory(getSourceDirName(source))
|
.createDirectory(getSourceDirName(source))
|
||||||
.createDirectory(getMangaDirName(manga))
|
.createDirectory(getMangaDirName(manga))
|
||||||
} catch (e: NullPointerException) {
|
} catch (e: NullPointerException) {
|
||||||
|
Timber.w(e)
|
||||||
throw Exception(context.getString(R.string.invalid_download_dir))
|
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,9 @@ import android.content.Intent
|
|||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkInfo.State.CONNECTED
|
import android.net.NetworkInfo.State.CONNECTED
|
||||||
import android.net.NetworkInfo.State.DISCONNECTED
|
import android.net.NetworkInfo.State.DISCONNECTED
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.github.pwittchen.reactivenetwork.library.Connectivity
|
import com.github.pwittchen.reactivenetwork.library.Connectivity
|
||||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
@ -47,11 +47,7 @@ class DownloadService : Service() {
|
|||||||
*/
|
*/
|
||||||
fun start(context: Context) {
|
fun start(context: Context) {
|
||||||
val intent = Intent(context, DownloadService::class.java)
|
val intent = Intent(context, DownloadService::class.java)
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
ContextCompat.startForegroundService(context, intent)
|
||||||
context.startService(intent)
|
|
||||||
} else {
|
|
||||||
context.startForegroundService(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,12 +2,15 @@ package eu.kanade.tachiyomi.data.download
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.google.gson.Gson
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,11 +28,7 @@ class DownloadStore(
|
|||||||
*/
|
*/
|
||||||
private val preferences = context.getSharedPreferences("active_downloads", Context.MODE_PRIVATE)
|
private val preferences = context.getSharedPreferences("active_downloads", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
/**
|
private val json: Json by injectLazy()
|
||||||
* Gson instance to serialize/deserialize downloads.
|
|
||||||
*/
|
|
||||||
private val gson: Gson by injectLazy()
|
|
||||||
|
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,7 +110,7 @@ class DownloadStore(
|
|||||||
*/
|
*/
|
||||||
private fun serialize(download: Download): String {
|
private fun serialize(download: Download): String {
|
||||||
val obj = DownloadObject(download.manga.id!!, download.chapter.id!!, counter++)
|
val obj = DownloadObject(download.manga.id!!, download.chapter.id!!, counter++)
|
||||||
return gson.toJson(obj)
|
return json.encodeToString(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -121,7 +120,7 @@ class DownloadStore(
|
|||||||
*/
|
*/
|
||||||
private fun deserialize(string: String): DownloadObject? {
|
private fun deserialize(string: String): DownloadObject? {
|
||||||
return try {
|
return try {
|
||||||
gson.fromJson(string, DownloadObject::class.java)
|
json.decodeFromString<DownloadObject>(string)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@ -134,5 +133,6 @@ class DownloadStore(
|
|||||||
* @param chapterId the id of the chapter.
|
* @param chapterId the id of the chapter.
|
||||||
* @param order the order of the download in the queue.
|
* @param order the order of the download in the queue.
|
||||||
*/
|
*/
|
||||||
|
@Serializable
|
||||||
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
|
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,12 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||||
import eu.kanade.tachiyomi.util.lang.RetryWithDelay
|
import eu.kanade.tachiyomi.util.lang.RetryWithDelay
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
|
||||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
import java.io.File
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -31,6 +30,7 @@ import rx.schedulers.Schedulers
|
|||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is the one in charge of downloading chapters.
|
* This class is the one in charge of downloading chapters.
|
||||||
@ -114,8 +114,8 @@ class Downloader(
|
|||||||
initializeSubscriptions()
|
initializeSubscriptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
val pending = queue.filter { it.status != Download.State.DOWNLOADED }
|
||||||
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
pending.forEach { if (it.status != Download.State.QUEUE) it.status = Download.State.QUEUE }
|
||||||
|
|
||||||
notifier.paused = false
|
notifier.paused = false
|
||||||
|
|
||||||
@ -129,20 +129,21 @@ class Downloader(
|
|||||||
fun stop(reason: String? = null) {
|
fun stop(reason: String? = null) {
|
||||||
destroySubscriptions()
|
destroySubscriptions()
|
||||||
queue
|
queue
|
||||||
.filter { it.status == Download.DOWNLOADING }
|
.filter { it.status == Download.State.DOWNLOADING }
|
||||||
.forEach { it.status = Download.ERROR }
|
.forEach { it.status = Download.State.ERROR }
|
||||||
|
|
||||||
if (reason != null) {
|
if (reason != null) {
|
||||||
notifier.onWarning(reason)
|
notifier.onWarning(reason)
|
||||||
} else {
|
return
|
||||||
if (notifier.paused) {
|
|
||||||
notifier.paused = false
|
|
||||||
notifier.onPaused()
|
|
||||||
} else {
|
|
||||||
notifier.dismissProgress()
|
|
||||||
notifier.onComplete()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (notifier.paused && !queue.isEmpty()) {
|
||||||
|
notifier.onPaused()
|
||||||
|
} else {
|
||||||
|
notifier.onComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
notifier.paused = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -151,8 +152,8 @@ class Downloader(
|
|||||||
fun pause() {
|
fun pause() {
|
||||||
destroySubscriptions()
|
destroySubscriptions()
|
||||||
queue
|
queue
|
||||||
.filter { it.status == Download.DOWNLOADING }
|
.filter { it.status == Download.State.DOWNLOADING }
|
||||||
.forEach { it.status = Download.QUEUE }
|
.forEach { it.status = Download.State.QUEUE }
|
||||||
notifier.paused = true
|
notifier.paused = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,8 +168,8 @@ class Downloader(
|
|||||||
// Needed to update the chapter view
|
// Needed to update the chapter view
|
||||||
if (isNotification) {
|
if (isNotification) {
|
||||||
queue
|
queue
|
||||||
.filter { it.status == Download.QUEUE }
|
.filter { it.status == Download.State.QUEUE }
|
||||||
.forEach { it.status = Download.NOT_DOWNLOADED }
|
.forEach { it.status = Download.State.NOT_DOWNLOADED }
|
||||||
}
|
}
|
||||||
queue.clear()
|
queue.clear()
|
||||||
notifier.dismissProgress()
|
notifier.dismissProgress()
|
||||||
@ -227,8 +228,8 @@ class Downloader(
|
|||||||
* @param chapters the list of chapters to download.
|
* @param chapters the list of chapters to download.
|
||||||
* @param autoStart whether to start the downloader after enqueing the chapters.
|
* @param autoStart whether to start the downloader after enqueing the chapters.
|
||||||
*/
|
*/
|
||||||
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
|
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchIO {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO
|
||||||
val wasEmpty = queue.isEmpty()
|
val wasEmpty = queue.isEmpty()
|
||||||
// Called in background thread, the operation can be slow with SAF.
|
// Called in background thread, the operation can be slow with SAF.
|
||||||
val chaptersWithoutDir = async {
|
val chaptersWithoutDir = async {
|
||||||
@ -271,7 +272,7 @@ class Downloader(
|
|||||||
|
|
||||||
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
||||||
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
||||||
download.status = Download.ERROR
|
download.status = Download.State.ERROR
|
||||||
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
||||||
return@defer Observable.just(download)
|
return@defer Observable.just(download)
|
||||||
}
|
}
|
||||||
@ -301,7 +302,7 @@ class Downloader(
|
|||||||
?.forEach { it.delete() }
|
?.forEach { it.delete() }
|
||||||
|
|
||||||
download.downloadedImages = 0
|
download.downloadedImages = 0
|
||||||
download.status = Download.DOWNLOADING
|
download.status = Download.State.DOWNLOADING
|
||||||
}
|
}
|
||||||
// Get all the URLs to the source images, fetch pages if necessary
|
// Get all the URLs to the source images, fetch pages if necessary
|
||||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||||
@ -317,7 +318,7 @@ class Downloader(
|
|||||||
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
|
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
|
||||||
// If the page list threw, it will resume here
|
// If the page list threw, it will resume here
|
||||||
.onErrorReturn { error ->
|
.onErrorReturn { error ->
|
||||||
download.status = Download.ERROR
|
download.status = Download.State.ERROR
|
||||||
notifier.onError(error.message, download.chapter.name)
|
notifier.onError(error.message, download.chapter.name)
|
||||||
download
|
download
|
||||||
}
|
}
|
||||||
@ -457,13 +458,13 @@ class Downloader(
|
|||||||
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
||||||
|
|
||||||
download.status = if (downloadedImages.size == download.pages!!.size) {
|
download.status = if (downloadedImages.size == download.pages!!.size) {
|
||||||
Download.DOWNLOADED
|
Download.State.DOWNLOADED
|
||||||
} else {
|
} else {
|
||||||
Download.ERROR
|
Download.State.ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only rename the directory if it's downloaded.
|
// Only rename the directory if it's downloaded.
|
||||||
if (download.status == Download.DOWNLOADED) {
|
if (download.status == Download.State.DOWNLOADED) {
|
||||||
tmpDir.renameTo(dirname)
|
tmpDir.renameTo(dirname)
|
||||||
cache.addChapter(dirname, mangaDir, download.manga)
|
cache.addChapter(dirname, mangaDir, download.manga)
|
||||||
|
|
||||||
@ -476,7 +477,7 @@ class Downloader(
|
|||||||
*/
|
*/
|
||||||
private fun completeDownload(download: Download) {
|
private fun completeDownload(download: Download) {
|
||||||
// Delete successful downloads from queue
|
// Delete successful downloads from queue
|
||||||
if (download.status == Download.DOWNLOADED) {
|
if (download.status == Download.State.DOWNLOADED) {
|
||||||
// remove downloaded chapter from queue
|
// remove downloaded chapter from queue
|
||||||
queue.remove(download)
|
queue.remove(download)
|
||||||
}
|
}
|
||||||
@ -489,7 +490,7 @@ class Downloader(
|
|||||||
* Returns true if all the queued downloads are in DOWNLOADED or ERROR state.
|
* Returns true if all the queued downloads are in DOWNLOADED or ERROR state.
|
||||||
*/
|
*/
|
||||||
private fun areAllDownloadsFinished(): Boolean {
|
private fun areAllDownloadsFinished(): Boolean {
|
||||||
return queue.none { it.status <= Download.DOWNLOADING }
|
return queue.none { it.status.value <= Download.State.DOWNLOADING.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -20,7 +20,7 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
|||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
@Transient
|
@Transient
|
||||||
var status: Int = 0
|
var status: State = State.NOT_DOWNLOADED
|
||||||
set(status) {
|
set(status) {
|
||||||
field = status
|
field = status
|
||||||
statusSubject?.onNext(this)
|
statusSubject?.onNext(this)
|
||||||
@ -47,11 +47,11 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
|||||||
statusCallback = f
|
statusCallback = f
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
enum class State(val value: Int) {
|
||||||
const val NOT_DOWNLOADED = 0
|
NOT_DOWNLOADED(0),
|
||||||
const val QUEUE = 1
|
QUEUE(1),
|
||||||
const val DOWNLOADING = 2
|
DOWNLOADING(2),
|
||||||
const val DOWNLOADED = 3
|
DOWNLOADED(3),
|
||||||
const val ERROR = 4
|
ERROR(4),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,9 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadStore
|
import eu.kanade.tachiyomi.data.download.DownloadStore
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
class DownloadQueue(
|
class DownloadQueue(
|
||||||
private val store: DownloadStore,
|
private val store: DownloadStore,
|
||||||
@ -22,7 +22,7 @@ class DownloadQueue(
|
|||||||
downloads.forEach { download ->
|
downloads.forEach { download ->
|
||||||
download.setStatusSubject(statusSubject)
|
download.setStatusSubject(statusSubject)
|
||||||
download.setStatusCallback(::setPagesFor)
|
download.setStatusCallback(::setPagesFor)
|
||||||
download.status = Download.QUEUE
|
download.status = Download.State.QUEUE
|
||||||
}
|
}
|
||||||
queue.addAll(downloads)
|
queue.addAll(downloads)
|
||||||
store.addAll(downloads)
|
store.addAll(downloads)
|
||||||
@ -34,8 +34,8 @@ class DownloadQueue(
|
|||||||
store.remove(download)
|
store.remove(download)
|
||||||
download.setStatusSubject(null)
|
download.setStatusSubject(null)
|
||||||
download.setStatusCallback(null)
|
download.setStatusCallback(null)
|
||||||
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
|
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||||
download.status = Download.NOT_DOWNLOADED
|
download.status = Download.State.NOT_DOWNLOADED
|
||||||
}
|
}
|
||||||
if (removed) {
|
if (removed) {
|
||||||
updatedRelay.call(Unit)
|
updatedRelay.call(Unit)
|
||||||
@ -60,8 +60,8 @@ class DownloadQueue(
|
|||||||
queue.forEach { download ->
|
queue.forEach { download ->
|
||||||
download.setStatusSubject(null)
|
download.setStatusSubject(null)
|
||||||
download.setStatusCallback(null)
|
download.setStatusCallback(null)
|
||||||
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
|
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||||
download.status = Download.NOT_DOWNLOADED
|
download.status = Download.State.NOT_DOWNLOADED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
queue.clear()
|
queue.clear()
|
||||||
@ -70,7 +70,7 @@ class DownloadQueue(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getActiveDownloads(): Observable<Download> =
|
fun getActiveDownloads(): Observable<Download> =
|
||||||
Observable.from(this).filter { download -> download.status == Download.DOWNLOADING }
|
Observable.from(this).filter { download -> download.status == Download.State.DOWNLOADING }
|
||||||
|
|
||||||
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
|
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ class DownloadQueue(
|
|||||||
.map { this }
|
.map { this }
|
||||||
|
|
||||||
private fun setPagesFor(download: Download) {
|
private fun setPagesFor(download: Download) {
|
||||||
if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
|
||||||
setPagesSubject(download.pages, null)
|
setPagesSubject(download.pages, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,19 +88,19 @@ class DownloadQueue(
|
|||||||
return statusSubject.onBackpressureBuffer()
|
return statusSubject.onBackpressureBuffer()
|
||||||
.startWith(getActiveDownloads())
|
.startWith(getActiveDownloads())
|
||||||
.flatMap { download ->
|
.flatMap { download ->
|
||||||
if (download.status == Download.DOWNLOADING) {
|
if (download.status == Download.State.DOWNLOADING) {
|
||||||
val pageStatusSubject = PublishSubject.create<Int>()
|
val pageStatusSubject = PublishSubject.create<Int>()
|
||||||
setPagesSubject(download.pages, pageStatusSubject)
|
setPagesSubject(download.pages, pageStatusSubject)
|
||||||
return@flatMap pageStatusSubject
|
return@flatMap pageStatusSubject
|
||||||
.onBackpressureBuffer()
|
.onBackpressureBuffer()
|
||||||
.filter { it == Page.READY }
|
.filter { it == Page.READY }
|
||||||
.map { download }
|
.map { download }
|
||||||
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
} else if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
|
||||||
setPagesSubject(download.pages, null)
|
setPagesSubject(download.pages, null)
|
||||||
}
|
}
|
||||||
Observable.just(download)
|
Observable.just(download)
|
||||||
}
|
}
|
||||||
.filter { it.status == Download.DOWNLOADING }
|
.filter { it.status == Download.State.DOWNLOADING }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
||||||
|