mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 02:34:39 +00:00
Compare commits
615 commits
Author | SHA1 | Date | |
---|---|---|---|
17879ddc5a | |||
7f83c117be | |||
abbe606473 | |||
7ac42d5545 | |||
|
f90e2a1425 | ||
f604e4e256 | |||
a04ea9f5ea | |||
43d4d5404d | |||
|
54df9436b8 | ||
|
cd5cdbe746 | ||
|
41662979fe | ||
|
f68e9df74d | ||
|
a77d315922 | ||
|
4e2c4aef8a | ||
|
d80b53ba78 | ||
|
a1f6eb6524 | ||
|
9f256bb8c6 | ||
|
1d49d65961 | ||
|
9ccdd36c46 | ||
|
48938f02dd | ||
|
63435b933a | ||
|
ef49bf3321 | ||
|
c05ba1a8fb | ||
|
764d52a729 | ||
|
af2be0d2d0 | ||
|
733fcbba4a | ||
|
b6e1cabc59 | ||
|
3867aabff1 | ||
|
ca6bb95b84 | ||
|
4b7564e410 | ||
|
8d3cfffa66 | ||
|
97339689c6 | ||
|
9453c3e808 | ||
|
6f03935c17 | ||
|
f1597bd95c | ||
|
903a37e390 | ||
|
1655540a16 | ||
|
6a7b386127 | ||
|
75191dde05 | ||
|
db0af71901 | ||
|
89c5e997cc | ||
e22559b2df | |||
|
370bb62ef9 | ||
|
18528fbd92 | ||
7964ac87c6 | |||
850151720b | |||
d3050d5799 | |||
c7d2ff0970 | |||
524c00fd44 | |||
93f819c236 | |||
1c73b925b1 | |||
|
ea179979b1 | ||
71c26b77fc | |||
43a5e8edd8 | |||
|
33d7c3cd2b | ||
0e0e865cb9 | |||
|
2b2e0491e8 | ||
f035454150 | |||
96f88d5e90 | |||
f362b0bda0 | |||
f74662c0f3 | |||
65639391b7 | |||
|
94c314559b | ||
66241774dc | |||
ea634a5ce3 | |||
271e440014 | |||
|
8be33e0f81 | ||
f13f98f19a | |||
7a08ca294a | |||
4faa641739 | |||
|
ebd891fa75 | ||
|
d46f5fb73e | ||
|
d6c5a9a7c2 | ||
|
2208a81013 | ||
0bf55a8ca0 | |||
4dd8aece0c | |||
ece849b008 | |||
d2ddf7dfb0 | |||
8b53e5ad10 | |||
63cdf247b4 | |||
9ed12ef07c | |||
86b01a297f | |||
d6ffbe15ee | |||
915ce20904 | |||
9e5d13f261 | |||
33fa77d527 | |||
baaa841278 | |||
453ea32bc9 | |||
f37e657a9b | |||
258708b038 | |||
c6da3325b3 | |||
c9a90f6847 | |||
9cf1fbb118 | |||
f01ace94be | |||
ad22250265 | |||
48c2ad9b33 | |||
7d9c0faf86 | |||
6614bd3ed8 | |||
c6c40ffb71 | |||
d0d322fd67 | |||
d655c3e09a | |||
6a680facd5 | |||
0565fc2665 | |||
568859891a | |||
7fc95e3029 | |||
eebc3dc822 | |||
968639a59b | |||
|
cae0332ef9 | ||
e415fd4ef2 | |||
a3672be728 | |||
e06b28a60e | |||
eba5aa1d2e | |||
a554c079fb | |||
49b10c1b4f | |||
1a16d84e61 | |||
fc87410d46 | |||
84d2924a82 | |||
03e1953c9f | |||
|
02c2b370b4 | ||
fa84ce8fe8 | |||
42dd857d94 | |||
2cf2fcfc4f | |||
b4377a4609 | |||
54a3059730 | |||
672d364f43 | |||
b19480de1a | |||
3c00a249c3 | |||
|
012407aede | ||
1461f048dd | |||
85af94d810 | |||
d02f1bdd11 | |||
1b92ae2e5f | |||
ac0d2e9fc0 | |||
8a28d1d484 | |||
3399d6a326 | |||
6747795690 | |||
e554513392 | |||
f7e5abba59 | |||
a7874f2f29 | |||
2f2ccac8e7 | |||
dd6a2f377a | |||
f8807f81b1 | |||
7ee9c7a746 | |||
67c4500cce | |||
cd4079aa4b | |||
c6a86d773b | |||
cab40214d2 | |||
37f1f0e330 | |||
310e90beb5 | |||
9bb869111d | |||
8a9d8166af | |||
71dcb2ab85 | |||
60ef9482c8 | |||
1cc8abb599 | |||
96348dbf7d | |||
23d4fb1fdd | |||
f78d4e9e6a | |||
120d2cfb96 | |||
dec1a70091 | |||
448c93365a | |||
55fad67223 | |||
c09c4045e2 | |||
b201e410a3 | |||
915debd41b | |||
6d2ae386a2 | |||
a853deae4a | |||
f9bb2b96cb | |||
031e30e227 | |||
0049653355 | |||
677d96eed5 | |||
640feb69ac | |||
f3cac7cac8 | |||
99bec41056 | |||
afb7e79ea4 | |||
76af51a319 | |||
5824ac81c2 | |||
b1665eaedf | |||
2b953a53d8 | |||
37f47ae2f5 | |||
3fa475c1cf | |||
|
bcf21858ee | ||
90f5dfc55a | |||
|
365d259e94 | ||
09bd1a9f78 | |||
f8e7002a9b | |||
4caefff11d | |||
07f9e921f1 | |||
|
4256e86e97 | ||
fdb69a1c7d | |||
e65497baef | |||
acafe931f1 | |||
bafd9e54aa | |||
a6e5b41d7d | |||
629f1891f6 | |||
bfbbd1b4f3 | |||
33a84f7e39 | |||
fe666b614f | |||
f240fe0dd4 | |||
3787845893 | |||
3606f67dba | |||
9e5262140e | |||
c73d0f843f | |||
b974eff320 | |||
8ac8187977 | |||
778bd05e21 | |||
3d2e2b2774 | |||
c1cb7a2066 | |||
2299aaac63 | |||
f985ad6daa | |||
12002f62cd | |||
eea87eede4 | |||
2466d3a493 | |||
bf8eccc186 | |||
1571678ddb | |||
cd8ff6f898 | |||
985ac6d7a8 | |||
17eec5f6aa | |||
33332110f1 | |||
659ba89218 | |||
02296985d6 | |||
b3e69bdb28 | |||
6c40fe92be | |||
18c5a68981 | |||
cba97eb94d | |||
0d205f4a6d | |||
fcaff92db1 | |||
5fad2c0154 | |||
bbfa8c8bc4 | |||
8020f6591c | |||
a404eb2c83 | |||
8c9208c3b6 | |||
2dfc1e3451 | |||
4ffb0ad8ee | |||
4a0f578211 | |||
60fe907cc0 | |||
f81be429df | |||
f8d74a6b2f | |||
2ef1195a90 | |||
50cad86c0c | |||
8ec29bf755 | |||
3651c2a853 | |||
2461b93af5 | |||
301acb9f4d | |||
4fc18b4913 | |||
e5b8ed9e9d | |||
263603616e | |||
6c1d8d5011 | |||
247ed3bca7 | |||
c7b6e8ee00 | |||
d99476f9bf | |||
834958a819 | |||
1bbfd97b30 | |||
59a8bfc7aa | |||
93e5effaaf | |||
0f54d5e59d | |||
316bc87a5c | |||
82b73bce76 | |||
71a9e2493b | |||
abcf06b921 | |||
e604c951ed | |||
62e26d1f38 | |||
ee2acf8b98 | |||
8c5b54df5f | |||
|
00aa93d189 | ||
bfee1de3b1 | |||
|
fbe9760616 | ||
|
86f5e743e1 | ||
8f45148a9e | |||
|
09621111bf | ||
5fe72d4cb5 | |||
5b637fae8f | |||
336579bd35 | |||
d61052485f | |||
|
16316d810b | ||
37535d3bcf | |||
39775ea308 | |||
1c996f9a59 | |||
ea04968581 | |||
365875590f | |||
1bc107f26b | |||
eeb572740a | |||
dbf5a7efcd | |||
b4e3dcfdda | |||
8e7f5d8897 | |||
87c13b44ab | |||
8c8b2f9634 | |||
88959d956f | |||
53f8a37c8e | |||
05b20e00e0 | |||
158049d4f4 | |||
119b1c64b2 | |||
1cb635999e | |||
160a7109da | |||
618109c80e | |||
|
9918c407c8 | ||
|
d780d6ceb1 | ||
|
ee52e6ecf7 | ||
|
4cdcf62351 | ||
|
1504d94f52 | ||
|
e1bf13f1d9 | ||
|
c6f6718d30 | ||
|
41319660f6 | ||
6935ae545c | |||
fe59d7f4ec | |||
e45baf6ab4 | |||
d3c98fb897 | |||
22978ab8bf | |||
07ed81454f | |||
6c111a1247 | |||
c357f6f658 | |||
06c7cc7d17 | |||
4c30881e91 | |||
|
6bd2f0ab9a | ||
2649425259 | |||
1d200f426c | |||
349b9c181a | |||
d7c3aa6b45 | |||
823860a56f | |||
28cbf0b988 | |||
2f8ae26a83 | |||
f114320123 | |||
a4ab7f11e2 | |||
8bc22fec28 | |||
c9eb3023ed | |||
0d264026cf | |||
b3fbc0bf39 | |||
|
c66bf9b280 | ||
2c36be8b8f | |||
5396b0408e | |||
332f3f7ee6 | |||
23db3244ce | |||
|
4c74e4e78b | ||
|
ffac6293f0 | ||
6bb2f5f94e | |||
3b9eb8a30a | |||
0406160452 | |||
cd978388b2 | |||
2e12817735 | |||
cb265d2225 | |||
39d891aa88 | |||
106737371f | |||
19ea7cbebd | |||
6df9e4f745 | |||
b4c6820ca4 | |||
11ef447321 | |||
ca982d93d1 | |||
50fecd5350 | |||
31773876b4 | |||
30d7b389a5 | |||
723abbe520 | |||
8c96e8d4b6 | |||
d2fdbf8717 | |||
ea9407b49d | |||
b9f7d18d3d | |||
fbb4112eac | |||
7f05d16039 | |||
2fd6146d32 | |||
737681173c | |||
29aa80104d | |||
d7e3a970d8 | |||
bd16db8823 | |||
2b639d0630 | |||
1738dfb510 | |||
ef8b81409e | |||
60c5e3f5c1 | |||
6c8ed6dc57 | |||
|
d800f183e7 | ||
f7450b8d17 | |||
9276382c12 | |||
598c4918f1 | |||
efea0103fe | |||
19b1ba76c7 | |||
|
ec36c9faf7 | ||
a940722f2b | |||
844a83b4e1 | |||
bfbc1f6742 | |||
7a20537cf9 | |||
07c1e7e67f | |||
a832e4d6dc | |||
393ccb822e | |||
42b3dce0ef | |||
e8054855ac | |||
726613e6d7 | |||
93ec13f324 | |||
17b07cd836 | |||
8cef33d9c6 | |||
e25f330118 | |||
83cb898068 | |||
333a7eea68 | |||
aae9a68c8b | |||
312f9e197b | |||
da1f60c5c5 | |||
cf06ebdb8b | |||
a4a7f8fb6d | |||
663360c283 | |||
ca41e02fe1 | |||
9c40aadca2 | |||
fa846d68e2 | |||
87bd36d025 | |||
97eacbbaea | |||
|
39428f8c79 | ||
445395200a | |||
291d4ffc35 | |||
c183802096 | |||
|
e9a68f661f | ||
97e72e3b4b | |||
5d2a08f2b0 | |||
34818eb7de | |||
65682bb5bd | |||
0c6f86c1ae | |||
ba837c75e7 | |||
|
5e84586ff5 | ||
a199ff326d | |||
f14118a8c1 | |||
128e14882d | |||
b8a2a4de47 | |||
7df619ac13 | |||
8f9194c4a9 | |||
f9bfb0b423 | |||
4f9e5bfe62 | |||
|
e9d22f4dba | ||
27f4cea4c4 | |||
|
ed23afa32d | ||
|
4c4163f270 | ||
6e76ab125e | |||
00b7ac6c25 | |||
bf7c7f79cb | |||
5378d2a99b | |||
822cfa56a6 | |||
a7a6dc96d3 | |||
060b40e59d | |||
d7160db53a | |||
5fa5815541 | |||
cea9da9c6d | |||
53ea5bafee | |||
b1766ebb94 | |||
5d6d25c261 | |||
|
5735003018 | ||
|
b6edf17860 | ||
|
8063cb2edb | ||
|
350c5f35dd | ||
95504d7582 | |||
|
9322836e48 | ||
37ba0634ac | |||
cdea102fcb | |||
d25a857b7a | |||
b1f2c30892 | |||
d163dc500c | |||
1f17965d24 | |||
|
fd73958923 | ||
27002a20ef | |||
d003e85b6b | |||
74168c0f1d | |||
|
01fcd7d122 | ||
fbe2d8c701 | |||
a2d6ac2a8b | |||
|
736b4e6b68 | ||
78df24e566 | |||
|
fcc234f0ab | ||
|
5996a8a863 | ||
|
fa0e565fa5 | ||
fabc9f4c1b | |||
|
14efd1b96d | ||
6affec4b97 | |||
|
7355ec841a | ||
52ba1b02d3 | |||
54b27a5efa | |||
|
5a12fc3670 | ||
|
87d803812a | ||
|
0df6647324 | ||
|
9d49957d8d | ||
|
f5dce81b64 | ||
|
59193076f1 | ||
|
53498c78c6 | ||
|
392e2895a3 | ||
6d470c0314 | |||
|
2a49f2437b | ||
6dc5c1e608 | |||
|
7ee44f1fe5 | ||
cd3e526fdf | |||
76de509ae5 | |||
dbdafafba7 | |||
e27c527ad9 | |||
0a7a65aa33 | |||
d832f8ce46 | |||
cae3edd77b | |||
c2db3f959f | |||
8469fe2535 | |||
dceac33b7c | |||
231ece7bfb | |||
ca7496bda1 | |||
30726d030c | |||
6e87bee93c | |||
71e7545b29 | |||
5160fabfde | |||
193d50d0d6 | |||
9b45767667 | |||
ad070fd59a | |||
e4cc6505dd | |||
fe2bdc3846 | |||
6ba22b9e45 | |||
1ae32840eb | |||
15ca197c4e | |||
0ebf79fcb0 | |||
|
75c7431b36 | ||
|
812c467cb4 | ||
|
9bda841d98 | ||
|
d8eb9cc6c1 | ||
c2f07fbfc0 | |||
54fdec2fc3 | |||
7fe5e9a767 | |||
f6e5a6c712 | |||
f4bf249477 | |||
|
a33a167e2c | ||
04d0003963 | |||
5ba3600c28 | |||
|
956da54722 | ||
|
26af20f067 | ||
badfd75ebb | |||
61b51756c9 | |||
f370aa7a83 | |||
6e567ea732 | |||
d8f8264b34 | |||
|
6d15f383d1 | ||
0f96f4dbe1 | |||
ec5e86942a | |||
a6f697199d | |||
|
465564f977 | ||
2bb525ca45 | |||
|
fda98a25be | ||
|
7ec631ba0b | ||
|
c744f35ffc | ||
|
47dbc34fad | ||
|
2b09434837 | ||
|
d41c21d128 | ||
5325c4c847 | |||
|
334fcdaf58 | ||
6c1dba1831 | |||
1ca914a792 | |||
|
bade424fb8 | ||
|
b3aa373621 | ||
|
1ef5d70150 | ||
|
63e7cda7b8 | ||
|
b1d63bdf66 | ||
f838dcb243 | |||
dbd007127d | |||
7719847106 | |||
83a8abe07c | |||
233758b219 | |||
a5a5ce8797 | |||
186d95fcf9 | |||
7752f64efb | |||
a8b3d97b14 | |||
3cd29696d4 | |||
58c5a17c50 | |||
5d457a7ae5 | |||
61870c1115 | |||
775829e28b | |||
e248de76d7 | |||
c6c6ed0553 | |||
|
9d858cc810 | ||
|
bc65f17f60 | ||
19f6b26567 | |||
|
33ec0d0f91 | ||
|
0d8276040f | ||
|
353a002eb5 | ||
6e585fd142 | |||
44742854c4 | |||
5271d1b66f | |||
c4e04c11d0 | |||
ae8959d4d1 | |||
|
942ca851d5 | ||
38db95b6de | |||
34ffc2dd85 | |||
3ac6c551d6 | |||
|
aed615fe9c | ||
3b427e8b7c | |||
b26b526b1e | |||
bf1f258455 | |||
0ee76b9a50 | |||
bede8460e7 | |||
|
93738ffaa3 | ||
|
6ee9a41587 | ||
5a1a7063b2 | |||
1925a503d9 | |||
51a5633751 | |||
153268052a | |||
c1dd80bd71 | |||
03043ef01b | |||
84f4a35180 | |||
59f9e556ec | |||
cb9f8fa398 | |||
|
0363d40f2b | ||
|
a73994b652 | ||
08b629d1f5 | |||
70cde33c04 | |||
91a9e081d7 | |||
a27c0edf13 | |||
0d9ffc2206 | |||
6e3eaad481 | |||
3199f07363 | |||
a19b767aff | |||
354ed7ce8a | |||
79929b395e | |||
3d7b6b88be | |||
84ee9213be | |||
94567a37fa | |||
da99cf5cfa | |||
c7405e0b33 | |||
2818bfa82f | |||
ce9a3ea399 | |||
195613cb1e | |||
|
5bd2190980 | ||
2debbc0a10 | |||
d48b4c330a |
644 changed files with 16247 additions and 11236 deletions
|
@ -1,13 +1,28 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style=space
|
||||
insert_final_newline=true
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{json,json5}]
|
||||
indent_size=2
|
||||
[*.xml]
|
||||
indent_size = 4
|
||||
|
||||
# noinspection EditorConfigKeyCorrectness
|
||||
[*.{kt,kts}]
|
||||
indent_size=4
|
||||
ij_kotlin_allow_trailing_comma=true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||
indent_size = 4
|
||||
max_line_length = 120
|
||||
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ij_kotlin_name_count_to_use_star_import = 2147483647
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
||||
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
ktlint_standard_class-signature = disabled
|
||||
ktlint_standard_discouraged-comment-location = disabled
|
||||
ktlint_standard_function-expression-body = disabled
|
||||
ktlint_standard_function-signature = disabled
|
||||
|
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1 +1,2 @@
|
|||
github: [null2264]
|
||||
ko_fi: ziro2264
|
||||
|
|
17
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
17
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
@ -35,9 +35,24 @@ body:
|
|||
required: true
|
||||
- label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help.
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.8.5.5](https://github.com/null2264/yokai/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.9.7.3](https://github.com/null2264/yokai/releases/latest)**.
|
||||
required: true
|
||||
- label: I have checked through the app settings for my feature.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
- type: "textarea"
|
||||
id: "prioritisation"
|
||||
attributes:
|
||||
label: "Is this issue important to you?"
|
||||
description: |
|
||||
**Please do not modify this text area!**
|
||||
|
||||
This template let users to vote with a :+1: reaction if they find it important.
|
||||
This is not a guarantee that highly-requested issues will be fixed first, but it helps us to figure out what's important to users. Please react on other users' issues if you find them important.
|
||||
value: |
|
||||
Add a :+1: [reaction] to [issues you find important].
|
||||
|
||||
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
|
||||
[issues you find important]: https://github.com/null2264/yokai/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc
|
||||
|
|
23
.github/ISSUE_TEMPLATE/issue_report.yml
vendored
23
.github/ISSUE_TEMPLATE/issue_report.yml
vendored
|
@ -94,15 +94,30 @@ body:
|
|||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help.
|
||||
- label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help ([Browse → Extensions → Find the extension → Settings → Tap the `[<>]` icon](https://cdn.aap.my.id/extension-repo-link.png), it *should* redirect you to the maintainer).
|
||||
required: true
|
||||
- label: I am reporting an issue exclusive to this fork. I have also checked that is not an issue on the [main version of Mihon](https://github.com/mihonapp/mihon)
|
||||
- label: I am reporting an issue exclusive to this fork. I have also checked that is not an issue on the [main version of Mihon](https://github.com/mihonapp/mihon).
|
||||
required: true
|
||||
- label: I have tried the [troubleshooting guide](https://mihon.app/help/).
|
||||
- label: I have tried the [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.8.5.5](https://github.com/null2264/yokai/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.9.7.3](https://github.com/null2264/yokai/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
- label: I have filled out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
- type: "textarea"
|
||||
id: "prioritisation"
|
||||
attributes:
|
||||
label: "Is this issue important to you?"
|
||||
description: |
|
||||
**Please do not modify this text area!**
|
||||
|
||||
This template let users to vote with a :+1: reaction if they find it important.
|
||||
This is not a guarantee that highly-requested issues will be fixed first, but it helps us to figure out what's important to users. Please react on other users' issues if you find them important.
|
||||
value: |
|
||||
Add a :+1: [reaction] to [issues you find important].
|
||||
|
||||
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
|
||||
[issues you find important]: https://github.com/null2264/yokai/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc
|
||||
|
|
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
<!--
|
||||
^ Please summarise the changes you have made here ^
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
Add a :+1: [reaction] to [pull requests you find important].
|
||||
|
||||
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
|
||||
[pull requests you find important]: https://github.com/null2264/yokai/pulls?q=is%3Aopen+sort%3Areactions-%2B1-desc
|
25
.github/workflows/build_check.yml
vendored
25
.github/workflows/build_check.yml
vendored
|
@ -4,7 +4,15 @@ concurrency:
|
|||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on: [pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**'
|
||||
- '!**.md'
|
||||
- '!i18n/src/commonMain/moko-resources/**/strings.xml'
|
||||
- '!i18n/src/commonMain/moko-resources/**/plurals.xml'
|
||||
- 'i18n/src/commonMain/moko-resources/base/strings.xml'
|
||||
- 'i18n/src/commonMain/moko-resources/base/plurals.xml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -19,24 +27,27 @@ jobs:
|
|||
|
||||
- name: Setup Android SDK
|
||||
run: |
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
|
||||
uses: null2264/actions/gradle-java-setup@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94
|
||||
with:
|
||||
java: 17
|
||||
distro: adopt
|
||||
distro: temurin
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build and run tests
|
||||
run: ./gradlew assembleStandardRelease testStandardReleaseUnitTest
|
||||
- name: Build the app
|
||||
run: ./gradlew assembleStandardRelease
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew testReleaseUnitTest testStandardReleaseUnitTest
|
||||
|
||||
- name: Publish test report
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
if: success() || failure()
|
||||
with:
|
||||
include_passed: true
|
||||
|
|
47
.github/workflows/build_push.yml
vendored
47
.github/workflows/build_push.yml
vendored
|
@ -40,13 +40,16 @@ jobs:
|
|||
|
||||
- name: Setup Android SDK
|
||||
run: |
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;34.0.0"
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
|
||||
uses: null2264/actions/gradle-java-setup@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94
|
||||
with:
|
||||
java: 17
|
||||
distro: adopt
|
||||
distro: temurin
|
||||
|
||||
- name: Setup CHANGELOG parser
|
||||
uses: taiki-e/install-action@parse-changelog
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
|
@ -62,10 +65,12 @@ jobs:
|
|||
id: changelog
|
||||
shell: bash
|
||||
run: |
|
||||
# extended SemVer (major.minor.patch.hotfix)
|
||||
VERSION_FORMAT='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?(-[0-9A-Za-z\.-]+)?(\+[0-9A-Za-z\.-]+)?$|^Unreleased$'
|
||||
{
|
||||
echo "CHANGELOG<<END_OF_FILE"
|
||||
cat CHANGELOG.md || echo ""
|
||||
echo
|
||||
parse-changelog CHANGELOG.md ${{ github.event.inputs.version == '' && 'Unreleased' || github.event.inputs.version }} --version-format $VERSION_FORMAT || parse-changelog CHANGELOG.md Unreleased --version-format $VERSION_FORMAT || echo "No documented changes so far..."
|
||||
echo ""
|
||||
echo "END_OF_FILE"
|
||||
} >> "$GITHUB_OUTPUT" 2> /dev/null
|
||||
|
||||
|
@ -75,6 +80,7 @@ jobs:
|
|||
run: |
|
||||
set -x
|
||||
echo "VERSION_TAG=v${{github.event.inputs.version}}" >> $GITHUB_ENV
|
||||
echo "BUILD_TYPE=StandardRelease" >> $GITHUB_ENV
|
||||
|
||||
# BETA
|
||||
- name: Prepare beta build
|
||||
|
@ -83,9 +89,14 @@ jobs:
|
|||
set -x
|
||||
|
||||
BETA_COUNT=$(git tag -l --sort=refname "v${{github.event.inputs.version}}-b*" | tail -n1 | sed "s/^\S*-b//g")
|
||||
[ "$BETA_COUNT" = "" ] && BETA_COUNT="1" || BETA_COUNT=$((BETA_COUNT+1))
|
||||
if [ -z "$BETA_COUNT" ]; then
|
||||
BETA_COUNT="1"
|
||||
else
|
||||
BETA_COUNT=$((BETA_COUNT+1))
|
||||
fi
|
||||
|
||||
echo "VERSION_TAG=v${{github.event.inputs.version}}-b${BETA_COUNT}" >> $GITHUB_ENV
|
||||
echo "BUILD_TYPE=StandardBeta" >> $GITHUB_ENV
|
||||
|
||||
# NIGHTLY
|
||||
- name: Prepare nightly build
|
||||
|
@ -93,23 +104,17 @@ jobs:
|
|||
run: |
|
||||
set -x
|
||||
echo "VERSION_TAG=r$(git rev-list --count HEAD)" >> $GITHUB_ENV
|
||||
echo "BUILD_TYPE=StandardNightly" >> $GITHUB_ENV
|
||||
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||
echo "COMMIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
# PROD
|
||||
- name: Build release build and run tests
|
||||
if: startsWith(env.VERSION_TAG, 'v') && github.event.inputs.beta != 'true'
|
||||
run: ./gradlew assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
|
||||
- name: Build the app
|
||||
if: startsWith(env.BUILD_TYPE, 'Standard')
|
||||
run: ./gradlew assemble${{ env.BUILD_TYPE }}
|
||||
|
||||
# BETA
|
||||
- name: Build beta build and run tests
|
||||
if: startsWith(env.VERSION_TAG, 'v') && github.event.inputs.beta == 'true'
|
||||
run: ./gradlew assembleStandardBeta testReleaseUnitTest testStandardBetaUnitTest
|
||||
|
||||
# NIGHTLY
|
||||
- name: Build nightly build and run tests
|
||||
if: startsWith(env.VERSION_TAG, 'r')
|
||||
run: ./gradlew assembleStandardNightly testReleaseUnitTest testStandardNightlyUnitTest
|
||||
- name: Run unit tests
|
||||
if: startsWith(env.BUILD_TYPE, 'Standard')
|
||||
run: ./gradlew testReleaseUnitTest test${{ env.BUILD_TYPE }}UnitTest
|
||||
|
||||
- name: Upload R8 APK to artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
@ -126,7 +131,7 @@ jobs:
|
|||
path: app/build/outputs/mapping/standard*
|
||||
|
||||
- name: Publish test report
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
if: success() || failure()
|
||||
with:
|
||||
include_passed: true
|
||||
|
@ -149,7 +154,7 @@ jobs:
|
|||
echo "STAGE=${stage}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Sign APK
|
||||
uses: null2264/actions/android-signer@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
|
||||
uses: null2264/actions/android-signer@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94
|
||||
if: env.VERSION_TAG != ''
|
||||
with:
|
||||
releaseDir: app/build/outputs/apk/standard/${{ steps.version_stage.outputs.STAGE }}
|
||||
|
|
51
.github/workflows/mirror.yml
vendored
Normal file
51
.github/workflows/mirror.yml
vendored
Normal file
|
@ -0,0 +1,51 @@
|
|||
# REF: https://github.com/JamesRobionyRogers/GitHub-to-GitBucket-Action/blob/47a44e9/.github/workflows/push-to-gitbucket.yml
|
||||
name: Mirror Repository
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Fetch all branches
|
||||
run: |
|
||||
git fetch --all
|
||||
git fetch --tags
|
||||
|
||||
- name: List branches
|
||||
run: |
|
||||
git branch -a
|
||||
|
||||
# Enables tracking of remote branches ensuring all branches are pushed to mirror repo
|
||||
- name: Track remote branches
|
||||
run: |
|
||||
for branch in $(git branch -r | grep -v '\->'); do
|
||||
local_branch=${branch#origin/}
|
||||
git branch --track "$local_branch" "$branch" || true
|
||||
done
|
||||
|
||||
- name: Push to mirror repo (GitLab)
|
||||
env:
|
||||
MIRROR_URL: "gitlab.com/null2264/yokai.git"
|
||||
MIRROR_USERNAME: ${{ secrets.MIRROR_USERNAME }}
|
||||
MIRROR_AUTH: ${{ secrets.MIRROR_AUTH }}
|
||||
run: |
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "actions@github.com"
|
||||
|
||||
git remote add mirror https://$MIRROR_USERNAME:$MIRROR_AUTH@$MIRROR_URL
|
||||
git push --mirror -v mirror || true
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,3 +10,4 @@
|
|||
*/*/build
|
||||
.kotlin/
|
||||
kls_database.db
|
||||
weblate.conf
|
||||
|
|
|
@ -1,29 +1,36 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"],
|
||||
"labels": ["dependencies"],
|
||||
"packageRules": [
|
||||
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
|
||||
extends: [
|
||||
'config:recommended',
|
||||
],
|
||||
labels: [
|
||||
'dependencies',
|
||||
],
|
||||
packageRules: [
|
||||
{
|
||||
"groupName": "Compose BOM (Alpha)",
|
||||
"matchPackageNames": [
|
||||
"dev.chrisbanes.compose:compose-bom"
|
||||
groupName: 'Compose BOM (Alpha)',
|
||||
matchPackageNames: [
|
||||
'dev.chrisbanes.compose:compose-bom',
|
||||
],
|
||||
"ignoreUnstable": false
|
||||
ignoreUnstable: false,
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"com.github.arkon.FlexibleAdapter:flexible-adapter-ui",
|
||||
"com.github.arkon.FlexibleAdapter:flexible-adapter",
|
||||
"com.github.tachiyomiorg:image-decoder",
|
||||
"com.github.null2264:subsampling-scale-image-view",
|
||||
"com.github.tachiyomiorg:unifile",
|
||||
"com.github.tachiyomiorg:conductor-support-preference",
|
||||
"com.github.CarlosEsco:ViewTooltip"
|
||||
matchPackageNames: [
|
||||
'com.github.arkon.FlexibleAdapter:flexible-adapter-ui',
|
||||
'com.github.arkon.FlexibleAdapter:flexible-adapter',
|
||||
'com.github.CarlosEsco:ViewTooltip',
|
||||
'com.github.null2264:subsampling-scale-image-view',
|
||||
'com.github.null2264:injekt-koin',
|
||||
'com.github.tachiyomiorg:image-decoder',
|
||||
'com.github.tachiyomiorg:unifile',
|
||||
'com.github.tachiyomiorg:conductor-support-preference',
|
||||
'com.github.chrisbanes:PhotoView',
|
||||
'com.github.PhilJay:MPAndroidChart',
|
||||
],
|
||||
"enabled": false
|
||||
}
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
"dependencyDashboardApproval": true,
|
||||
"semanticCommits": true,
|
||||
"commitMessageLowerCase": "never"
|
||||
dependencyDashboardApproval: true,
|
||||
semanticCommits: 'enabled',
|
||||
commitMessageLowerCase: 'never',
|
||||
}
|
||||
|
|
812
CHANGELOG.md
812
CHANGELOG.md
|
@ -1,23 +1,817 @@
|
|||
<!-- Formatting
|
||||
## Additions ?? New features
|
||||
# Changelog
|
||||
|
||||
## Changes ?? Behaviour changes
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## Fixes ?? Bugfixes
|
||||
The format is simplified version of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
|
||||
- `Additions` - New features
|
||||
- `Changes` - Behaviour/visual changes
|
||||
- `Fixes` - Bugfixes
|
||||
- `Other` - Technical changes/updates
|
||||
|
||||
## Translation ?? translation changes/updates
|
||||
## [Unreleased]
|
||||
|
||||
## Other ?? Technical stuff, what happened behind the scene
|
||||
-->
|
||||
## Fixes
|
||||
### Additions
|
||||
- Add random library sort
|
||||
- Add the ability to save search queries
|
||||
- Add toggle to enable/disable hide source on swipe (@Hiirbaf)
|
||||
- Add the ability to mark duplicate read chapters as read (@AntsyLich)
|
||||
|
||||
### Changes
|
||||
- Temporarily disable log file
|
||||
- Categories' header now show filtered count when you search the library when you have "Show number of items" enabled (@LeeSF03)
|
||||
- Chapter progress now saved everything the page is changed
|
||||
|
||||
### Fixes
|
||||
- Allow users to bypass onboarding's permission step if Shizuku is installed
|
||||
- Fix Recents page shows "No recent chapters" instead of a loading screen
|
||||
- Fix not fully loaded entries can't be selected on Library page
|
||||
- Fix certain Infinix devices being unable to use any "Open link in browser" actions, including tracker setup (@MajorTanya)
|
||||
- Fix source filter bottom sheet unable to be fully scrolled to the bottom
|
||||
- Prevent potential "Comparison method violates its general contract!" crash
|
||||
- Fix staggered grid cover being squashed for local source (@AwkwardPeak7)
|
||||
|
||||
### Translation
|
||||
- Update translations from Weblate
|
||||
|
||||
### Other
|
||||
- Refactor Library to utilize Flow even more
|
||||
- Refactor EmptyView to use Compose
|
||||
- Refactor Reader ChapterTransition to use Compose (@arkon)
|
||||
- [Experimental] Add modified version of LargeTopAppBar that mimic J2K's ExpandedAppBarLayout
|
||||
- Refactor About page to use Compose
|
||||
- Adjust Compose-based pages' transition to match J2K's Conductor transition
|
||||
- Resolve deprecation warnings
|
||||
- Kotlin's context-receiver, schedule for removal on Kotlin v2.1.x and planned to be replaced by context-parameters on Kotlin v2.2
|
||||
- Project.exec -> Providers.exec
|
||||
- Remove internal API usage to retrieve Kotlin version for kotlin-stdlib
|
||||
- Move :core module to :core:main
|
||||
- Move archive related code to :core:archive (@AntsyLich)
|
||||
- Refactor Library to store LibraryMap instead of flatten list of LibraryItem
|
||||
- LibraryItem abstraction to make it easier to manage
|
||||
- LibraryManga no longer extend MangaImpl
|
||||
- Update dependency gradle to v8.12
|
||||
- Update user agent (@Hiirbaf)
|
||||
- Update serialization to v1.8.1
|
||||
- Update dependency io.github.fornewid:material-motion-compose-core to v1.2.1
|
||||
- Update lifecycle to v2.9.0
|
||||
- Update dependency org.jsoup:jsoup to v1.20.1
|
||||
- Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.4.0
|
||||
- Update dependency io.mockk:mockk to v1.14.2
|
||||
- Update dependency io.coil-kt.coil3:coil-bom to v3.2.0
|
||||
- Update dependency com.squareup.okio:okio to v3.12.0
|
||||
- Update dependency com.google.firebase:firebase-bom to v33.14.0
|
||||
- Update dependency com.google.accompanist:accompanist-themeadapter-material3 to v0.36.0
|
||||
- Update dependency com.github.requery:sqlite-android to v3.49.0
|
||||
- Update dependency com.getkeepsafe.taptargetview:taptargetview to v1.15.0
|
||||
- Update dependency androidx.window:window to v1.4.0
|
||||
- Update dependency androidx.webkit:webkit to v1.13.0
|
||||
- Update dependency androidx.sqlite:sqlite-ktx to v2.5.1
|
||||
- Update dependency androidx.sqlite:sqlite to v2.5.1
|
||||
- Update dependency androidx.recyclerview:recyclerview to v1.4.0
|
||||
- Update dependency androidx.core:core-ktx to v1.16.0
|
||||
- Update dependency androidx.compose:compose-bom to v2025.05.01
|
||||
- Update aboutlibraries to v11.6.3
|
||||
- Update plugin kotlinter to v5.1.0
|
||||
- Update plugin gradle-versions to v0.52.0
|
||||
- Update okhttp monorepo to v5.0.0-alpha.16
|
||||
- Update moko to v0.24.5
|
||||
- Update kotlin monorepo to v2.1.21
|
||||
- Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.2
|
||||
- Update dependency me.zhanghai.android.libarchive:library to v1.1.5
|
||||
- Update dependency io.insert-koin:koin-bom to v4.0.4
|
||||
- Update dependency com.android.tools:desugar_jdk_libs to v2.1.5
|
||||
- Update dependency androidx.work:work-runtime-ktx to v2.10.1
|
||||
- Update dependency androidx.constraintlayout:constraintlayout to v2.2.1
|
||||
- Update plugin firebase-crashlytics to v3.0.3
|
||||
- Update null2264/actions digest to 363cb9c
|
||||
- Update dependency io.github.pdvrieze.xmlutil:core-android to v0.91.1
|
||||
|
||||
## [1.9.7.3]
|
||||
|
||||
### Fixes
|
||||
- More `Comparison method violates its general contract!` crash prevention
|
||||
|
||||
## [1.9.7.2]
|
||||
|
||||
### Fixes
|
||||
- Fix MyAnimeList timeout issue
|
||||
|
||||
## [1.9.7.1]
|
||||
|
||||
### Fixes
|
||||
- Prevent `Comparison method violates its general contract!` crashes
|
||||
|
||||
## [1.9.7]
|
||||
|
||||
### Changes
|
||||
- Adjust log file to only log important information by default
|
||||
|
||||
### Fixes
|
||||
- Fix sorting by latest chapter is not working properly
|
||||
- Prevent some NPE crashes
|
||||
- Fix some flickering issues when browsing sources
|
||||
- Fix download count is not updating
|
||||
|
||||
### Translation
|
||||
- Update Korean translation (@Meokjeng)
|
||||
|
||||
### Other
|
||||
- Update NDK to v27.2.12479018
|
||||
|
||||
## [1.9.6]
|
||||
|
||||
### Fixes
|
||||
- Fix some crashes
|
||||
|
||||
## [1.9.5]
|
||||
|
||||
### Changes
|
||||
- Entries from local source now behaves similar to entries from online sources
|
||||
|
||||
### Fixes
|
||||
- Fix new chapters not showing up in `Recents > Grouped`
|
||||
- Add potential workarounds for duplicate chapter bug
|
||||
- Fix favorite state is not being updated when browsing source
|
||||
|
||||
### Other
|
||||
- Update dependency androidx.compose:compose-bom to v2024.12.01
|
||||
- Update plugin kotlinter to v5
|
||||
- Update plugin gradle-versions to v0.51.0
|
||||
- Update kotlin monorepo to v2.1.0
|
||||
|
||||
## [1.9.4]
|
||||
|
||||
### Fixes
|
||||
- Fix chapter date fetch always null causing it to not appear on Updates tab
|
||||
|
||||
## [1.9.3]
|
||||
|
||||
### Fixes
|
||||
- Fix slow chapter load
|
||||
- Fix chapter bookmark state is not persistent
|
||||
|
||||
### Other
|
||||
- Refactor downloader
|
||||
- Replace RxJava usage with Kotlin coroutines
|
||||
- Replace DownloadQueue with Flow to hopefully fix ConcurrentModificationException entirely
|
||||
|
||||
## [1.9.2]
|
||||
|
||||
### Changes
|
||||
- Adjust chapter title-details contrast
|
||||
- Make app updater notification consistent with other notifications
|
||||
|
||||
### Fixes
|
||||
- Fix "Remove from read" not working properly
|
||||
|
||||
## [1.9.1]
|
||||
|
||||
### Fixes
|
||||
- Fix chapters cannot be opened from `Recents > Grouped` and `Recents > All`
|
||||
- Fix crashes caused by malformed XML
|
||||
- Fix potential memory leak
|
||||
|
||||
### Other
|
||||
- Update dependency io.github.kevinnzou:compose-webview to v0.33.6
|
||||
- Update dependency org.jsoup:jsoup to v1.18.3
|
||||
- Update voyager to v1.1.0-beta03
|
||||
- Update dependency androidx.annotation:annotation to v1.9.1
|
||||
- Update dependency androidx.constraintlayout:constraintlayout to v2.2.0
|
||||
- Update dependency androidx.glance:glance-appwidget to v1.1.1
|
||||
- Update dependency com.google.firebase:firebase-bom to v33.7.0
|
||||
- Update fast.adapter to v5.7.0
|
||||
- Downgrade dependency org.conscrypt:conscrypt-android to v2.5.2
|
||||
|
||||
## [1.9.0]
|
||||
|
||||
### Additions
|
||||
- Sync DoH provider list with upstream (added Mullvad, Control D, Njalla, and Shecan)
|
||||
- Add option to enable verbose logging
|
||||
- Add category hopper long-press action to open random series from **any** category
|
||||
- Add option to enable reader debug mode
|
||||
- Add option to adjust reader's hardware bitmap threshold (@AntsyLich)
|
||||
- Always use software bitmap on certain devices (@MajorTanya)
|
||||
- Add option to scan local entries from `/storage/(sdcard|emulated/0)/Android/data/<yokai>/files/local`
|
||||
|
||||
### Changes
|
||||
- Enable 'Split Tall Images' by default (@Smol-Ame)
|
||||
- Minor visual adjustments
|
||||
- Tell user to restart the app when User-Agent is changed (@NGB-Was-Taken)
|
||||
- Re-enable fetching licensed manga (@Animeboynz)
|
||||
- Bangumi search now shows the score and summary of a search result (@MajorTanya)
|
||||
- Logs are now written to a file for easier debugging
|
||||
- Bump default user agent (@AntsyLich)
|
||||
- Custom cover is now compressed to WebP to prevent OOM crashes
|
||||
|
||||
### Fixes
|
||||
- Fix only few DoH provider is actually being used (Cloudflare, Google, AdGuard, and Quad9)
|
||||
- Fix "Group by Ungrouped" showing duplicate entries
|
||||
- Fix reader sometimes won't load images
|
||||
- Handle some uncaught crashes
|
||||
- Fix crashes due to GestureDetector's firstEvent is sometimes null on some devices
|
||||
- Fix download failed due to invalid XML 1.0 character
|
||||
- Fix issues with shizuku in a multi-user setup (@Redjard)
|
||||
- Fix some regional/variant languages is not listed in app language option
|
||||
- Fix browser not opening in some cases in Honor devices (@MajorTanya)
|
||||
- Fix "ConcurrentModificationException" crashes
|
||||
- Fix Komga unread badge, again
|
||||
- Fix default category can't be updated manually
|
||||
- Fix crashes trying to load Library caused by cover being too large
|
||||
|
||||
### Other
|
||||
- Simplify network helper code
|
||||
- Fully migrated from StorIO to SQLDelight
|
||||
- Update dependency com.android.tools:desugar_jdk_libs to v2.1.3
|
||||
- Update moko to v0.24.4
|
||||
- Refactor trackers to use DTOs (@MajorTanya)
|
||||
- Fix AniList `ALSearchItem.status` nullibility (@Secozzi)
|
||||
- Replace Injekt with Koin
|
||||
- Remove unnecessary permission added by Firebase
|
||||
- Remove unnecessary features added by Firebase
|
||||
- Replace BOM dev.chrisbanes.compose:compose-bom with JetPack's BOM
|
||||
- Update dependency androidx.compose:compose-bom to v2024.11.00
|
||||
- Update dependency com.google.firebase:firebase-bom to v33.6.0
|
||||
- Update dependency com.squareup.okio:okio to v3.9.1
|
||||
- Update activity to v1.9.3
|
||||
- Update lifecycle to v2.8.7
|
||||
- Update dependency me.zhanghai.android.libarchive:library to v1.1.4
|
||||
- Update agp to v8.7.3
|
||||
- Update junit5 monorepo to v5.11.3
|
||||
- Update dependency androidx.test.ext:junit to v1.2.1
|
||||
- Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.3.8
|
||||
- Update dependency org.jsoup:jsoup to v1.18.1
|
||||
- Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.9.0
|
||||
- Update serialization to v1.7.3
|
||||
- Update dependency gradle to v8.11.1
|
||||
- Update dependency androidx.webkit:webkit to v1.12.0
|
||||
- Update dependency io.mockk:mockk to v1.13.13
|
||||
- Update shizuku to v13.1.5
|
||||
- Use reflection to fix shizuku breaking changes (@Jobobby04)
|
||||
- Bump compile sdk to 35
|
||||
- Handle Android SDK 35 API collision (@AntsyLich)
|
||||
- Update kotlin monorepo to v2.0.21
|
||||
- Update dependency androidx.work:work-runtime-ktx to v2.10.0
|
||||
- Update dependency androidx.core:core-ktx to v1.15.0
|
||||
- Update dependency io.coil-kt.coil3:coil-bom to v3.0.4
|
||||
- Update xml.serialization to v0.90.3
|
||||
- Update dependency co.touchlab:kermit to v2.0.5
|
||||
- Replace WebView to use Compose (@arkon)
|
||||
- Fixed Keyboard is covering web page inputs
|
||||
- Increased `tryToSetForeground` delay to fix potential crashes (@nonproto)
|
||||
- Update dependency org.conscrypt:conscrypt-android to v2.5.3
|
||||
- Port upstream's download cache system
|
||||
|
||||
## [1.8.5.13]
|
||||
|
||||
### Fixed
|
||||
- Fix version checker
|
||||
|
||||
## [1.8.5.12]
|
||||
|
||||
### Fixed
|
||||
- Fixed scanlator data sometimes disappear
|
||||
|
||||
## [1.8.5.11]
|
||||
|
||||
### Fixed
|
||||
- Fixed crashes caused by Bangumi invalid status
|
||||
|
||||
## [1.8.5.10]
|
||||
|
||||
### Fixes
|
||||
- Fixed scanlator filter not working properly
|
||||
|
||||
## [1.8.5.9]
|
||||
|
||||
### Changes
|
||||
- Revert create backup to use file picker
|
||||
|
||||
## [1.8.5.8]
|
||||
|
||||
### Other
|
||||
- Separate backup error log when destination is null or not a file
|
||||
- Replace com.github.inorichi.injekt with com.github.null2264.injekt
|
||||
|
||||
## [1.8.5.7]
|
||||
|
||||
### Fixes
|
||||
- Fixed more NPE crashes
|
||||
|
||||
## [1.8.5.6]
|
||||
|
||||
### Fixes
|
||||
- Fixed NPE crash on tablets
|
||||
|
||||
## [1.8.5.5]
|
||||
|
||||
### Fixes
|
||||
- Fixed crashes caused by certain extension implementation
|
||||
- Fixed "Theme buttons based on cover" doesn't work properly
|
||||
- Fixed library cover images looks blurry then become sharp after going to
|
||||
entry's detail screen
|
||||
|
||||
## Other
|
||||
### Other
|
||||
- More StorIO to SQLDelight migration effort
|
||||
- Update dependency dev.chrisbanes.compose:compose-bom to v2024.08.00-alpha02
|
||||
- Update kotlin monorepo to v2.0.20
|
||||
- Update aboutlibraries to v11.2.3
|
||||
- Remove dependency com.github.leandroBorgesFerreira:LoadingButtonAndroid
|
||||
|
||||
## [1.8.5.4]
|
||||
|
||||
### Fixes
|
||||
- Fixed custom cover set from reader didn't show up on manga details
|
||||
|
||||
## [1.8.5.3]
|
||||
|
||||
### Additions
|
||||
- Add toggle to enable/disable chapter swipe action(s)
|
||||
- Add toggle to enable/disable webtoon double tap to zoom
|
||||
|
||||
### Changes
|
||||
- Custom cover now shown globally
|
||||
|
||||
### Fixes
|
||||
- Fixed chapter number parsing (@Naputt1)
|
||||
- Reduced library flickering (still happened in some cases when the cached image size is too different from the original image size, but should be reduced quite a bit)
|
||||
- Fixed entry details header didn't update when being removed from library
|
||||
|
||||
### Other
|
||||
- Refactor chapter recognition (@stevenyomi)
|
||||
- (Re)added unit test for chapter recognition
|
||||
- More StorIO to SQLDelight migration effort
|
||||
- Target Android 15
|
||||
- Adjust manga cover cache key
|
||||
- Refactor manga cover fetcher (@ivaniskandar, @AntsyLich, @null2264)
|
||||
|
||||
## [1.8.5.2]
|
||||
|
||||
### Fixes
|
||||
- Fixed some preference not being saved properly
|
||||
|
||||
### Other
|
||||
- Update dependency co.touchlab:kermit to v2.0.4
|
||||
- Update lifecycle to v2.8.4
|
||||
|
||||
## [1.8.5.1]
|
||||
|
||||
### Fixes
|
||||
- Fixed library showing duplicate entry when using dynamic category
|
||||
|
||||
## [1.8.5]
|
||||
|
||||
### Additions
|
||||
- Add missing "Max automatic backups" option on experimental Data and Storage setting menu
|
||||
- Add information on when was the last time backup automatically created to experimental Data and Storage setting menu
|
||||
- Add monochrome icon
|
||||
|
||||
### Changes
|
||||
- Add more info to WorkerInfo page
|
||||
- Added "next scheduled run"
|
||||
- Added attempt count
|
||||
- `english` tag no longer cause reading mode to switch to LTR (@mangkoran)
|
||||
- `chinese` tag no longer cause reading mode to switch to LTR
|
||||
- `manhua` tag no longer cause reading mode to switch to LTR
|
||||
- Local source manga's cover now being invalidated on refresh
|
||||
- It is now possible to create a backup without any entries using experimental Data and Storage setting menu
|
||||
- Increased default maximum automatic backup files to 5
|
||||
- It is now possible to edit a local source entry without adding it to library
|
||||
- Long Strip and Continuous Vertical background color now respect user setting
|
||||
- Display Color Profile setting no longer limited to Android 8 or newer
|
||||
- Increased long strip cache size to 4 for Android 8 or newer (@FooIbar)
|
||||
- Use Coil pipeline to handle HEIF images
|
||||
|
||||
### Fixes
|
||||
- Fixed auto backup, auto extension update, and app update checker stop working
|
||||
if it crash/failed
|
||||
- Fixed crashes when trying to reload extension repo due to connection issue
|
||||
- Fixed tap controls not working properly after zoom (@arkon, @Paloys, @FooIbar)
|
||||
- Fixed (sorta, more like workaround) ANR issues when running background tasks, such as updating extensions (@ivaniskandar)
|
||||
- Fixed split (downloaded) tall images sometimes doesn't work
|
||||
- Fixed status bar stuck in dark mode when app is following system theme
|
||||
- Fixed splash screen state only getting updates if library is empty (Should slightly reduce splash screen duration)
|
||||
- Fixed kitsu tracker issue due to domain change
|
||||
- Fixed entry custom cover won't load if entry doesn't have cover from source
|
||||
- Fixed unread badge doesn't work properly for some sources (notably Komga)
|
||||
- Fixed MAL start date parsing (@MajorTanya)
|
||||
|
||||
### Translation
|
||||
- Update Japanese translation (@akir45)
|
||||
- Update Brazilian Portuguese translation (@AshbornXS)
|
||||
- Update Filipino translation (@infyProductions)
|
||||
|
||||
### Other
|
||||
- Re-added several social media links to Mihon
|
||||
- Some code refactors
|
||||
- Simplify some messy code
|
||||
- Rewrite version checker
|
||||
- Rewrite Migrator (@ghostbear)
|
||||
- Split the project into several modules
|
||||
- Migrated i18n to use Moko Resources
|
||||
- Removed unnecessary dependencies (@null2264, @nonproto)
|
||||
- Update firebase bom to v33.1.0
|
||||
- Replace com.google.android.gms:play-services-oss-licenses with com.mikepenz:aboutlibraries
|
||||
- Update dependency com.google.gms:google-services to v4.4.2
|
||||
- Add crashlytics integration for Kermit
|
||||
- Replace ProgressBar with ProgressIndicator from Material3 to improve UI consistency
|
||||
- More StorIO to SQLDelight migrations
|
||||
- Merge lastFetch and lastRead query into library_view VIEW
|
||||
- Migrated a few more chapter related queries
|
||||
- Migrated most of the manga related queries
|
||||
- Bump dependency com.github.tachiyomiorg:unifile revision to a9de196cc7
|
||||
- Update project to Kotlin 2.0 (v2.0.10)
|
||||
- Update compose bom to v2024.08.00-alpha01
|
||||
- Refactor archive support to use `libarchive` (@FooIbar)
|
||||
- Use version catalog for gradle plugins
|
||||
- Update dependency org.jsoup:jsoup to v1.7.1
|
||||
- Bump dependency com.github.tachiyomiorg:image-decoder revision to 41c059e540
|
||||
- Update dependency io.coil-kt.coil3 to v3.0.0-alpha10
|
||||
- Update Android Gradle Plugin to v8.5.2
|
||||
- Update gradle to v8.9
|
||||
- Start using Voyager for navigation
|
||||
- Update dependency androidx.work:work-runtime-ktx to v2.9.1
|
||||
- Update dependency androidx.annotation:annotation to v1.8.2
|
||||
|
||||
## [1.8.4.6]
|
||||
|
||||
### Fixes
|
||||
- Fixed scanlator filter not working properly if it contains " & "
|
||||
|
||||
### Other
|
||||
- Removed dependency com.dmitrymalkovich.android:material-design-dimens
|
||||
- Replace dependency br.com.simplepass:loading-button-android with
|
||||
com.github.leandroBorgesFerreira:LoadingButtonAndroid
|
||||
- Replace dependency com.github.florent37:viewtooltip with
|
||||
com.github.CarlosEsco:ViewTooltip
|
||||
|
||||
## [1.8.4.5]
|
||||
|
||||
### Fixes
|
||||
- Fixed incorrect library entry chapter count
|
||||
|
||||
## [1.8.4.4]
|
||||
|
||||
### Fixes
|
||||
- Fixed incompatibility issue with J2K backup file
|
||||
|
||||
## [1.8.4.3]
|
||||
|
||||
### Fixes
|
||||
- Fixed "Open source repo" icon's colour
|
||||
|
||||
## [1.8.4.2]
|
||||
|
||||
### Changes
|
||||
- Changed "Open source repo" icon to prevent confusion
|
||||
|
||||
## [1.8.4.1]
|
||||
|
||||
### Fixes
|
||||
- Fixed saving combined pages not doing anything
|
||||
|
||||
## [1.8.4]
|
||||
|
||||
### Additions
|
||||
- Added option to change long tap browse and recents nav behaviour
|
||||
- Added browse long tap behaviour to open global search (@AshbornXS)
|
||||
- Added recents long tap behaviour to open last read chapter (@AshbornXS)
|
||||
- Added option to backup sensitive settings (such as tracker login tokens)
|
||||
- Added beta version of "Data and storage" settings (can be accessed by long tapping "Data and storage")
|
||||
|
||||
### Changes
|
||||
- Remove download location redirection from `Settings > Downloads`
|
||||
- Moved cache related stuff from `Settings > Advanced` to `Settings > Data and storage`
|
||||
- Improve webview (@AshbornXS)
|
||||
- Show url as subtitle
|
||||
- Add option to clear cookies
|
||||
- Allow zoom
|
||||
- Handle urls on global search (@AshbornXS)
|
||||
- Improve download queue (@AshbornXS)
|
||||
- Download badge now show download queue count
|
||||
- Add option to move series to bottom
|
||||
- Only show "open repo url" button when repo url is not empty
|
||||
|
||||
### Fixes
|
||||
- Fix potential crashes for some custom Android rom
|
||||
- Allow MultipartBody.Builder for extensions
|
||||
- Refresh extension repo now actually refresh extension(s) trust status
|
||||
- Custom manga info now relink properly upon migration
|
||||
- Fixed extension repo list did not update when a repo is added via deep link
|
||||
- Fixed download unread trying to download filtered (by scanlator) chapters
|
||||
- Fixed extensions not retaining their repo url
|
||||
- Fixed more NullPointerException crashes
|
||||
- Fixed split layout caused non-split images to not load
|
||||
|
||||
### Other
|
||||
- Migrate some StorIO queries to SQLDelight, should improve stability
|
||||
- Migrate from Timber to Kermit
|
||||
- Update okhttp monorepo to v5.0.0-alpha.14
|
||||
- Refactor backup code
|
||||
- Migrate backup flags to not use bitwise
|
||||
- Split it to several smaller classes
|
||||
- Update androidx.compose.material3:material3 to v1.3.0-beta02
|
||||
|
||||
## [1.8.3.4]
|
||||
|
||||
### Fixes
|
||||
- Fixed crashes caused by invalid ComicInfo XML
|
||||
|
||||
If this caused your custom manga info to stop working, try resetting it by deleting `ComicInfoEdits.xml` file located in `Android/data/eu.kanade.tachiyomi.yokai`
|
||||
|
||||
- Fixed crashes caused by the app trying to round NaN value
|
||||
|
||||
## [1.8.3.3]
|
||||
|
||||
### Changes
|
||||
- Crash report can now actually be disabled
|
||||
|
||||
### Other
|
||||
- Loading GlobalExceptionHandler before Crashlytics
|
||||
|
||||
## [1.8.3.2]
|
||||
|
||||
### Other
|
||||
- Some more NullPointerException prevention that I missed
|
||||
|
||||
## [1.8.3.1]
|
||||
|
||||
### Other
|
||||
- A bunch of NullPointerException prevention
|
||||
|
||||
## [1.8.3]
|
||||
|
||||
### Additions
|
||||
- Extensions now can be trusted by repo
|
||||
|
||||
### Changes
|
||||
- Extensions now required to have `repo.json`
|
||||
|
||||
### Other
|
||||
- Migrate to SQLDelight
|
||||
- Custom manga info is now stored in the database
|
||||
|
||||
## [1.8.2]
|
||||
|
||||
### Additions
|
||||
- Downloaded chapters now include ComicInfo file
|
||||
- (LocalSource) entry chapters' info can be edited using ComicInfo
|
||||
|
||||
### Fixes
|
||||
- Fixed smart background colour by page failing causing the image to not load
|
||||
- Fixed downloaded chapter can't be opened if it's too large
|
||||
- Downloaded page won't auto append chapter ID even tho the option is enabled
|
||||
|
||||
### Other
|
||||
- Re-route nightly to use its own repo, should fix "What's new" page
|
||||
|
||||
## [1.8.1.2]
|
||||
|
||||
### Additions
|
||||
- Added a couple new tags to set entry as SFW (`sfw` and `non-erotic`)
|
||||
|
||||
### Fixes
|
||||
- Fixed smart background colour by page failing causing the image to not load
|
||||
|
||||
### Other
|
||||
- Re-route nightly to use its own repo, should fix "What's new" page
|
||||
|
||||
## [1.8.1.1]
|
||||
|
||||
### Fixes
|
||||
- Fixed crashes when user try to edit an entry
|
||||
|
||||
## [1.8.1]
|
||||
|
||||
### Additions
|
||||
- (Experimental) Option to append chapter ID to download filename to avoid conflict
|
||||
|
||||
### Changes
|
||||
- Changed notification icon to use Yōkai's logo instead
|
||||
- Yōkai is now ComicInfo compliant. [Click here to learn more](https://anansi-project.github.io/docs/comicinfo/intro)
|
||||
- Removed "Couldn't split downloaded image" notification to reduce confusion. It has nothing to do with unsuccessful split, it just think it shouldn't split the image
|
||||
|
||||
### Fixes
|
||||
- Fixed not being able to open different chapter when a chapter is already opened
|
||||
- Fixed not being able to read chapters from local source
|
||||
- Fixed local source can't detect archives
|
||||
|
||||
### Other
|
||||
- Wrap SplashState to singleton factory, might fix issue where splash screen shown multiple times
|
||||
- Use Okio instead of `java.io`, should improve reader stability (especially long strip)
|
||||
|
||||
## [1.8.0.2]
|
||||
|
||||
### Fixes
|
||||
- Fixed app crashes when backup directory is null
|
||||
- Fixed app asking for All Files access permission when it's no longer needed
|
||||
|
||||
## [1.8.0.1]
|
||||
|
||||
### Additions
|
||||
- Added CrashScreen
|
||||
|
||||
### Fixes
|
||||
- Fixed version checker for nightly against hotfix patch version
|
||||
- Fixed download cache causes the app to crash
|
||||
|
||||
## [1.8.0]
|
||||
|
||||
### Additions
|
||||
- Added cutout support for some pre-Android P devices
|
||||
- Added option to add custom colour profile
|
||||
- Added onboarding screen
|
||||
|
||||
### Changes
|
||||
- Permanently enable 32-bit colour mode
|
||||
- Unified Storage™ ([Click here](https://mihon.app/docs/faq/storage#migrating-from-tachiyomi-v0-14-x-or-earlier) to learn more about it)
|
||||
|
||||
### Fixes
|
||||
- Fixed cutout behaviour for Android P
|
||||
- Fixed some extensions doesn't detect "added to library" entries properly ([GH-40](https://github.com/null2264/yokai/issues/40))
|
||||
- Fixed nightly and debug variant doesn't include their respective prefix on their app name
|
||||
- Fixed nightly version checker
|
||||
|
||||
### Other
|
||||
- Update dependency com.github.tachiyomiorg:image-decoder to e08e9be535
|
||||
- Update dependency com.github.null2264:subsampling-scale-image-view to 338caedb5f
|
||||
- Added Unit Test for version checker
|
||||
- Use Coil pipeline instead of SSIV for image decode whenever possible, might improve webtoon performance
|
||||
- Migrated from Coil2 to Coil3
|
||||
- Update compose compiler to v1.5.14
|
||||
- Update dependency androidx.compose.animation:animation to v1.6.7
|
||||
- Update dependency androidx.compose.foundation:foundation to v1.6.7
|
||||
- Update dependency androidx.compose.material:material to v1.6.7
|
||||
- Update dependency androidx.compose.ui:ui to v1.6.7
|
||||
- Update dependency androidx.compose.ui:ui-tooling to v1.6.7
|
||||
- Update dependency androidx.compose.ui:ui-tooling-preview to v1.6.7
|
||||
- Update dependency androidx.compose.material:material-icons-extended to v1.6.7
|
||||
- Update dependency androidx.lifecycle:lifecycle-viewmodel-compose to v2.8.0
|
||||
- Update dependency androidx.activity:activity-ktx to v1.9.0
|
||||
- Update dependency androidx.activity:activity-compose to v1.9.0
|
||||
- Update dependency androidx.annotation:annotation to v1.8.0
|
||||
- Update dependency androidx.browser:browser to v1.8.0
|
||||
- Update dependency androidx.core:core-ktx to v1.13.1
|
||||
- Update dependency androidx.lifecycle:lifecycle-viewmodel-ktx to v2.8.0
|
||||
- Update dependency androidx.lifecycle:lifecycle-livedata-ktx to v2.8.0
|
||||
- Update dependency androidx.lifecycle:lifecycle-common to v2.8.0
|
||||
- Update dependency androidx.lifecycle:lifecycle-process to v2.8.0
|
||||
- Update dependency androidx.lifecycle:lifecycle-runtime-ktx to v2.8.0
|
||||
- Update dependency androidx.recyclerview:recyclerview to v1.3.2
|
||||
- Update dependency androidx.sqlite:sqlite to v2.4.0
|
||||
- Update dependency androidx.webkit:webkit to v1.11.0
|
||||
- Update dependency androidx.work:work-runtime-ktx to v2.9.0
|
||||
- Update dependency androidx.window:window to v1.2.0
|
||||
- Update dependency com.google.firebase:firebase-crashlytics-gradle to v3.0.1
|
||||
- Update dependency com.google.gms:google-services to v4.4.1
|
||||
- Update dependency com.google.android.material:material to v1.12.0
|
||||
- Update dependency com.squareup.okio:okio to v3.8.0
|
||||
- Update dependency com.google.firebase:firebase-bom to v33.0.0
|
||||
- Update dependency org.jetbrains.kotlin:kotlin-gradle-plugin to v1.9.24
|
||||
- Update dependency org.jetbrains.kotlin:kotlin-serialization to v1.9.24
|
||||
- Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.6.2
|
||||
- Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json-okio to v1.6.2
|
||||
- Update dependency org.jetbrains.kotlinx:kotlinx-serialization-protobuf to v1.6.2
|
||||
- Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-android to v1.8.0
|
||||
- Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-core to v1.8.0
|
||||
- Resolved some compile warnings
|
||||
- Update dependency com.github.tachiyomiorg:unifile to 7c257e1c64
|
||||
|
||||
## [1.7.14]
|
||||
|
||||
### Changes
|
||||
- Added splash to reader (in case it being opened from shortcut)
|
||||
- Increased long strip split height
|
||||
- Use normalized app name by default as folder name
|
||||
|
||||
### Fixes
|
||||
- Fixed cutout support being broken
|
||||
|
||||
### Other
|
||||
- Move AppState from DI to Application class to reduce race condition
|
||||
|
||||
## [1.7.13]
|
||||
|
||||
### Additions
|
||||
- Ported Tachi's cutout option
|
||||
- Added Doki theme (dark only)
|
||||
|
||||
### Changes
|
||||
- Repositioned cutout options in settings
|
||||
- Splash icon now uses coloured variant of the icon
|
||||
- Removed deep link for sources, this should be handled by extensions
|
||||
- Removed braces from nightly (and debug) app name
|
||||
|
||||
### Fixes
|
||||
- Fixed preference summary not updating after being changed once
|
||||
- Fixed legacy appbar is visible on compose when being launched from deeplink
|
||||
- Fixed some app icon not generated properly
|
||||
- Fixed splash icon doesn't fit properly on Android 12+
|
||||
|
||||
### Other
|
||||
- Migrate to using Android 12's SplashScreen API
|
||||
- Clean up unused variables from ExtensionInstaller
|
||||
|
||||
## [1.7.12]
|
||||
|
||||
### Additions
|
||||
- Scanlator filter is now being backed up (@jobobby04)
|
||||
|
||||
### Fixes
|
||||
- Fixed error handling for MAL tracking (@AntsyLich)
|
||||
- Fixed extension installer preference incompatibility with modern Tachi
|
||||
|
||||
### Other
|
||||
- Split PreferencesHelper even more
|
||||
- Simplify extension install issue fix (@AwkwardPeak7)
|
||||
- Update dependency com.github.tachiyomiorg:image-decoder to fbd6601290
|
||||
- Replace dependency com.github.jays2kings:subsampling-scale-image-view with com.github.null2264:subsampling-scale-image-view
|
||||
- Update dependency com.github.null2264:subsampling-scale-image-view to e3cffd59c5
|
||||
|
||||
## [1.7.11]
|
||||
|
||||
### Fixes
|
||||
- Fixed MAL tracker issue (@AntsyLich)
|
||||
- Fixed trusting extension caused it to appear twice
|
||||
|
||||
### Other
|
||||
- Change Shikimori client from Tachi's to Yōkai's
|
||||
- Move TrackPreferences to PreferenceModule
|
||||
|
||||
## [1.7.10]
|
||||
|
||||
### Addition
|
||||
- Content type filter to hide SFW/NSFW entries
|
||||
- Confirmation before revoking all trusted extension
|
||||
|
||||
### Changes
|
||||
- Revert Webcomic -> Webtoon
|
||||
|
||||
### Fixes
|
||||
- Fix app bar disappearing on (scrolled) migration page
|
||||
- Fix installed extensions stuck in "installable" state
|
||||
- Fix untrusted extensions not having an icon
|
||||
|
||||
### Other
|
||||
- Changed (most) trackers' client id and secret
|
||||
- Add or changed user-agent for trackers
|
||||
|
||||
## [1.7.9]
|
||||
|
||||
### Other
|
||||
- Sync project with J2K [v1.7.4](https://github.com/Jays2Kings/tachiyomiJ2K/releases/tag/v1.7.4)
|
||||
|
||||
## [1.7.8]
|
||||
|
||||
### Changes
|
||||
- Local source now try to find entries not only in `Yōkai/` but also in `Yokai/` and `TachiyomiJ2K/` for easier migration
|
||||
|
||||
### Other
|
||||
- Changed AniList and MAL clientId, you may need to logout and re-login
|
||||
|
||||
## [1.7.7]
|
||||
|
||||
### Changes
|
||||
- Hopper icon now changes depending on currently active group type (J2K)
|
||||
|
||||
### Fixes
|
||||
- Fixed bookmarked entries not being detected as bookmarked on certain extensions
|
||||
|
||||
## [1.7.6]
|
||||
|
||||
### Additions
|
||||
- Shortcut to Extension Repos from Browser -> Extensions page
|
||||
- Added confirmation before extension repo deletion
|
||||
|
||||
### Changes
|
||||
- Adjusted dialogs background colour to be more consistent with app theme
|
||||
|
||||
### Fixes
|
||||
- Fixed visual glitch where page sometime empty on launch
|
||||
- Fixed extension interceptors receiving compressed responses (T)
|
||||
|
||||
### Other
|
||||
- Newly added strings from v1.7.5 is now translatable
|
||||
|
||||
## [1.7.5]
|
||||
|
||||
### Additions
|
||||
- Ported custom extension repo from upstream
|
||||
|
||||
### Changes
|
||||
- Removed built-in extension repo
|
||||
- Removed links related to Tachiyomi
|
||||
- Ported upstream's trust extension logic
|
||||
- Rebrand to Yōkai
|
||||
|
||||
### Other
|
||||
- Start migrating to Compose
|
||||
|
||||
## [1.7.4]
|
||||
|
||||
### Changes
|
||||
- Rename project to Yōkai (Z)
|
||||
- Replace Tachiyomi's purged extensions with Keiyoushi extensions (Temporary solution until I ported custom extension repo feature) (Z)
|
||||
- Unread count now respect scanlator filter (J2K)
|
||||
|
||||
### Fixes
|
||||
- Fixed visual glitch on certain page (J2K)
|
||||
|
|
33
README.md
33
README.md
|
@ -12,10 +12,13 @@
|
|||
|
||||
A free and open source manga reader
|
||||
|
||||
[](https://github.com/null2264/yokai/actions/workflows/build_push.yml)
|
||||
[](/LICENSE)
|
||||
[](https://discord.gg/mihon)
|
||||
[](https://gitlab.com/null2264/yokai)
|
||||
[](https://gitlab.com/null2264/yokai)
|
||||
[](https://git.aap.my.id/null2264/yokai)
|
||||
|
||||
[](https://github.com/null2264/yokai/actions/workflows/build_push.yml)
|
||||
[](/LICENSE)
|
||||
[](https://hosted.weblate.org/engage/yokai/)
|
||||
|
||||
<img src="./.github/readme-images/screens.gif" alt="Yokai screenshots" />
|
||||
|
||||
|
@ -32,10 +35,22 @@ This fork was created for personal usage, the name Yōkai is chosen in theme of
|
|||
|
||||
Updates are sporadic, sometime fast, sometime slow.
|
||||
|
||||
As of the time of writing, this fork is currently focusing on migrating to a much more modern infrastructure, some features may be added, but most changes are happening behind the scene.
|
||||
|
||||
## Features
|
||||
|
||||
<div align="left">
|
||||
|
||||
<details open="">
|
||||
<summary><h3>From Yōkai</h3></summary>
|
||||
|
||||
* NSFW/SFW library filter (taken from [TachiyomiSY](https://github.com/jobobby04/TachiyomiSY)).
|
||||
* Fix backup incompatibility with upstream.
|
||||
* New theme.
|
||||
* Local Source chapters now reads ComicInfo.xml for chapter title, number, and scanlator.
|
||||
|
||||
</details>
|
||||
|
||||
<details open="">
|
||||
<summary><h3>From upstream (Tachiyomi/Mihon)</h3></summary>
|
||||
|
||||
|
@ -80,14 +95,6 @@ Updates are sporadic, sometime fast, sometime slow.
|
|||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>From Yōkai</h3></summary>
|
||||
|
||||
* New theme
|
||||
* NSFW/SFW filter
|
||||
|
||||
</details>
|
||||
|
||||
</div>
|
||||
|
||||
## Contributing
|
||||
|
@ -142,8 +149,8 @@ The developer(s) of this application does not have any affiliation with the cont
|
|||
### License
|
||||
|
||||
<pre>
|
||||
Copyright 2015 Javier Tomás
|
||||
Copyright 2024 null2264
|
||||
Copyright © 2015 Javier Tomás
|
||||
Copyright © 2024 null2264
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsPlugin
|
||||
import com.google.gms.googleservices.GoogleServicesPlugin
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
alias(androidx.plugins.application)
|
||||
alias(kotlinx.plugins.android)
|
||||
alias(kotlinx.plugins.compose.compiler)
|
||||
id("yokai.android.application")
|
||||
id("yokai.android.application.compose")
|
||||
alias(kotlinx.plugins.serialization)
|
||||
alias(kotlinx.plugins.parcelize)
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
|
@ -23,15 +21,12 @@ if (gradle.startParameter.taskRequests.toString().contains("standard", true)) {
|
|||
}
|
||||
|
||||
fun runCommand(command: String): String {
|
||||
val byteOut = ByteArrayOutputStream()
|
||||
project.exec {
|
||||
commandLine = command.split(" ")
|
||||
standardOutput = byteOut
|
||||
}
|
||||
return String(byteOut.toByteArray()).trim()
|
||||
val result = providers.exec { commandLine(command.split(" ")) }
|
||||
return result.standardOutput.asText.get().trim()
|
||||
}
|
||||
|
||||
val _versionName = "1.8.5.5"
|
||||
@Suppress("PropertyName")
|
||||
val _versionName = "1.10.0"
|
||||
val betaCount by lazy {
|
||||
val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*")
|
||||
|
||||
|
@ -54,7 +49,7 @@ val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
|||
android {
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
versionCode = 146
|
||||
versionCode = 158
|
||||
versionName = _versionName
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled = true
|
||||
|
@ -72,11 +67,6 @@ android {
|
|||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters += supportedAbis
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
this.arguments("-DHAVE_LIBJXL=FALSE")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
|
@ -122,7 +112,6 @@ android {
|
|||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
// If you're here because there's not BuildConfig, build the app first, it'll generate it for you
|
||||
buildConfig = true
|
||||
|
||||
|
@ -156,7 +145,8 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core)
|
||||
implementation(projects.core.archive)
|
||||
implementation(projects.core.main)
|
||||
implementation(projects.data)
|
||||
implementation(projects.domain)
|
||||
implementation(projects.i18n)
|
||||
|
@ -168,7 +158,7 @@ dependencies {
|
|||
implementation(compose.bundles.compose)
|
||||
debugImplementation(compose.ui.tooling)
|
||||
implementation(libs.compose.theme.adapter3)
|
||||
implementation(libs.accompanist.webview)
|
||||
implementation(compose.webview)
|
||||
|
||||
implementation(libs.flexbox)
|
||||
|
||||
|
@ -212,10 +202,6 @@ dependencies {
|
|||
// Database
|
||||
implementation(libs.sqlite.android)
|
||||
implementation(libs.bundles.sqlite)
|
||||
//noinspection UseTomlInstead
|
||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||
//noinspection UseTomlInstead
|
||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||
|
||||
// Model View Presenter
|
||||
implementation(libs.conductor)
|
||||
|
@ -255,8 +241,6 @@ dependencies {
|
|||
implementation(libs.shizuku.api)
|
||||
implementation(libs.shizuku.provider)
|
||||
|
||||
implementation(kotlin("stdlib", org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION))
|
||||
|
||||
implementation(platform(kotlinx.coroutines.bom))
|
||||
implementation(kotlinx.bundles.coroutines)
|
||||
|
||||
|
@ -273,23 +257,26 @@ dependencies {
|
|||
testRuntimeOnly(libs.bundles.test.runtime)
|
||||
androidTestImplementation(libs.bundles.test.android)
|
||||
testImplementation(kotlinx.coroutines.test)
|
||||
|
||||
// For detecting memory leaks
|
||||
// REF: https://square.github.io/leakcanary/
|
||||
debugImplementation(libs.leakcanary.android)
|
||||
implementation(libs.leakcanary.plumber)
|
||||
}
|
||||
|
||||
tasks {
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||
withType<KotlinCompile> {
|
||||
compilerOptions.freeCompilerArgs.addAll(
|
||||
"-Xcontext-receivers",
|
||||
// "-opt-in=kotlin.Experimental",
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
// "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
// "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
|
@ -297,19 +284,6 @@ tasks {
|
|||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
|
||||
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
|
||||
compilerOptions.freeCompilerArgs.addAll(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
||||
(project.layout.buildDirectory.asFile.orNull?.absolutePath ?: "/tmp/yokai") + "/compose_metrics",
|
||||
)
|
||||
compilerOptions.freeCompilerArgs.addAll(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
||||
(project.layout.buildDirectory.asFile.orNull?.absolutePath ?: "/tmp/yokai") + "/compose_metrics",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||
|
|
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
|
@ -17,6 +17,7 @@
|
|||
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||
-keep,allowoptimization class org.koin.** { public protected *; }
|
||||
-keep,allowoptimization class eu.davidea.flexibleadapter.** { public protected *; }
|
||||
-keep class io.requery.android.database.** { public protected *; }
|
||||
|
||||
|
|
|
@ -257,4 +257,7 @@
|
|||
|
||||
</application>
|
||||
|
||||
<uses-sdk tools:overrideLibrary="rikka.shizuku.api"
|
||||
tools:ignore="ManifestOrder" />
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -22,6 +22,7 @@ import androidx.lifecycle.LifecycleOwner
|
|||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.multidex.MultiDex
|
||||
import co.touchlab.kermit.LogWriter
|
||||
import co.touchlab.kermit.Logger
|
||||
import coil3.ImageLoader
|
||||
import coil3.PlatformContext
|
||||
|
@ -33,6 +34,7 @@ import coil3.request.crossfade
|
|||
import coil3.util.DebugLogger
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager
|
||||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
|
@ -44,30 +46,38 @@ import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
|||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
||||
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.ui.source.SourcePresenter
|
||||
import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import eu.kanade.tachiyomi.util.system.localeContext
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import eu.kanade.tachiyomi.util.system.setToDefault
|
||||
import java.security.Security
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.conscrypt.Conscrypt
|
||||
import org.koin.core.context.startKoin
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.core.CrashlyticsLogWriter
|
||||
import yokai.core.di.AppModule
|
||||
import yokai.core.di.DomainModule
|
||||
import yokai.core.di.PreferenceModule
|
||||
import yokai.core.RollingUniFileLogWriter
|
||||
import yokai.core.di.appModule
|
||||
import yokai.core.di.domainModule
|
||||
import yokai.core.di.initExpensiveComponents
|
||||
import yokai.core.di.preferenceModule
|
||||
import yokai.core.migration.Migrator
|
||||
import yokai.core.migration.migrations.migrations
|
||||
import yokai.domain.base.BasePreferences
|
||||
import yokai.domain.storage.StorageManager
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
||||
|
@ -75,6 +85,8 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
|
|||
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
val basePreferences: BasePreferences by injectLazy()
|
||||
val networkPreferences: NetworkPreferences by injectLazy()
|
||||
private val storageManager: StorageManager by injectLazy()
|
||||
|
||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||
|
||||
|
@ -82,8 +94,6 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
|
|||
override fun onCreate() {
|
||||
super<Application>.onCreate()
|
||||
|
||||
if (!BuildConfig.DEBUG) Logger.addLogWriter(CrashlyticsLogWriter())
|
||||
|
||||
// TLS 1.3 support for Android 10 and below
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||
|
@ -95,11 +105,21 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
|
|||
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
||||
}
|
||||
|
||||
Injekt.apply {
|
||||
importModule(PreferenceModule(this@App))
|
||||
importModule(AppModule(this@App))
|
||||
importModule(DomainModule())
|
||||
startKoin {
|
||||
modules(preferenceModule(this@App), appModule(this@App), domainModule())
|
||||
}
|
||||
initExpensiveComponents(this)
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
|
||||
val scope = ProcessLifecycleOwner.get().lifecycleScope
|
||||
|
||||
networkPreferences.verboseLogging().changes()
|
||||
.onEach { enabled ->
|
||||
// FlexibleAdapter.enableLogs(if (enabled) Level.VERBOSE else Level.SUPPRESS)
|
||||
Logger.setToDefault(buildLogWritersToAdd(storageManager.getLogsDirectory(), enabled))
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
basePreferences.crashReport().changes()
|
||||
.onEach {
|
||||
|
@ -109,18 +129,23 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
|
|||
// Probably already enabled/disabled
|
||||
}
|
||||
}
|
||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||
.launchIn(scope)
|
||||
|
||||
setupNotificationChannels()
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
|
||||
MangaCoverMetadata.load()
|
||||
preferences.nightMode().changes()
|
||||
.onEach { AppCompatDelegate.setDefaultNightMode(it) }
|
||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||
.launchIn(scope)
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycleScope.launchIO {
|
||||
basePreferences.hardwareBitmapThreshold().let { preference ->
|
||||
if (!preference.isSet()) preference.set(GLUtil.DEVICE_TEXTURE_LIMIT)
|
||||
}
|
||||
basePreferences.hardwareBitmapThreshold().changes()
|
||||
.onEach { ImageUtil.hardwareBitmapThreshold = it }
|
||||
.launchIn(scope)
|
||||
|
||||
scope.launchIO {
|
||||
with(TachiyomiWidgetManager()) { this@App.init() }
|
||||
}
|
||||
|
||||
|
@ -159,7 +184,7 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
|
|||
notificationManager.cancel(Notifications.ID_INCOGNITO_MODE)
|
||||
}
|
||||
}
|
||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||
.launchIn(scope)
|
||||
|
||||
initializeMigrator()
|
||||
}
|
||||
|
@ -253,7 +278,7 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
|
|||
crossfade(true)
|
||||
allowRgb565(this@App.getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||
allowHardware(true)
|
||||
if (BuildConfig.DEBUG) {
|
||||
if (networkPreferences.verboseLogging().get()) {
|
||||
logger(DebugLogger())
|
||||
}
|
||||
|
||||
|
@ -264,4 +289,18 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
|
|||
}
|
||||
}
|
||||
|
||||
fun buildLogWritersToAdd(logPath: UniFile?): List<LogWriter> {
|
||||
val networkPreferences: NetworkPreferences = Injekt.get()
|
||||
return buildLogWritersToAdd(logPath, networkPreferences.verboseLogging().get())
|
||||
}
|
||||
|
||||
fun buildLogWritersToAdd(
|
||||
logPath: UniFile?,
|
||||
isVerbose: Boolean,
|
||||
) = buildList {
|
||||
if (!BuildConfig.DEBUG) add(CrashlyticsLogWriter())
|
||||
|
||||
// if (logPath != null && !BuildConfig.DEBUG) add(RollingUniFileLogWriter(logPath = logPath, isVerbose = isVerbose))
|
||||
}
|
||||
|
||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||
|
|
|
@ -40,9 +40,13 @@ import eu.kanade.tachiyomi.util.system.launchIO
|
|||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToLong
|
||||
import kotlinx.coroutines.MainScope
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.manga.models.cover
|
||||
import yokai.domain.recents.interactor.GetRecents
|
||||
|
||||
class UpdatesGridGlanceWidget : GlanceAppWidget() {
|
||||
private val app: Application by injectLazy()
|
||||
|
@ -64,6 +68,33 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME: Don't depends on RecentsPresenter
|
||||
private suspend fun getUpdates(customAmount: Int = 0, getRecents: GetRecents = Injekt.get()): List<Pair<Manga, Long>> {
|
||||
return getRecents
|
||||
.awaitUpdates(
|
||||
limit = when {
|
||||
customAmount > 0 -> (customAmount * 1.5).roundToLong()
|
||||
else -> 25L
|
||||
}
|
||||
)
|
||||
.mapNotNull {
|
||||
when {
|
||||
it.chapter.read || it.chapter.id == null -> RecentsPresenter.getNextChapter(it.manga)
|
||||
it.history.id == null -> RecentsPresenter.getFirstUpdatedChapter(it.manga, it.chapter)
|
||||
else -> it.chapter
|
||||
} ?: return@mapNotNull null
|
||||
it
|
||||
}
|
||||
.asSequence()
|
||||
.distinctBy { it.manga.id }
|
||||
.sortedByDescending { it.history.last_read }
|
||||
// nChapterItems + nAdditionalItems + cReadingItems
|
||||
.take((RecentsPresenter.UPDATES_CHAPTER_LIMIT * 2) + RecentsPresenter.UPDATES_READING_LIMIT_LOWER)
|
||||
.filter { it.manga.id != null }
|
||||
.map { it.manga to it.history.last_read }
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun loadData(list: List<Pair<Manga, Long>>? = null) {
|
||||
coroutineScope.launchIO {
|
||||
// Don't show anything when lock is active
|
||||
|
@ -80,7 +111,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
|
|||
.flatMap { manager.getAppWidgetSizes(it) }
|
||||
.maxBy { it.height.value * it.width.value }
|
||||
.calculateRowAndColumnCount()
|
||||
val processList = list ?: RecentsPresenter.getRecentManga(customAmount = min(50, rowCount * columnCount))
|
||||
val processList = list ?: getUpdates(customAmount = min(50, rowCount * columnCount))
|
||||
|
||||
data = prepareList(processList, rowCount * columnCount)
|
||||
ids.forEach { update(app, it) }
|
||||
|
|
|
@ -17,15 +17,15 @@ import androidx.glance.text.TextAlign
|
|||
import androidx.glance.text.TextStyle
|
||||
import androidx.glance.unit.ColorProvider
|
||||
import eu.kanade.tachiyomi.R
|
||||
import yokai.i18n.MR
|
||||
import eu.kanade.tachiyomi.appwidget.ContainerModifier
|
||||
import eu.kanade.tachiyomi.appwidget.util.stringResource
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import yokai.i18n.MR
|
||||
import yokai.presentation.core.Constants
|
||||
|
||||
@Composable
|
||||
fun LockedWidget() {
|
||||
val context = LocalContext.current
|
||||
val intent = Intent(LocalContext.current, Class.forName(MainActivity.MAIN_ACTIVITY)).apply {
|
||||
val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
Box(
|
||||
|
|
|
@ -17,19 +17,18 @@ import androidx.glance.layout.Row
|
|||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.text.Text
|
||||
import eu.kanade.tachiyomi.R
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import eu.kanade.tachiyomi.appwidget.ContainerModifier
|
||||
import eu.kanade.tachiyomi.appwidget.util.calculateRowAndColumnCount
|
||||
import eu.kanade.tachiyomi.appwidget.util.stringResource
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
||||
import yokai.i18n.MR
|
||||
import yokai.presentation.core.Constants
|
||||
|
||||
@Composable
|
||||
fun UpdatesWidget(data: List<Pair<Long, Bitmap?>>?) {
|
||||
val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount()
|
||||
val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(MainActivity.SHORTCUT_RECENTS)
|
||||
val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(Constants.SHORTCUT_RECENTS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
Column(
|
||||
modifier = ContainerModifier.clickable(actionStartActivity(mainIntent)),
|
||||
|
|
|
@ -5,9 +5,17 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun <T> Preference<T>.collectAsState(): State<T> {
|
||||
val flow = remember(this) { changes() }
|
||||
return flow.collectAsState(initial = get())
|
||||
}
|
||||
|
||||
fun String.asDateFormat(): DateFormat = when (this) {
|
||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
else -> SimpleDateFormat(this, Locale.getDefault())
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ class BackupFileValidator(
|
|||
|
||||
val trackers = backup.backupManga
|
||||
.flatMap { it.tracking }
|
||||
.map { it.syncId }
|
||||
.map { it.syncId.toLong() }
|
||||
.distinct()
|
||||
val missingTrackers = trackers
|
||||
.mapNotNull { trackManager.getService(it) }
|
||||
|
|
|
@ -38,7 +38,7 @@ class BackupNotifier(private val context: Context) {
|
|||
context.notificationManager.notify(id, build())
|
||||
}
|
||||
|
||||
fun showBackupProgress() {
|
||||
fun showBackupProgress(): NotificationCompat.Builder {
|
||||
val builder = with(progressNotificationBuilder) {
|
||||
setContentTitle(context.getString(MR.strings.creating_backup))
|
||||
|
||||
|
@ -47,6 +47,8 @@ class BackupNotifier(private val context: Context) {
|
|||
}
|
||||
|
||||
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
fun showBackupError(error: String?) {
|
||||
|
|
|
@ -5,26 +5,29 @@ import android.net.Uri
|
|||
import co.touchlab.kermit.Logger
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.util.system.e
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import java.io.FileOutputStream
|
||||
import java.time.Instant
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import okio.sink
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.backup.BackupPreferences
|
||||
import yokai.domain.storage.StorageManager
|
||||
import yokai.domain.manga.interactor.GetManga
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import java.io.FileOutputStream
|
||||
import java.time.Instant
|
||||
|
||||
class BackupCreator(
|
||||
val context: Context,
|
||||
|
@ -32,22 +35,11 @@ class BackupCreator(
|
|||
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
|
||||
private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
|
||||
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
) {
|
||||
|
||||
val parser = ProtoBuf
|
||||
private val db: DatabaseHelper = Injekt.get()
|
||||
private val backupPreferences: BackupPreferences = Injekt.get()
|
||||
private val storageManager: StorageManager by injectLazy()
|
||||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
private suspend fun getDatabaseManga(includeReadManga: Boolean) = db.inTransactionReturn {
|
||||
db.getFavoriteMangas().executeAsBlocking() +
|
||||
if (includeReadManga) {
|
||||
db.getReadNotInLibraryMangas().executeAsBlocking()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create backup Json file from database
|
||||
|
@ -60,7 +52,7 @@ class BackupCreator(
|
|||
try {
|
||||
file = if (isAutoBackup) {
|
||||
// Get dir of file and create
|
||||
val dir = storageManager.getAutomaticBackupsDirectory()
|
||||
val dir = UniFile.fromUri(context, uri)
|
||||
|
||||
// Delete older backups
|
||||
val numberOfBackups = backupPreferences.numberOfBackups().get()
|
||||
|
@ -74,20 +66,20 @@ class BackupCreator(
|
|||
dir?.createFile(Backup.getBackupFilename())
|
||||
} else {
|
||||
UniFile.fromUri(context, uri)
|
||||
} ?: throw IllegalStateException("Unable to retrieve backup destination")
|
||||
|
||||
if (!file.isFile) {
|
||||
throw IllegalStateException("Invalid backup destination")
|
||||
}
|
||||
|
||||
if (file == null || !file.isFile) {
|
||||
throw IllegalStateException("Failed to get handle on file")
|
||||
}
|
||||
|
||||
val backupManga = mangaBackupCreator(getDatabaseManga(options.readManga), options)
|
||||
val readNotFavorites = if (options.readManga) getManga.awaitReadNotFavorites() else emptyList()
|
||||
val backupManga = backupMangas(getManga.awaitFavorites() + readNotFavorites, options)
|
||||
val backup = Backup(
|
||||
backupManga = backupManga,
|
||||
backupCategories = categoriesBackupCreator(options),
|
||||
backupBrokenSources = emptyList(),
|
||||
backupSources = sourcesBackupCreator(backupManga),
|
||||
backupPreferences = preferenceBackupCreator.backupAppPreferences(options),
|
||||
backupSourcePreferences = preferenceBackupCreator.backupSourcePreferences(options),
|
||||
backupCategories = backupCategories(options),
|
||||
backupSources = backupSources(backupManga),
|
||||
backupPreferences = backupAppPreferences(options),
|
||||
backupSourcePreferences = backupSourcePreferences(options),
|
||||
)
|
||||
|
||||
val byteArray = parser.encodeToByteArray(Backup.serializer(), backup)
|
||||
|
@ -110,9 +102,37 @@ class BackupCreator(
|
|||
|
||||
return fileUri.toString()
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e)
|
||||
Logger.e(e) { "Backup failed: ${e.message}" }
|
||||
file?.delete()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun backupCategories(options: BackupOptions): List<BackupCategory> {
|
||||
if (!options.categories) return emptyList()
|
||||
|
||||
return categoriesBackupCreator()
|
||||
}
|
||||
|
||||
private suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
||||
if (!options.libraryEntries) return emptyList()
|
||||
|
||||
return mangaBackupCreator(mangas, options)
|
||||
}
|
||||
|
||||
private fun backupSources(mangas: List<BackupManga>): List<BackupSource> {
|
||||
return sourcesBackupCreator(mangas)
|
||||
}
|
||||
|
||||
private fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
||||
if (!options.appPrefs) return emptyList()
|
||||
|
||||
return preferenceBackupCreator.createApp(includePrivatePreferences = options.includePrivate)
|
||||
}
|
||||
|
||||
private fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||
if (!options.sourcePrefs) return emptyList()
|
||||
|
||||
return preferenceBackupCreator.createSource(includePrivatePreferences = options.includePrivate)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +1,62 @@
|
|||
package eu.kanade.tachiyomi.data.backup.create
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.e
|
||||
import eu.kanade.tachiyomi.util.system.localeContext
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import eu.kanade.tachiyomi.util.system.tryToSetForeground
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.backup.BackupPreferences
|
||||
import yokai.domain.storage.StorageManager
|
||||
import java.util.concurrent.*
|
||||
|
||||
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
|
||||
private val notifier = BackupNotifier(context.localeContext)
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val storageManager: StorageManager by injectLazy()
|
||||
val notifier = BackupNotifier(context.localeContext)
|
||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() ?: storageManager.getAutomaticBackupsDirectory()?.uri
|
||||
val options = inputData.getBooleanArray(BACKUP_FLAGS_KEY)?.let { BackupOptions.fromBooleanArray(it) }
|
||||
?: BackupOptions()
|
||||
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
|
||||
|
||||
notifier.showBackupProgress()
|
||||
if (isAutoBackup && BackupRestoreJob.isRunning(context)) return Result.retry()
|
||||
|
||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
||||
?: getAutomaticBackupLocation()
|
||||
?: return Result.failure()
|
||||
|
||||
tryToSetForeground()
|
||||
|
||||
val options = inputData.getBooleanArray(BACKUP_FLAGS_KEY)?.let { BackupOptions.fromBooleanArray(it) }
|
||||
?: BackupOptions()
|
||||
|
||||
return try {
|
||||
val location = BackupCreator(context).createBackup(uri!!, options, isAutoBackup)
|
||||
val location = BackupCreator(context).createBackup(uri, options, isAutoBackup)
|
||||
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!)
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e)
|
||||
Logger.e(e) { "Unable to create backup" }
|
||||
if (!isAutoBackup) notifier.showBackupError(e.message)
|
||||
Result.failure()
|
||||
} finally {
|
||||
|
@ -51,30 +64,53 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||
}
|
||||
}
|
||||
|
||||
private fun getAutomaticBackupLocation(): Uri? {
|
||||
val storageManager = Injekt.get<StorageManager>()
|
||||
return storageManager.getAutomaticBackupsDirectory()?.uri
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_BACKUP_PROGRESS,
|
||||
notifier.showBackupProgress().build(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isManualJobRunning(context: Context): Boolean {
|
||||
val list = WorkManager.getInstance(context).getWorkInfosByTag(TAG_MANUAL).get()
|
||||
return list.find { it.state == WorkInfo.State.RUNNING } != null
|
||||
return context.workManager
|
||||
.getWorkInfosByTag(TAG_MANUAL).get()
|
||||
.find { it.state == WorkInfo.State.RUNNING } != null
|
||||
}
|
||||
|
||||
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||
val preferences = Injekt.get<BackupPreferences>()
|
||||
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
if (interval > 0) {
|
||||
val constraints = Constraints(
|
||||
requiresBatteryNotLow = true,
|
||||
)
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||
interval.toLong(),
|
||||
TimeUnit.HOURS,
|
||||
10,
|
||||
TimeUnit.MINUTES,
|
||||
)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES)
|
||||
.addTag(TAG_AUTO)
|
||||
.setConstraints(constraints)
|
||||
.setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true))
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||
context.workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||
} else {
|
||||
workManager.cancelUniqueWork(TAG_AUTO)
|
||||
context.workManager.cancelUniqueWork(TAG_AUTO)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,7 +124,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||
.addTag(TAG_MANUAL)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||
context.workManager.enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,23 +2,20 @@ package eu.kanade.tachiyomi.data.backup.create.creators
|
|||
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.domain.category.interactor.GetCategories
|
||||
|
||||
class CategoriesBackupCreator(
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
) {
|
||||
/**
|
||||
* Backup the categories of library
|
||||
*
|
||||
* @return list of [BackupCategory] to be backed up
|
||||
*/
|
||||
operator fun invoke(options: BackupOptions): List<BackupCategory> {
|
||||
if (!options.categories) return emptyList()
|
||||
|
||||
return db.getCategories()
|
||||
.executeAsBlocking()
|
||||
suspend operator fun invoke(): List<BackupCategory> {
|
||||
return getCategories.await()
|
||||
.map { BackupCategory.copyFrom(it) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,21 +5,25 @@ import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
|||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupTracking
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.data.DatabaseHandler
|
||||
import yokai.domain.category.interactor.GetCategories
|
||||
import yokai.domain.chapter.interactor.GetChapter
|
||||
import yokai.domain.history.interactor.GetHistory
|
||||
import yokai.domain.track.interactor.GetTrack
|
||||
|
||||
class MangaBackupCreator(
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val customMangaManager: CustomMangaManager = Injekt.get(),
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val getChapter: GetChapter = Injekt.get(),
|
||||
private val getHistory: GetHistory = Injekt.get(),
|
||||
private val getTrack: GetTrack = Injekt.get(),
|
||||
) {
|
||||
suspend operator fun invoke(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
||||
if (!options.libraryEntries) return emptyList()
|
||||
|
||||
return mangas.map {
|
||||
backupManga(it, options)
|
||||
}
|
||||
|
@ -39,16 +43,25 @@ class MangaBackupCreator(
|
|||
// Check if user wants chapter information in backup
|
||||
if (options.chapters) {
|
||||
// Backup all the chapters
|
||||
val chapters = manga.id?.let { getChapter.awaitAll(it, false) }.orEmpty()
|
||||
val chapters = manga.id?.let {
|
||||
handler.awaitList {
|
||||
chaptersQueries.getChaptersByMangaId(
|
||||
it,
|
||||
0, // We want all chapters, so ignore scanlator filter
|
||||
BackupChapter::mapper)
|
||||
}
|
||||
}.orEmpty()
|
||||
if (chapters.isNotEmpty()) {
|
||||
mangaObject.chapters = chapters.map { BackupChapter.copyFrom(it) }
|
||||
mangaObject.chapters = chapters
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants category information in backup
|
||||
if (options.categories) {
|
||||
// Backup categories for this manga
|
||||
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||
val categoriesForManga = manga.id?.let {
|
||||
getCategories.awaitByMangaId(it)
|
||||
}.orEmpty()
|
||||
if (categoriesForManga.isNotEmpty()) {
|
||||
mangaObject.categories = categoriesForManga.mapNotNull { it.order }
|
||||
}
|
||||
|
@ -56,7 +69,9 @@ class MangaBackupCreator(
|
|||
|
||||
// Check if user wants track information in backup
|
||||
if (options.tracking) {
|
||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||
val tracks = manga.id?.let {
|
||||
getTrack.awaitAllByMangaId(it)
|
||||
}.orEmpty()
|
||||
if (tracks.isNotEmpty()) {
|
||||
mangaObject.tracking = tracks.map { BackupTracking.copyFrom(it) }
|
||||
}
|
||||
|
@ -64,7 +79,9 @@ class MangaBackupCreator(
|
|||
|
||||
// Check if user wants history information in backup
|
||||
if (options.history) {
|
||||
val historyForManga = db.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||
val historyForManga = manga.id?.let {
|
||||
getHistory.awaitAllByMangaId(it)
|
||||
}.orEmpty()
|
||||
if (historyForManga.isNotEmpty()) {
|
||||
val history = historyForManga.mapNotNull { history ->
|
||||
val url = getChapter.awaitById(history.chapter_id)?.url
|
||||
|
|
|
@ -23,21 +23,19 @@ class PreferenceBackupCreator(
|
|||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||
) {
|
||||
fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
||||
if (!options.appPrefs) return emptyList()
|
||||
fun createApp(includePrivatePreferences: Boolean): List<BackupPreference> {
|
||||
return preferenceStore.getAll().toBackupPreferences()
|
||||
.withPrivatePreferences(options.includePrivate)
|
||||
.withPrivatePreferences(includePrivatePreferences)
|
||||
}
|
||||
|
||||
fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||
if (!options.sourcePrefs) return emptyList()
|
||||
fun createSource(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
|
||||
return sourceManager.getOnlineSources()
|
||||
.filterIsInstance<ConfigurableSource>()
|
||||
.map {
|
||||
BackupSourcePreferences(
|
||||
it.preferenceKey(),
|
||||
it.sourcePreferences().all.toBackupPreferences()
|
||||
.withPrivatePreferences(options.includePrivate),
|
||||
.withPrivatePreferences(includePrivatePreferences),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
package eu.kanade.tachiyomi.data.backup.models
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
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(),
|
||||
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
||||
//@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
||||
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package eu.kanade.tachiyomi.data.backup.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
|
||||
|
@ -42,20 +41,32 @@ data class BackupChapter(
|
|||
}
|
||||
|
||||
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,
|
||||
pagesLeft = chapter.pages_left,
|
||||
)
|
||||
}
|
||||
fun mapper(
|
||||
id: Long,
|
||||
mangaId: Long,
|
||||
url: String,
|
||||
name: String,
|
||||
scanlator: String?,
|
||||
read: Boolean,
|
||||
bookmark: Boolean,
|
||||
lastPageRead: Long,
|
||||
pagesLeft: Long,
|
||||
chapterNumber: Double,
|
||||
sourceOrder: Long,
|
||||
dateFetch: Long,
|
||||
dateUpload: Long,
|
||||
) = BackupChapter(
|
||||
url = url,
|
||||
name = name,
|
||||
scanlator = scanlator,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
lastPageRead = lastPageRead.toInt(),
|
||||
pagesLeft = pagesLeft.toInt(),
|
||||
chapterNumber = chapterNumber.toFloat(),
|
||||
sourceOrder = sourceOrder.toInt(),
|
||||
dateFetch = dateFetch,
|
||||
dateUpload = dateUpload,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,8 +57,10 @@ data class BackupManga(
|
|||
@ProtoNumber(805) var customGenre: List<String>? = null,
|
||||
) {
|
||||
fun getMangaImpl(): MangaImpl {
|
||||
return MangaImpl().apply {
|
||||
url = this@BackupManga.url
|
||||
return MangaImpl(
|
||||
source = this.source,
|
||||
url = this.url,
|
||||
).apply {
|
||||
title = this@BackupManga.title
|
||||
artist = this@BackupManga.artist
|
||||
author = this@BackupManga.author
|
||||
|
@ -67,7 +69,6 @@ data class BackupManga(
|
|||
status = this@BackupManga.status
|
||||
thumbnail_url = this@BackupManga.thumbnailUrl
|
||||
favorite = this@BackupManga.favorite
|
||||
source = this@BackupManga.source
|
||||
date_added = this@BackupManga.dateAdded
|
||||
viewer_flags = (
|
||||
this@BackupManga.viewer_flags
|
||||
|
|
|
@ -32,7 +32,7 @@ data class BackupTracking(
|
|||
|
||||
fun getTrackingImpl(): TrackImpl {
|
||||
return TrackImpl().apply {
|
||||
sync_id = this@BackupTracking.syncId
|
||||
sync_id = this@BackupTracking.syncId.toLong()
|
||||
media_id = if (this@BackupTracking.mediaIdInt != 0) {
|
||||
this@BackupTracking.mediaIdInt.toLong()
|
||||
} else {
|
||||
|
@ -41,7 +41,7 @@ data class BackupTracking(
|
|||
library_id = this@BackupTracking.libraryId
|
||||
title = this@BackupTracking.title
|
||||
last_chapter_read = this@BackupTracking.lastChapterRead
|
||||
total_chapters = this@BackupTracking.totalChapters
|
||||
total_chapters = this@BackupTracking.totalChapters.toLong()
|
||||
score = this@BackupTracking.score
|
||||
status = this@BackupTracking.status
|
||||
started_reading_date = this@BackupTracking.startedReadingDate
|
||||
|
@ -53,13 +53,13 @@ data class BackupTracking(
|
|||
companion object {
|
||||
fun copyFrom(track: Track): BackupTracking {
|
||||
return BackupTracking(
|
||||
syncId = track.sync_id,
|
||||
syncId = track.sync_id.toInt(),
|
||||
mediaId = track.media_id,
|
||||
// forced not null so its compatible with 1.x backup system
|
||||
libraryId = track.library_id!!,
|
||||
libraryId = track.library_id ?: 0L,
|
||||
title = track.title,
|
||||
lastChapterRead = track.last_chapter_read,
|
||||
totalChapters = track.total_chapters,
|
||||
totalChapters = track.total_chapters.toInt(),
|
||||
score = track.score,
|
||||
status = track.status,
|
||||
startedReadingDate = track.started_reading_date,
|
||||
|
|
|
@ -9,7 +9,6 @@ import androidx.work.ExistingWorkPolicy
|
|||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.OutOfQuotaPolicy
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import co.touchlab.kermit.Logger
|
||||
|
@ -20,6 +19,7 @@ import eu.kanade.tachiyomi.util.system.jobIsRunning
|
|||
import eu.kanade.tachiyomi.util.system.localeContext
|
||||
import eu.kanade.tachiyomi.util.system.tryToSetForeground
|
||||
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
@ -30,13 +30,15 @@ class BackupRestoreJob(val context: Context, workerParams: WorkerParameters) : C
|
|||
private val restorer = BackupRestorer(context, notifier)
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
val notification = notifier.showRestoreProgress(progress = -1).build()
|
||||
val id = Notifications.ID_RESTORE_PROGRESS
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ForegroundInfo(id, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else {
|
||||
ForegroundInfo(id, notification)
|
||||
}
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_RESTORE_PROGRESS,
|
||||
notifier.showRestoreProgress(progress = -1).build(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
|
@ -72,14 +74,13 @@ class BackupRestoreJob(val context: Context, workerParams: WorkerParameters) : C
|
|||
.setInputData(workDataOf(BackupConst.EXTRA_URI to uri.toString()))
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.build()
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
WorkManager.getInstance(context).cancelUniqueWork(TAG)
|
||||
context.workManager.cancelUniqueWork(TAG)
|
||||
}
|
||||
|
||||
fun isRunning(context: Context) = WorkManager.getInstance(context).jobIsRunning(TAG)
|
||||
fun isRunning(context: Context) = context.workManager.jobIsRunning(TAG)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.backup.restore
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesBackupRestorer
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaBackupRestorer
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceBackupRestorer
|
||||
|
@ -54,9 +53,7 @@ class BackupRestorer(
|
|||
|
||||
restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs
|
||||
|
||||
// Store source mapping for error messages
|
||||
val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
||||
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
||||
sourceMapping = backup.backupSources.associate { it.sourceId to it.name }
|
||||
|
||||
coroutineScope {
|
||||
// Restore categories
|
||||
|
|
|
@ -1,25 +1,23 @@
|
|||
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
||||
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.data.DatabaseHandler
|
||||
import yokai.domain.category.interactor.GetCategories
|
||||
|
||||
class CategoriesBackupRestorer(
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
) {
|
||||
suspend fun restoreCategories(backupCategories: List<BackupCategory>, onComplete: () -> Unit) {
|
||||
// Get categories from file and from db
|
||||
// Do it outside of transaction because StorIO might hang because we're using SQLDelight
|
||||
val dbCategories = getCategories.await()
|
||||
db.inTransaction {
|
||||
handler.await(true) {
|
||||
// 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) {
|
||||
for (dbCategory in getCategories.await()) {
|
||||
// If the category is already in the db, assign the id to the file's category
|
||||
// and do nothing
|
||||
if (category.name == dbCategory.name) {
|
||||
|
@ -33,8 +31,13 @@ class CategoriesBackupRestorer(
|
|||
if (!found) {
|
||||
// Let the db assign the id
|
||||
category.id = null
|
||||
val result = db.insertCategory(category).executeAsBlocking()
|
||||
category.id = result.insertedId()?.toInt()
|
||||
categoriesQueries.insert(
|
||||
name = category.name,
|
||||
mangaOrder = category.mangaOrderToString(),
|
||||
sort = category.order.toLong(),
|
||||
flags = category.flags.toLong(),
|
||||
)
|
||||
category.id = categoriesQueries.selectLastInsertedRowId().executeAsOneOrNull()?.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.backup.restore.restorers
|
|||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
|
@ -18,25 +17,36 @@ import eu.kanade.tachiyomi.util.system.launchNow
|
|||
import kotlin.math.max
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.data.DatabaseHandler
|
||||
import yokai.domain.category.interactor.GetCategories
|
||||
import yokai.domain.category.interactor.SetMangaCategories
|
||||
import yokai.domain.chapter.interactor.GetChapter
|
||||
import yokai.domain.chapter.interactor.InsertChapter
|
||||
import yokai.domain.chapter.interactor.UpdateChapter
|
||||
import yokai.domain.history.interactor.GetHistory
|
||||
import yokai.domain.history.interactor.UpsertHistory
|
||||
import yokai.domain.library.custom.model.CustomMangaInfo
|
||||
import yokai.domain.manga.interactor.GetManga
|
||||
import yokai.domain.manga.interactor.InsertManga
|
||||
import yokai.domain.manga.interactor.UpdateManga
|
||||
import yokai.domain.track.interactor.GetTrack
|
||||
import yokai.domain.track.interactor.InsertTrack
|
||||
|
||||
class MangaBackupRestorer(
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val customMangaManager: CustomMangaManager = Injekt.get(),
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||
private val getChapter: GetChapter = Injekt.get(),
|
||||
private val insertChapter: InsertChapter = Injekt.get(),
|
||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
private val insertManga: InsertManga = Injekt.get(),
|
||||
private val updateManga: UpdateManga = Injekt.get(),
|
||||
private val getHistory: GetHistory = Injekt.get(),
|
||||
private val upsertHistory: UpsertHistory = Injekt.get(),
|
||||
private val getTrack: GetTrack = Injekt.get(),
|
||||
private val insertTrack: InsertTrack = Injekt.get(),
|
||||
) {
|
||||
suspend fun restoreManga(
|
||||
backupManga: BackupManga,
|
||||
|
@ -188,8 +198,7 @@ class MangaBackupRestorer(
|
|||
|
||||
// Update database
|
||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||
db.deleteOldMangasCategories(listOf(manga)).executeAsBlocking()
|
||||
db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||
setMangaCategories.awaitAll(listOf(manga.id!!), mangaCategoriesToUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -202,7 +211,7 @@ class MangaBackupRestorer(
|
|||
// List containing history to be updated
|
||||
val historyToBeUpdated = ArrayList<History>(history.size)
|
||||
for ((url, lastRead, readDuration) in history) {
|
||||
val dbHistory = db.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||
val dbHistory = handler.awaitOneOrNull { historyQueries.getByChapterUrl(url, History::mapper) }
|
||||
// Check if history already in database and update
|
||||
if (dbHistory != null) {
|
||||
dbHistory.apply {
|
||||
|
@ -221,7 +230,7 @@ class MangaBackupRestorer(
|
|||
}
|
||||
}
|
||||
}
|
||||
db.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||
upsertHistory.awaitBulk(historyToBeUpdated)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -235,7 +244,7 @@ class MangaBackupRestorer(
|
|||
tracks.map { it.manga_id = manga.id!! }
|
||||
|
||||
// Get tracks from database
|
||||
val dbTracks = db.getTracks(manga).executeAsBlocking()
|
||||
val dbTracks = getTrack.awaitAllByMangaId(manga.id!!)
|
||||
val trackToUpdate = mutableListOf<Track>()
|
||||
|
||||
tracks.forEach { track ->
|
||||
|
@ -263,7 +272,7 @@ class MangaBackupRestorer(
|
|||
}
|
||||
// Update database
|
||||
if (trackToUpdate.isNotEmpty()) {
|
||||
db.insertTracks(trackToUpdate).executeAsBlocking()
|
||||
insertTrack.awaitBulk(trackToUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
package eu.kanade.tachiyomi.data.cache
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import android.text.format.Formatter
|
||||
import co.touchlab.kermit.Logger
|
||||
import coil3.imageLoader
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.updateCoverLastModified
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.system.e
|
||||
import eu.kanade.tachiyomi.util.system.executeOnIO
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||
import eu.kanade.tachiyomi.util.system.withUIContext
|
||||
|
@ -19,8 +19,8 @@ import java.io.InputStream
|
|||
import java.util.concurrent.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.manga.interactor.GetManga
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
|
||||
|
@ -40,6 +40,8 @@ class CoverCache(val context: Context) {
|
|||
private const val ONLINE_COVERS_DIR = "online_covers"
|
||||
}
|
||||
|
||||
private val getManga: GetManga by injectLazy()
|
||||
|
||||
/** Cache directory used for cache management.*/
|
||||
private val cacheDir = getCacheDir(COVERS_DIR)
|
||||
|
||||
|
@ -68,9 +70,8 @@ class CoverCache(val context: Context) {
|
|||
}
|
||||
|
||||
suspend fun deleteOldCovers() {
|
||||
val db = Injekt.get<DatabaseHelper>()
|
||||
var deletedSize = 0L
|
||||
val urls = db.getFavoriteMangas().executeOnIO().mapNotNull {
|
||||
val urls = getManga.awaitFavorites().mapNotNull {
|
||||
it.thumbnail_url?.let { url ->
|
||||
it.updateCoverLastModified()
|
||||
return@mapNotNull DiskUtil.hashKeyForDisk(url)
|
||||
|
@ -145,7 +146,7 @@ class CoverCache(val context: Context) {
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e)
|
||||
Logger.e(e) { "Unable to delete unused cover cache" }
|
||||
}
|
||||
lastClean = System.currentTimeMillis()
|
||||
}
|
||||
|
@ -173,8 +174,38 @@ class CoverCache(val context: Context) {
|
|||
*/
|
||||
@Throws(IOException::class)
|
||||
fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) {
|
||||
val maxTextureSize = 4096f
|
||||
var bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
if (maxOf(bitmap.width, bitmap.height) > maxTextureSize) {
|
||||
val widthRatio = bitmap.width / maxTextureSize
|
||||
val heightRatio = bitmap.height / maxTextureSize
|
||||
|
||||
val targetWidth: Float
|
||||
val targetHeight: Float
|
||||
|
||||
if (widthRatio >= heightRatio) {
|
||||
targetWidth = maxTextureSize
|
||||
targetHeight = (targetWidth / bitmap.width) * bitmap.height
|
||||
} else {
|
||||
targetHeight = maxTextureSize
|
||||
targetWidth = (targetHeight / bitmap.height) * bitmap.width
|
||||
}
|
||||
|
||||
val scaledBitmap = Bitmap.createScaledBitmap(bitmap, targetWidth.toInt(), targetHeight.toInt(), true)
|
||||
bitmap.recycle()
|
||||
bitmap = scaledBitmap
|
||||
}
|
||||
getCustomCoverFile(manga).outputStream().use {
|
||||
inputStream.copyTo(it)
|
||||
@Suppress("DEPRECATION")
|
||||
bitmap.compress(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
|
||||
Bitmap.CompressFormat.WEBP_LOSSLESS
|
||||
else
|
||||
Bitmap.CompressFormat.WEBP,
|
||||
100,
|
||||
it
|
||||
)
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
|||
if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
options.bitmapConfig == Bitmap.Config.HARDWARE &&
|
||||
maxOf(bitmap.width, bitmap.height) <= GLUtil.maxTextureSize
|
||||
!ImageUtil.isHardwareThresholdExceeded(bitmap)
|
||||
) {
|
||||
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
|
||||
if (hwBitmap != null) {
|
||||
|
@ -59,29 +59,6 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
val maxTextureSize = 4096f
|
||||
if (maxOf(bitmap.width, bitmap.height) > maxTextureSize) {
|
||||
val widthRatio = bitmap.width / maxTextureSize
|
||||
val heightRatio = bitmap.height / maxTextureSize
|
||||
|
||||
val targetWidth: Float
|
||||
val targetHeight: Float
|
||||
|
||||
if (widthRatio >= heightRatio) {
|
||||
targetWidth = maxTextureSize
|
||||
targetHeight = (targetWidth / bitmap.width) * bitmap.height
|
||||
} else {
|
||||
targetHeight = maxTextureSize
|
||||
targetWidth = (targetHeight / bitmap.height) * bitmap.width
|
||||
}
|
||||
|
||||
val scaledBitmap = Bitmap.createScaledBitmap(bitmap, targetWidth.toInt(), targetHeight.toInt(), true)
|
||||
bitmap.recycle()
|
||||
bitmap = scaledBitmap
|
||||
}
|
||||
*/
|
||||
|
||||
return DecodeResult(
|
||||
image = bitmap.asImage(),
|
||||
isSampled = sampleSize > 1,
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||
import eu.kanade.tachiyomi.data.database.mappers.CategoryTypeMapping
|
||||
import eu.kanade.tachiyomi.data.database.mappers.ChapterTypeMapping
|
||||
import eu.kanade.tachiyomi.data.database.mappers.HistoryTypeMapping
|
||||
import eu.kanade.tachiyomi.data.database.mappers.MangaCategoryTypeMapping
|
||||
import eu.kanade.tachiyomi.data.database.mappers.MangaTypeMapping
|
||||
import eu.kanade.tachiyomi.data.database.mappers.SearchMetadataTypeMapping
|
||||
import eu.kanade.tachiyomi.data.database.mappers.TrackTypeMapping
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.models.SearchMetadata
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.queries.CategoryQueries
|
||||
import eu.kanade.tachiyomi.data.database.queries.ChapterQueries
|
||||
import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
|
||||
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
||||
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||
import eu.kanade.tachiyomi.data.database.queries.SearchMetadataQueries
|
||||
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
||||
/**
|
||||
* This class provides operations to manage the database through its interfaces.
|
||||
*/
|
||||
open class DatabaseHelper(
|
||||
context: Context,
|
||||
openHelper: SupportSQLiteOpenHelper,
|
||||
) :
|
||||
MangaQueries,
|
||||
ChapterQueries,
|
||||
TrackQueries,
|
||||
CategoryQueries,
|
||||
MangaCategoryQueries,
|
||||
HistoryQueries,
|
||||
SearchMetadataQueries {
|
||||
|
||||
override val db = DefaultStorIOSQLite.builder()
|
||||
.sqliteOpenHelper(openHelper)
|
||||
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
||||
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
||||
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
|
||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||
.build()
|
||||
|
||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||
|
||||
inline fun <T> inTransactionReturn(block: () -> T): T = db.inTransactionReturn(block)
|
||||
|
||||
fun lowLevel() = db.lowLevel()
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
|
||||
inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
|
||||
lowLevel().beginTransaction()
|
||||
try {
|
||||
block()
|
||||
lowLevel().setTransactionSuccessful()
|
||||
} finally {
|
||||
lowLevel().endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
|
||||
lowLevel().beginTransaction()
|
||||
try {
|
||||
val result = block()
|
||||
lowLevel().setTransactionSuccessful()
|
||||
return result
|
||||
} finally {
|
||||
lowLevel().endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
fun Cursor.getBoolean(index: Int) = getLong(index) > 0
|
|
@ -1,45 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
||||
import co.touchlab.kermit.Logger
|
||||
import yokai.data.Database
|
||||
|
||||
class DbOpenCallback : AndroidSqliteDriver.Callback(Database.Schema) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Name of the database file.
|
||||
*/
|
||||
const val DATABASE_NAME = "tachiyomi.db"
|
||||
}
|
||||
|
||||
override fun onOpen(db: SupportSQLiteDatabase) {
|
||||
super.onOpen(db)
|
||||
setPragma(db, "foreign_keys = ON")
|
||||
setPragma(db, "journal_mode = WAL")
|
||||
setPragma(db, "synchronous = NORMAL")
|
||||
}
|
||||
|
||||
private fun setPragma(db: SupportSQLiteDatabase, pragma: String) {
|
||||
val cursor = db.query("PRAGMA $pragma")
|
||||
cursor.moveToFirst()
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
Logger.d { "Creating new database..." }
|
||||
super.onCreate(db)
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
if (oldVersion < newVersion) {
|
||||
Logger.d { "Upgrading database from $oldVersion to $newVersion" }
|
||||
super.onUpgrade(db, oldVersion, newVersion)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||
db.setForeignKeyConstraintsEnabled(true)
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||
|
||||
interface DbProvider {
|
||||
|
||||
val db: DefaultStorIOSQLite
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_FLAGS
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_MANGA_ORDER
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_NAME
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
|
||||
|
||||
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
|
||||
CategoryPutResolver(),
|
||||
CategoryGetResolver(),
|
||||
CategoryDeleteResolver(),
|
||||
)
|
||||
|
||||
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_NAME, obj.name)
|
||||
put(COL_ORDER, obj.order)
|
||||
put(COL_FLAGS, obj.flags)
|
||||
if (obj.mangaSort != null) {
|
||||
put(COL_MANGA_ORDER, obj.mangaSort.toString())
|
||||
} else {
|
||||
val orderString = obj.mangaOrder.joinToString("/")
|
||||
put(COL_MANGA_ORDER, orderString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().also {
|
||||
it.id = cursor.getInt(cursor.getColumnIndex(COL_ID))
|
||||
it.name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||
it.order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
|
||||
it.flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
||||
|
||||
val orderString = cursor.getString(cursor.getColumnIndex(COL_MANGA_ORDER))
|
||||
val (sort, order) = Category.mangaOrderFromString(orderString)
|
||||
if (sort != null) it.mangaSort = sort
|
||||
it.mangaOrder = order
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_BOOKMARK
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_CHAPTER_NUMBER
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_FETCH
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_UPLOAD
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_PAGES_LEFT
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
||||
|
||||
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
|
||||
ChapterPutResolver(),
|
||||
ChapterGetResolver(),
|
||||
ChapterDeleteResolver(),
|
||||
)
|
||||
|
||||
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_MANGA_ID, obj.manga_id)
|
||||
put(COL_URL, obj.url)
|
||||
put(COL_NAME, obj.name)
|
||||
put(COL_READ, obj.read)
|
||||
put(COL_SCANLATOR, obj.scanlator)
|
||||
put(COL_BOOKMARK, obj.bookmark)
|
||||
put(COL_DATE_FETCH, obj.date_fetch)
|
||||
put(COL_DATE_UPLOAD, obj.date_upload)
|
||||
put(COL_LAST_PAGE_READ, obj.last_page_read)
|
||||
put(COL_PAGES_LEFT, obj.pages_left)
|
||||
put(COL_CHAPTER_NUMBER, obj.chapter_number)
|
||||
put(COL_SOURCE_ORDER, obj.source_order)
|
||||
}
|
||||
}
|
||||
|
||||
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
|
||||
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
||||
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
||||
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
||||
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
|
||||
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
|
||||
pages_left = cursor.getInt(cursor.getColumnIndex(COL_PAGES_LEFT))
|
||||
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
|
||||
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
|
||||
}
|
||||
}
|
||||
|
||||
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_CHAPTER_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_LAST_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
|
||||
|
||||
class HistoryTypeMapping : SQLiteTypeMapping<History>(
|
||||
HistoryPutResolver(),
|
||||
HistoryGetResolver(),
|
||||
HistoryDeleteResolver(),
|
||||
)
|
||||
|
||||
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_CHAPTER_ID, obj.chapter_id)
|
||||
put(COL_LAST_READ, obj.last_read)
|
||||
put(COL_TIME_READ, obj.time_read)
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): History = History.mapper(
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
|
||||
chapterId = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID)),
|
||||
lastRead = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ)),
|
||||
timeRead = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ)),
|
||||
)
|
||||
}
|
||||
|
||||
class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_CATEGORY_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
|
||||
|
||||
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
|
||||
MangaCategoryPutResolver(),
|
||||
MangaCategoryGetResolver(),
|
||||
MangaCategoryDeleteResolver(),
|
||||
)
|
||||
|
||||
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_MANGA_ID, obj.manga_id)
|
||||
put(COL_CATEGORY_ID, obj.category_id)
|
||||
}
|
||||
}
|
||||
|
||||
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
|
||||
}
|
||||
}
|
||||
|
||||
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.getBoolean
|
||||
import eu.kanade.tachiyomi.data.database.models.mapper
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFIED
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FILTERED_SCANLATORS
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_HIDE_TITLE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_TITLE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_UPDATE_STRATEGY
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_URL
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import yokai.data.updateStrategyAdapter
|
||||
|
||||
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
|
||||
MangaPutResolver(),
|
||||
MangaGetResolver(),
|
||||
MangaDeleteResolver(),
|
||||
)
|
||||
|
||||
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_SOURCE, obj.source)
|
||||
put(COL_URL, obj.url)
|
||||
put(COL_ARTIST, obj.originalArtist)
|
||||
put(COL_AUTHOR, obj.originalAuthor)
|
||||
put(COL_DESCRIPTION, obj.originalDescription)
|
||||
put(COL_GENRE, obj.originalGenre)
|
||||
put(COL_TITLE, obj.ogTitle)
|
||||
put(COL_STATUS, obj.ogStatus)
|
||||
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
|
||||
put(COL_FAVORITE, obj.favorite)
|
||||
put(COL_LAST_UPDATE, obj.last_update)
|
||||
put(COL_INITIALIZED, obj.initialized)
|
||||
put(COL_VIEWER, obj.viewer_flags)
|
||||
put(COL_HIDE_TITLE, obj.hide_title)
|
||||
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
||||
put(COL_DATE_ADDED, obj.date_added)
|
||||
put(COL_FILTERED_SCANLATORS, obj.filtered_scanlators)
|
||||
put(COL_UPDATE_STRATEGY, obj.update_strategy.let(updateStrategyAdapter::encode))
|
||||
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
|
||||
}
|
||||
}
|
||||
|
||||
interface BaseMangaGetResolver {
|
||||
@SuppressLint("Range")
|
||||
fun mapBaseFromCursor(cursor: Cursor) = Manga.mapper(
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
|
||||
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE)),
|
||||
url = cursor.getString(cursor.getColumnIndex(COL_URL)),
|
||||
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST)),
|
||||
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR)),
|
||||
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION)),
|
||||
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE)),
|
||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE)),
|
||||
status = cursor.getLong(cursor.getColumnIndex(COL_STATUS)),
|
||||
thumbnailUrl = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL)),
|
||||
favorite = cursor.getBoolean(cursor.getColumnIndex(COL_FAVORITE)),
|
||||
lastUpdate = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE)),
|
||||
initialized = cursor.getBoolean(cursor.getColumnIndex(COL_INITIALIZED)),
|
||||
viewerFlags = cursor.getLong(cursor.getColumnIndex(COL_VIEWER)),
|
||||
chapterFlags = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_FLAGS)),
|
||||
hideTitle = cursor.getBoolean(cursor.getColumnIndex(COL_HIDE_TITLE)),
|
||||
dateAdded = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED)),
|
||||
filteredScanlators = cursor.getString(cursor.getColumnIndex(COL_FILTERED_SCANLATORS)),
|
||||
updateStrategy = cursor.getLong(cursor.getColumnIndex(COL_UPDATE_STRATEGY)),
|
||||
coverLastModified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED)),
|
||||
)
|
||||
}
|
||||
|
||||
open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Manga {
|
||||
return mapBaseFromCursor(cursor)
|
||||
}
|
||||
}
|
||||
|
||||
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.SearchMetadata
|
||||
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.COL_EXTRA
|
||||
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.COL_EXTRA_VERSION
|
||||
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.COL_INDEXED_EXTRA
|
||||
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.COL_MANGA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.COL_UPLOADER
|
||||
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.TABLE
|
||||
|
||||
class SearchMetadataTypeMapping : SQLiteTypeMapping<SearchMetadata>(
|
||||
SearchMetadataPutResolver(),
|
||||
SearchMetadataGetResolver(),
|
||||
SearchMetadataDeleteResolver(),
|
||||
)
|
||||
|
||||
class SearchMetadataPutResolver : DefaultPutResolver<SearchMetadata>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: SearchMetadata) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: SearchMetadata) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_MANGA_ID = ?")
|
||||
.whereArgs(obj.mangaId)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: SearchMetadata) = ContentValues(5).apply {
|
||||
put(COL_MANGA_ID, obj.mangaId)
|
||||
put(COL_UPLOADER, obj.uploader)
|
||||
put(COL_EXTRA, obj.extra)
|
||||
put(COL_INDEXED_EXTRA, obj.indexedExtra)
|
||||
put(COL_EXTRA_VERSION, obj.extraVersion)
|
||||
}
|
||||
}
|
||||
|
||||
class SearchMetadataGetResolver : DefaultGetResolver<SearchMetadata>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): SearchMetadata = SearchMetadata(
|
||||
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
|
||||
uploader = cursor.getString(cursor.getColumnIndex(COL_UPLOADER)),
|
||||
extra = cursor.getString(cursor.getColumnIndex(COL_EXTRA)),
|
||||
indexedExtra = cursor.getString(cursor.getColumnIndex(COL_INDEXED_EXTRA)),
|
||||
extraVersion = cursor.getInt(cursor.getColumnIndex(COL_EXTRA_VERSION)),
|
||||
)
|
||||
}
|
||||
|
||||
class SearchMetadataDeleteResolver : DefaultDeleteResolver<SearchMetadata>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: SearchMetadata) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_MANGA_ID = ?")
|
||||
.whereArgs(obj.mangaId)
|
||||
.build()
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_FINISH_DATE
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LIBRARY_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MEDIA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_START_DATE
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TOTAL_CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TRACKING_URL
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
||||
|
||||
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
||||
TrackPutResolver(),
|
||||
TrackGetResolver(),
|
||||
TrackDeleteResolver(),
|
||||
)
|
||||
|
||||
class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Track) = contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_MANGA_ID to obj.manga_id,
|
||||
COL_SYNC_ID to obj.sync_id,
|
||||
COL_MEDIA_ID to obj.media_id,
|
||||
COL_LIBRARY_ID to obj.library_id,
|
||||
COL_TITLE to obj.title,
|
||||
COL_LAST_CHAPTER_READ to obj.last_chapter_read,
|
||||
COL_TOTAL_CHAPTERS to obj.total_chapters,
|
||||
COL_STATUS to obj.status,
|
||||
COL_TRACKING_URL to obj.tracking_url,
|
||||
COL_SCORE to obj.score,
|
||||
COL_START_DATE to obj.started_reading_date,
|
||||
COL_FINISH_DATE to obj.finished_reading_date,
|
||||
)
|
||||
}
|
||||
|
||||
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
||||
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
||||
sync_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SYNC_ID))
|
||||
media_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MEDIA_ID))
|
||||
library_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LIBRARY_ID))
|
||||
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
|
||||
last_chapter_read = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_LAST_CHAPTER_READ))
|
||||
total_chapters = cursor.getInt(cursor.getColumnIndexOrThrow(COL_TOTAL_CHAPTERS))
|
||||
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
|
||||
score = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_SCORE))
|
||||
tracking_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_TRACKING_URL))
|
||||
started_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_START_DATE))
|
||||
finished_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_FINISH_DATE))
|
||||
}
|
||||
}
|
||||
|
||||
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
|
@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.data.database.models
|
|||
import android.content.Context
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||
import java.io.Serializable
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import java.io.Serializable
|
||||
|
||||
interface Category : Serializable {
|
||||
|
||||
|
@ -37,8 +37,7 @@ interface Category : Serializable {
|
|||
return ((mangaSort?.minus('a') ?: 0) % 2) != 1
|
||||
}
|
||||
|
||||
fun sortingMode(nullAsDND: Boolean = false): LibrarySort? = LibrarySort.valueOf(mangaSort)
|
||||
?: if (nullAsDND && !isDynamic) LibrarySort.DragAndDrop else null
|
||||
fun sortingMode(): LibrarySort? = LibrarySort.valueOf(mangaSort)
|
||||
|
||||
val isDragAndDrop
|
||||
get() = (
|
||||
|
@ -53,7 +52,24 @@ interface Category : Serializable {
|
|||
mangaSort = (LibrarySort.valueOf(sort) ?: LibrarySort.Title).categoryValue
|
||||
}
|
||||
|
||||
fun mangaOrderToString(): String =
|
||||
if (mangaSort != null) mangaSort.toString() else mangaOrder.joinToString("/")
|
||||
|
||||
// For dynamic categories
|
||||
fun dynamicHeaderKey(): String {
|
||||
if (!isDynamic) throw IllegalStateException("This category is not a dynamic category")
|
||||
|
||||
return when {
|
||||
sourceId != null -> "${name}$sourceSplitter${sourceId}"
|
||||
langId != null -> "${langId}$langSplitter${name}"
|
||||
else -> name
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val sourceSplitter = "◘•◘"
|
||||
const val langSplitter = "⨼⨦⨠"
|
||||
|
||||
var lastCategoriesAddedTo = emptySet<Int>()
|
||||
|
||||
fun create(name: String): Category = CategoryImpl().apply {
|
||||
|
|
|
@ -32,10 +32,14 @@ class CategoryImpl : Category {
|
|||
|
||||
val category = other as Category
|
||||
|
||||
if (isDynamic && category.isDynamic) return dynamicHeaderKey() == category.dynamicHeaderKey()
|
||||
|
||||
return name == category.name
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
if (isDynamic) return dynamicHeaderKey().hashCode()
|
||||
|
||||
return name.hashCode()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,4 +90,8 @@ interface Chapter : SChapter, Serializable {
|
|||
source_order = other.source_order
|
||||
copyFrom(other as SChapter)
|
||||
}
|
||||
|
||||
fun copy() = ChapterImpl().apply {
|
||||
copyFrom(this@Chapter)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,11 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||
|
||||
fun SChapter.toChapter(): ChapterImpl {
|
||||
return ChapterImpl().apply {
|
||||
name = this@SChapter.name
|
||||
url = this@SChapter.url
|
||||
date_upload = this@SChapter.date_upload
|
||||
chapter_number = this@SChapter.chapter_number
|
||||
scanlator = this@SChapter.scanlator
|
||||
name = this@toChapter.name
|
||||
url = this@toChapter.url
|
||||
date_upload = this@toChapter.date_upload
|
||||
chapter_number = this@toChapter.chapter_number
|
||||
scanlator = this@toChapter.scanlator
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,16 +39,18 @@ interface History : Serializable {
|
|||
this.chapter_id = chapter.id!!
|
||||
}
|
||||
|
||||
fun create(): History = HistoryImpl()
|
||||
|
||||
fun mapper(
|
||||
id: Long,
|
||||
chapterId: Long,
|
||||
lastRead: Long,
|
||||
timeRead: Long
|
||||
) = HistoryImpl().apply {
|
||||
lastRead: Long?,
|
||||
timeRead: Long?,
|
||||
): History = HistoryImpl().apply {
|
||||
this.id = id
|
||||
this.chapter_id = chapterId
|
||||
this.last_read = lastRead
|
||||
this.time_read = timeRead
|
||||
lastRead?.let { this.last_read = it }
|
||||
timeRead?.let { this.time_read = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import kotlin.math.roundToInt
|
||||
import yokai.data.updateStrategyAdapter
|
||||
|
||||
data class LibraryManga(
|
||||
val manga: Manga,
|
||||
var unread: Int = 0,
|
||||
var read: Int = 0,
|
||||
var category: Int = 0,
|
||||
|
@ -13,41 +13,11 @@ data class LibraryManga(
|
|||
var latestUpdate: Long = 0,
|
||||
var lastRead: Long = 0,
|
||||
var lastFetch: Long = 0,
|
||||
) : MangaImpl() {
|
||||
|
||||
var realMangaCount = 0
|
||||
get() = if (isBlank()) field else throw IllegalStateException("realMangaCount is only accessible by placeholders")
|
||||
set(value) {
|
||||
if (!isBlank()) throw IllegalStateException("realMangaCount can only be set by placeholders")
|
||||
field = value
|
||||
}
|
||||
|
||||
) {
|
||||
val hasRead
|
||||
get() = read > 0
|
||||
|
||||
@Transient
|
||||
var items: List<LibraryItem>? = null
|
||||
get() = if (isHidden()) field else throw IllegalStateException("items only accessible by placeholders")
|
||||
set(value) {
|
||||
if (!isHidden()) throw IllegalStateException("items can only be set by placeholders")
|
||||
field = value
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createBlank(categoryId: Int): LibraryManga = LibraryManga().apply {
|
||||
title = ""
|
||||
id = Long.MIN_VALUE
|
||||
category = categoryId
|
||||
}
|
||||
|
||||
fun createHide(categoryId: Int, title: String, hiddenItems: List<LibraryItem>): LibraryManga =
|
||||
createBlank(categoryId).apply {
|
||||
this.title = title
|
||||
this.status = -1
|
||||
this.read = hiddenItems.size
|
||||
this.items = hiddenItems
|
||||
}
|
||||
|
||||
fun mapper(
|
||||
// manga
|
||||
id: Long,
|
||||
|
@ -78,34 +48,37 @@ data class LibraryManga(
|
|||
latestUpdate: Long,
|
||||
lastRead: Long,
|
||||
lastFetch: Long,
|
||||
): LibraryManga = createBlank(categoryId.toInt()).apply {
|
||||
this.id = id
|
||||
this.source = source
|
||||
this.url = url
|
||||
this.artist = artist
|
||||
this.author = author
|
||||
this.description = description
|
||||
this.genre = genre
|
||||
this.title = title
|
||||
this.status = status.toInt()
|
||||
this.thumbnail_url = thumbnailUrl
|
||||
this.favorite = favorite
|
||||
this.last_update = lastUpdate ?: 0L
|
||||
this.initialized = initialized
|
||||
this.viewer_flags = viewerFlags.toInt()
|
||||
this.hide_title = hideTitle
|
||||
this.chapter_flags = chapterFlags.toInt()
|
||||
this.date_added = dateAdded ?: 0L
|
||||
this.filtered_scanlators = filteredScanlators
|
||||
this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode)
|
||||
this.cover_last_modified = coverLastModified
|
||||
this.read = readCount.roundToInt()
|
||||
this.unread = maxOf((total - readCount).roundToInt(), 0)
|
||||
this.totalChapters = total.toInt()
|
||||
this.bookmarkCount = bookmarkCount.roundToInt()
|
||||
this.latestUpdate = latestUpdate
|
||||
this.lastRead = lastRead
|
||||
this.lastFetch = lastFetch
|
||||
}
|
||||
): LibraryManga = LibraryManga(
|
||||
manga = Manga.mapper(
|
||||
id = id,
|
||||
source = source,
|
||||
url = url,
|
||||
artist = artist,
|
||||
author = author,
|
||||
description = description,
|
||||
genre = genre,
|
||||
title = title,
|
||||
status = status,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
favorite = favorite,
|
||||
lastUpdate = lastUpdate,
|
||||
initialized = initialized,
|
||||
viewerFlags = viewerFlags,
|
||||
hideTitle = hideTitle,
|
||||
chapterFlags = chapterFlags,
|
||||
dateAdded = dateAdded,
|
||||
filteredScanlators = filteredScanlators,
|
||||
updateStrategy = updateStrategy,
|
||||
coverLastModified = coverLastModified,
|
||||
),
|
||||
read = readCount.roundToInt(),
|
||||
unread = maxOf((total - readCount).roundToInt(), 0),
|
||||
totalChapters = total.toInt(),
|
||||
bookmarkCount = bookmarkCount.roundToInt(),
|
||||
category = categoryId.toInt(),
|
||||
latestUpdate = latestUpdate,
|
||||
lastRead = lastRead,
|
||||
lastFetch = lastFetch,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -182,15 +182,13 @@ var Manga.vibrantCoverColor: Int?
|
|||
id?.let { MangaCoverMetadata.setVibrantColor(it, value) }
|
||||
}
|
||||
|
||||
fun Manga.Companion.create(source: Long) = MangaImpl().apply {
|
||||
this.source = source
|
||||
}
|
||||
|
||||
fun Manga.Companion.create(pathUrl: String, title: String, source: Long = 0) = MangaImpl().apply {
|
||||
url = pathUrl
|
||||
this.title = title
|
||||
this.source = source
|
||||
}
|
||||
fun Manga.Companion.create(url: String, title: String, source: Long = 0) =
|
||||
MangaImpl(
|
||||
source = source,
|
||||
url = url,
|
||||
).apply {
|
||||
this.title = title
|
||||
}
|
||||
|
||||
fun Manga.Companion.mapper(
|
||||
id: Long,
|
||||
|
@ -213,14 +211,12 @@ fun Manga.Companion.mapper(
|
|||
filteredScanlators: String?,
|
||||
updateStrategy: Long,
|
||||
coverLastModified: Long,
|
||||
) = create(source).apply {
|
||||
) = create(url, title, source).apply {
|
||||
this.id = id
|
||||
this.url = url
|
||||
this.artist = artist
|
||||
this.author = author
|
||||
this.description = description
|
||||
this.genre = genre
|
||||
this.title = title
|
||||
this.status = status.toInt()
|
||||
this.thumbnail_url = thumbnailUrl
|
||||
this.favorite = favorite
|
||||
|
|
|
@ -12,7 +12,114 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga
|
|||
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, var extraChapters: List<ChapterHistory> = emptyList()) {
|
||||
|
||||
companion object {
|
||||
fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl())
|
||||
fun createBlank() = MangaChapterHistory(
|
||||
MangaImpl(null, -1, ""),
|
||||
ChapterImpl(),
|
||||
HistoryImpl(),
|
||||
)
|
||||
|
||||
fun mapper(
|
||||
// manga
|
||||
mangaId: Long,
|
||||
source: Long,
|
||||
mangaUrl: String,
|
||||
artist: String?,
|
||||
author: String?,
|
||||
description: String?,
|
||||
genre: String?,
|
||||
title: String,
|
||||
status: Long,
|
||||
thumbnailUrl: String?,
|
||||
favorite: Boolean,
|
||||
lastUpdate: Long?,
|
||||
initialized: Boolean,
|
||||
viewer: Long,
|
||||
hideTitle: Boolean,
|
||||
chapterFlags: Long,
|
||||
dateAdded: Long?,
|
||||
filteredScanlators: String?,
|
||||
updateStrategy: Long,
|
||||
coverLastModified: Long,
|
||||
// chapter
|
||||
chapterId: Long?,
|
||||
chapterMangaId: Long?,
|
||||
chapterUrl: String?,
|
||||
name: String?,
|
||||
scanlator: String?,
|
||||
read: Boolean?,
|
||||
bookmark: Boolean?,
|
||||
lastPageRead: Long?,
|
||||
pagesLeft: Long?,
|
||||
chapterNumber: Double?,
|
||||
sourceOrder: Long?,
|
||||
dateFetch: Long?,
|
||||
dateUpload: Long?,
|
||||
// history
|
||||
historyId: Long?,
|
||||
historyChapterId: Long?,
|
||||
historyLastRead: Long?,
|
||||
historyTimeRead: Long?,
|
||||
): MangaChapterHistory {
|
||||
val manga = Manga.mapper(
|
||||
id = mangaId,
|
||||
source = source,
|
||||
url = mangaUrl,
|
||||
artist = artist,
|
||||
author = author,
|
||||
description = description,
|
||||
genre = genre,
|
||||
title = title,
|
||||
status = status,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
favorite = favorite,
|
||||
lastUpdate = lastUpdate,
|
||||
initialized = initialized,
|
||||
viewerFlags = viewer,
|
||||
hideTitle = hideTitle,
|
||||
chapterFlags = chapterFlags,
|
||||
dateAdded = dateAdded,
|
||||
filteredScanlators = filteredScanlators,
|
||||
updateStrategy = updateStrategy,
|
||||
coverLastModified = coverLastModified,
|
||||
)
|
||||
|
||||
val chapter = try {
|
||||
Chapter.mapper(
|
||||
id = chapterId!!,
|
||||
mangaId = chapterMangaId!!,
|
||||
url = chapterUrl!!,
|
||||
name = name!!,
|
||||
scanlator = scanlator,
|
||||
read = read!!,
|
||||
bookmark = bookmark!!,
|
||||
lastPageRead = lastPageRead!!,
|
||||
pagesLeft = pagesLeft!!,
|
||||
chapterNumber = chapterNumber!!,
|
||||
sourceOrder = sourceOrder!!,
|
||||
dateFetch = dateFetch!!,
|
||||
dateUpload = dateUpload!!,
|
||||
)
|
||||
} catch (_: NullPointerException) {
|
||||
ChapterImpl()
|
||||
}
|
||||
|
||||
val history = try {
|
||||
History.mapper(
|
||||
id = historyId!!,
|
||||
chapterId = historyChapterId!!,
|
||||
lastRead = historyLastRead,
|
||||
timeRead = historyTimeRead,
|
||||
)
|
||||
} catch (_: NullPointerException) {
|
||||
HistoryImpl().apply {
|
||||
historyChapterId?.let { chapter_id = it }
|
||||
historyLastRead?.let { last_read = it }
|
||||
historyTimeRead?.let { time_read = it }
|
||||
}
|
||||
}
|
||||
|
||||
return MangaChapterHistory(manga, chapter, history)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,13 +8,11 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
open class MangaImpl : Manga {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
override var source: Long = -1
|
||||
|
||||
override lateinit var url: String
|
||||
open class MangaImpl(
|
||||
override var id: Long? = null,
|
||||
override var source: Long = -1,
|
||||
override var url: String = "",
|
||||
) : Manga {
|
||||
|
||||
private val customMangaManager: CustomMangaManager by injectLazy()
|
||||
|
||||
|
@ -72,7 +70,8 @@ open class MangaImpl : Manga {
|
|||
|
||||
override var update_strategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE
|
||||
|
||||
override var filtered_scanlators: String? = null
|
||||
// TODO: It's probably fine to set this to non-null string in the future
|
||||
override var filtered_scanlators: String? = ""
|
||||
|
||||
override lateinit var ogTitle: String
|
||||
override var ogAuthor: String? = null
|
||||
|
@ -106,7 +105,7 @@ open class MangaImpl : Manga {
|
|||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return if (::url.isInitialized) {
|
||||
return if (url.isNotBlank()) {
|
||||
url.hashCode()
|
||||
} else {
|
||||
(id ?: 0L).hashCode()
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
data class SourceIdMangaCount(val source: Long, val count: Int)
|
||||
data class SourceIdMangaCount(val source: Long, val count: Long)
|
||||
|
|
|
@ -8,7 +8,7 @@ interface Track : Serializable {
|
|||
|
||||
var manga_id: Long
|
||||
|
||||
var sync_id: Int
|
||||
var sync_id: Long
|
||||
|
||||
var media_id: Long
|
||||
|
||||
|
@ -18,7 +18,7 @@ interface Track : Serializable {
|
|||
|
||||
var last_chapter_read: Float
|
||||
|
||||
var total_chapters: Int
|
||||
var total_chapters: Long
|
||||
|
||||
var score: Float
|
||||
|
||||
|
@ -39,8 +39,38 @@ interface Track : Serializable {
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun create(serviceId: Int): Track = TrackImpl().apply {
|
||||
fun create(serviceId: Long): Track = TrackImpl().apply {
|
||||
sync_id = serviceId
|
||||
}
|
||||
|
||||
fun mapper(
|
||||
id: Long,
|
||||
mangaId: Long,
|
||||
syncId: Long,
|
||||
remoteId: Long,
|
||||
libraryId: Long?,
|
||||
title: String,
|
||||
lastChapterRead: Double,
|
||||
totalChapters: Long,
|
||||
status: Long,
|
||||
score: Double,
|
||||
remoteUrl: String,
|
||||
startDate: Long,
|
||||
finishDate: Long,
|
||||
) = TrackImpl().apply {
|
||||
this.id = id
|
||||
this.manga_id = mangaId
|
||||
this.sync_id = syncId
|
||||
this.media_id = remoteId
|
||||
this.library_id = libraryId
|
||||
this.title = title
|
||||
this.last_chapter_read = lastChapterRead.toFloat()
|
||||
this.total_chapters = totalChapters
|
||||
this.score = score.toFloat()
|
||||
this.status = status.toInt()
|
||||
this.started_reading_date = startDate
|
||||
this.finished_reading_date = finishDate
|
||||
this.tracking_url = remoteUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,11 @@ class TrackImpl : Track {
|
|||
|
||||
override var id: Long? = null
|
||||
|
||||
override var manga_id: Long = 0
|
||||
override var manga_id: Long = 0L
|
||||
|
||||
override var sync_id: Int = 0
|
||||
override var sync_id: Long = 0L
|
||||
|
||||
override var media_id: Long = 0
|
||||
override var media_id: Long = 0L
|
||||
|
||||
override var library_id: Long? = null
|
||||
|
||||
|
@ -16,7 +16,7 @@ class TrackImpl : Track {
|
|||
|
||||
override var last_chapter_read: Float = 0F
|
||||
|
||||
override var total_chapters: Int = 0
|
||||
override var total_chapters: Long = 0L
|
||||
|
||||
override var score: Float = 0f
|
||||
|
||||
|
@ -43,7 +43,7 @@ class TrackImpl : Track {
|
|||
|
||||
override fun hashCode(): Int {
|
||||
var result = manga_id.hashCode()
|
||||
result = 31 * result + sync_id
|
||||
result = 31 * result + sync_id.hashCode()
|
||||
result = 31 * result + media_id.hashCode()
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
||||
interface CategoryQueries : DbProvider {
|
||||
|
||||
fun getCategories() = db.get()
|
||||
.listOfObjects(Category::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(CategoryTable.TABLE)
|
||||
.orderBy(CategoryTable.COL_ORDER)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getCategoriesForManga(manga: Manga) = db.get()
|
||||
.listOfObjects(Category::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getCategoriesForMangaQuery())
|
||||
.args(manga.id)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
|
||||
|
||||
fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
|
||||
|
||||
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
|
||||
|
||||
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
||||
interface ChapterQueries : DbProvider {
|
||||
|
||||
fun getChapters(manga: Manga) = getChapters(manga.id)
|
||||
|
||||
fun getChapters(mangaId: Long?) = db.get()
|
||||
.listOfObjects(Chapter::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
// FIXME: Migrate to SQLDelight, on halt: in StorIO transaction
|
||||
fun getChapter(id: Long) = db.get()
|
||||
.`object`(Chapter::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_ID} = ?")
|
||||
.whereArgs(id)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
// FIXME: Migrate to SQLDelight, on halt: in StorIO transaction
|
||||
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
|
||||
|
||||
// FIXME: Migrate to SQLDelight, on halt: in StorIO transaction
|
||||
fun updateChapterProgress(chapter: Chapter) = db.put()
|
||||
.`object`(chapter)
|
||||
.withPutResolver(ChapterProgressPutResolver())
|
||||
.prepare()
|
||||
}
|
|
@ -1,192 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryUpsertResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||
import eu.kanade.tachiyomi.util.lang.sqLite
|
||||
|
||||
interface HistoryQueries : DbProvider {
|
||||
|
||||
/**
|
||||
* Insert history into database
|
||||
* @param history object containing history information
|
||||
*/
|
||||
// fun insertHistory(history: History) = db.put().`object`(history).prepare()
|
||||
|
||||
// /**
|
||||
// * Returns history of recent manga containing last read chapter in 25s
|
||||
// * @param date recent date range
|
||||
// * @offset offset the db by
|
||||
// */
|
||||
// fun getRecentManga(date: Date, offset: Int = 0, search: String = "") = db.get()
|
||||
// .listOfObjects(MangaChapterHistory::class.java)
|
||||
// .withQuery(
|
||||
// RawQuery.builder()
|
||||
// .query(getRecentMangasQuery(offset, search.sqLite))
|
||||
// .args(date.time)
|
||||
// .observesTables(HistoryTable.TABLE)
|
||||
// .build()
|
||||
// )
|
||||
// .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||
// .prepare()
|
||||
|
||||
/**
|
||||
* Returns history of recent manga containing last read chapter in 25s
|
||||
* @param date recent date range
|
||||
* @offset offset the db by
|
||||
*/
|
||||
fun getHistoryUngrouped(search: String = "", offset: Int, isResuming: Boolean) = db.get()
|
||||
.listOfObjects(MangaChapterHistory::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getRecentHistoryUngrouped(search.sqLite, offset, isResuming))
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
/**
|
||||
* Returns history of recent manga containing last read chapter in 25s
|
||||
* @param date recent date range
|
||||
* @offset offset the db by
|
||||
*/
|
||||
fun getRecentMangaLimit(search: String = "", offset: Int, isResuming: Boolean) = db.get()
|
||||
.listOfObjects(MangaChapterHistory::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getRecentMangasLimitQuery(search.sqLite, offset, isResuming))
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
/**
|
||||
* Returns history of manga read during period
|
||||
* @param startDate start date of the period
|
||||
* @param endDate end date of the period
|
||||
* @offset offset the db by
|
||||
*/
|
||||
fun getHistoryPerPeriod(startDate: Long, endDate: Long) = db.get()
|
||||
.listOfObjects(MangaChapterHistory::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getHistoryPerPeriodQuery(startDate, endDate))
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
/**
|
||||
* Returns history of recent manga containing last read chapter in 25s
|
||||
* @param date recent date range
|
||||
* @offset offset the db by
|
||||
*/
|
||||
fun getAllRecentsTypes(
|
||||
search: String = "",
|
||||
includeRead: Boolean,
|
||||
endless: Boolean,
|
||||
offset: Int,
|
||||
isResuming: Boolean,
|
||||
) = db.get()
|
||||
.listOfObjects(MangaChapterHistory::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
getAllRecentsType(
|
||||
search.sqLite,
|
||||
includeRead,
|
||||
endless,
|
||||
offset,
|
||||
isResuming,
|
||||
),
|
||||
)
|
||||
// .args(date.time, startDate.time)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun getHistoryByMangaId(mangaId: Long) = db.get()
|
||||
.listOfObjects(History::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getHistoryByMangaId())
|
||||
.args(mangaId)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getTotalReadDuration(): Long {
|
||||
val cursor = db.lowLevel()
|
||||
.rawQuery(
|
||||
RawQuery.builder()
|
||||
.query("SELECT SUM(${HistoryTable.COL_TIME_READ}) FROM ${HistoryTable.TABLE}")
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
cursor.moveToFirst()
|
||||
return cursor.getLong(0)
|
||||
}
|
||||
|
||||
fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
|
||||
.`object`(History::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getHistoryByChapterUrl())
|
||||
.args(chapterUrl)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
/**
|
||||
* Updates the history last read.
|
||||
* Inserts history object if not yet in database
|
||||
* @param history history object
|
||||
*/
|
||||
fun upsertHistoryLastRead(history: History) = db.put()
|
||||
.`object`(history)
|
||||
.withPutResolver(HistoryUpsertResolver())
|
||||
.prepare()
|
||||
|
||||
/**
|
||||
* Updates the history last read.
|
||||
* Inserts history object if not yet in database
|
||||
* @param historyList history object list
|
||||
*/
|
||||
fun upsertHistoryLastRead(historyList: List<History>) = db.inTransactionReturn {
|
||||
db.put()
|
||||
.objects(historyList)
|
||||
.withPutResolver(HistoryUpsertResolver())
|
||||
.prepare()
|
||||
}
|
||||
|
||||
fun deleteHistory() = db.delete()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun deleteHistoryNoLastRead() = db.delete()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.where("${HistoryTable.COL_LAST_READ} = ?")
|
||||
.whereArgs(0)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.Queries
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.inTransaction
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
||||
interface MangaCategoryQueries : DbProvider {
|
||||
|
||||
fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare()
|
||||
|
||||
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
|
||||
|
||||
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(MangaCategoryTable.TABLE)
|
||||
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
||||
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
|
||||
db.inTransaction {
|
||||
deleteOldMangasCategories(mangas).executeAsBlocking()
|
||||
insertMangasCategories(mangasCategories).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.Queries
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaDateAddedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
||||
interface MangaQueries : DbProvider {
|
||||
|
||||
fun getDuplicateLibraryManga(manga: Manga) = db.get()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = 1 AND LOWER(${MangaTable.COL_TITLE}) = ? AND ${MangaTable.COL_SOURCE} != ?")
|
||||
.whereArgs(
|
||||
manga.title.lowercase(),
|
||||
manga.source,
|
||||
)
|
||||
.limit(1)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getFavoriteMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||
.whereArgs(1)
|
||||
.orderBy(MangaTable.COL_TITLE)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getManga(url: String, sourceId: Long) = db.get()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
|
||||
.whereArgs(url, sourceId)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getManga(id: Long) = db.get()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(id)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getSourceIdsWithNonLibraryManga() = db.get()
|
||||
.listOfObjects(SourceIdMangaCount::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getSourceIdsWithNonLibraryMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
.withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||
|
||||
// FIXME: Migrate to SQLDelight, on halt: used by StorIO's inTransaction
|
||||
fun updateMangaFavorite(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaFavoritePutResolver())
|
||||
.prepare()
|
||||
|
||||
// FIXME: Migrate to SQLDelight, on halt: used by StorIO's inTransaction
|
||||
fun updateMangaAdded(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaDateAddedPutResolver())
|
||||
.prepare()
|
||||
|
||||
// FIXME: Migrate to SQLDelight, on halt: used by StorIO's inTransaction
|
||||
fun updateMangaTitle(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaTitlePutResolver())
|
||||
.prepare()
|
||||
|
||||
fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) = db.delete()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)})")
|
||||
.whereArgs(0, *sourceIds.toTypedArray())
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun deleteMangasNotInLibraryAndNotReadBySourceIds(sourceIds: List<Long>) = db.delete()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where(
|
||||
"""
|
||||
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN (
|
||||
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
|
||||
)
|
||||
""".trimIndent(),
|
||||
)
|
||||
.whereArgs(0, *sourceIds.toTypedArray())
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getReadNotInLibraryMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getReadMangaNotInLibraryQuery())
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
}
|
|
@ -1,317 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
||||
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||
|
||||
// TODO: Migrate to SQLDelight
|
||||
/**
|
||||
* Query to get the recent chapters of manga from the library up to a date.
|
||||
*/
|
||||
fun getRecentsQuery(search: String, offset: Int, isResuming: Boolean) =
|
||||
"""
|
||||
SELECT
|
||||
M.url AS mangaUrl,
|
||||
M.*,
|
||||
C.*
|
||||
FROM mangas AS M
|
||||
JOIN chapters AS C
|
||||
ON M._id = C.manga_id
|
||||
LEFT JOIN scanlators_view AS S
|
||||
ON C.manga_id = S.manga_id
|
||||
AND ifnull(C.scanlator, 'N/A') = ifnull(S.name, '/<INVALID>/')
|
||||
WHERE M.favorite = 1
|
||||
AND C.date_fetch > M.date_added
|
||||
AND lower(M.title) LIKE '%$search%'
|
||||
AND S.name IS NULL
|
||||
ORDER BY C.date_fetch DESC
|
||||
${limitAndOffset(true, isResuming, offset)}
|
||||
"""
|
||||
|
||||
fun limitAndOffset(endless: Boolean, isResuming: Boolean, offset: Int): String {
|
||||
return when {
|
||||
isResuming && endless && offset > 0 -> "LIMIT $offset"
|
||||
endless -> "LIMIT ${RecentsPresenter.ENDLESS_LIMIT}\nOFFSET $offset"
|
||||
else -> "LIMIT ${RecentsPresenter.SHORT_LIMIT}"
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Migrate to SQLDelight
|
||||
/**
|
||||
* Query to get the recently read chapters of manga from the library up to a date.
|
||||
* 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
|
||||
* and are read after the given time period
|
||||
*/
|
||||
fun getRecentHistoryUngrouped(
|
||||
search: String = "",
|
||||
offset: Int = 0,
|
||||
isResuming: Boolean,
|
||||
) =
|
||||
"""
|
||||
SELECT
|
||||
M.url AS mangaUrl,
|
||||
M.*,
|
||||
C.*,
|
||||
H.*
|
||||
FROM mangas AS M
|
||||
JOIN chapters AS C
|
||||
ON M._id = C.manga_id
|
||||
JOIN history AS H
|
||||
ON C._id = H.history_chapter_id
|
||||
AND H.history_last_read > 0
|
||||
LEFT JOIN scanlators_view AS S
|
||||
ON C.manga_id = S.manga_id
|
||||
AND ifnull(C.scanlator, 'N/A') = ifnull(S.name, '/<INVALID>/')
|
||||
WHERE lower(M.title) LIKE '%$search%'
|
||||
AND S.name IS NULL
|
||||
ORDER BY H.history_last_read DESC
|
||||
${limitAndOffset(true, isResuming, offset)}
|
||||
"""
|
||||
|
||||
// TODO: Migrate to SQLDelight
|
||||
/**
|
||||
* Query to get the recently read chapters of manga from the library up to a date.
|
||||
* 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
|
||||
* and are read after the given time period
|
||||
*/
|
||||
fun getRecentMangasLimitQuery(
|
||||
search: String = "",
|
||||
offset: Int = 0,
|
||||
isResuming: Boolean,
|
||||
) =
|
||||
"""
|
||||
SELECT
|
||||
M.url AS mangaUrl,
|
||||
M.*,
|
||||
C.*,
|
||||
H.*
|
||||
FROM mangas AS M
|
||||
JOIN chapters AS C
|
||||
ON M._id = C.manga_id
|
||||
JOIN history AS H
|
||||
ON C._id = H.history_chapter_id
|
||||
JOIN (
|
||||
SELECT
|
||||
C2.manga_id AS manga_id,
|
||||
C2._id AS history_chapter_id,
|
||||
MAX(H2.history_last_read) AS history_last_read
|
||||
FROM chapters AS C2 JOIN history AS H2
|
||||
ON C2._id = H2.history_chapter_id
|
||||
GROUP BY C2.manga_id
|
||||
) AS max_last_read
|
||||
ON C.manga_id = max_last_read.manga_id
|
||||
AND max_last_read.history_chapter_id = H.history_chapter_id
|
||||
AND max_last_read.history_last_read > 0
|
||||
LEFT JOIN scanlators_view AS S
|
||||
ON C.manga_id = S.manga_id
|
||||
AND ifnull(C.scanlator, 'N/A') = ifnull(S.name, '/<INVALID>/')
|
||||
WHERE lower(M.title) LIKE '%$search%'
|
||||
AND S.name IS NULL
|
||||
ORDER BY max_last_read.history_last_read DESC
|
||||
${limitAndOffset(true, isResuming, offset)}
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the read chapters of manga from the library during the period.
|
||||
* 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
|
||||
* and are read after the given time period
|
||||
*/
|
||||
fun getHistoryPerPeriodQuery(startDate: Long, endDate: Long) =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
JOIN ${History.TABLE}
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||
AND ${History.TABLE}.${History.COL_LAST_READ} >= $startDate
|
||||
AND ${History.TABLE}.${History.COL_LAST_READ} <= $endDate
|
||||
ORDER BY ${History.TABLE}.${History.COL_LAST_READ} DESC
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the recently read manga that has more chapters to read
|
||||
* The first from checks that there's an unread chapter
|
||||
* 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
|
||||
* and are read after the given time period
|
||||
* The Second Union/Select gets recents chapters
|
||||
* Final Union gets newly added manga
|
||||
*/
|
||||
fun getAllRecentsType(
|
||||
search: String = "",
|
||||
includeRead: Boolean,
|
||||
endless: Boolean,
|
||||
offset: Int = 0,
|
||||
isResuming: Boolean,
|
||||
) = """
|
||||
SELECT * FROM
|
||||
(SELECT mangas.url as mangaUrl, mangas.*, chapters.*, history.*
|
||||
FROM (
|
||||
SELECT mangas.*
|
||||
FROM mangas
|
||||
LEFT JOIN (
|
||||
SELECT manga_id, COUNT(*) AS unread
|
||||
FROM chapters
|
||||
WHERE read = 0
|
||||
GROUP BY manga_id
|
||||
) AS C
|
||||
ON _id = C.manga_id
|
||||
${if (includeRead) "" else "WHERE C.unread > 0"}
|
||||
GROUP BY _id
|
||||
ORDER BY title
|
||||
) AS mangas
|
||||
JOIN chapters
|
||||
ON mangas._id = chapters.manga_id
|
||||
JOIN history
|
||||
ON chapters._id = history.history_chapter_id
|
||||
JOIN (
|
||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
|
||||
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||
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}
|
||||
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||
AND max_last_read.${History.COL_LAST_READ} > 0
|
||||
AND lower(${Manga.COL_TITLE}) LIKE '%$search%')
|
||||
UNION
|
||||
SELECT * FROM
|
||||
(SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*,
|
||||
Null as history_id,
|
||||
Null as history_chapter_id,
|
||||
chapters.date_fetch as history_last_read,
|
||||
Null as history_time_read
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
JOIN (
|
||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID},MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD})
|
||||
FROM ${Chapter.TABLE} JOIN ${Manga.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
WHERE ${Chapter.COL_READ} = 0
|
||||
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS newest_chapter
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = newest_chapter.${Chapter.COL_MANGA_ID}
|
||||
WHERE ${Manga.COL_FAVORITE} = 1
|
||||
AND newest_chapter.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||
AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED}
|
||||
AND lower(${Manga.COL_TITLE}) LIKE '%$search%')
|
||||
UNION
|
||||
SELECT * FROM
|
||||
(SELECT mangas.url as mangaUrl,
|
||||
mangas.*,
|
||||
Null as _id,
|
||||
Null as manga_id,
|
||||
Null as url,
|
||||
Null as name,
|
||||
Null as read,
|
||||
Null as scanlator,
|
||||
Null as bookmark,
|
||||
Null as date_fetch,
|
||||
Null as date_upload,
|
||||
Null as last_page_read,
|
||||
Null as pages_left,
|
||||
Null as chapter_number,
|
||||
Null as source_order,
|
||||
Null as history_id,
|
||||
Null as history_chapter_id,
|
||||
${Manga.TABLE}.${Manga.COL_DATE_ADDED} as history_last_read,
|
||||
Null as history_time_read
|
||||
FROM mangas
|
||||
WHERE ${Manga.COL_FAVORITE} = 1
|
||||
AND lower(${Manga.COL_TITLE}) LIKE '%$search%')
|
||||
ORDER BY history_last_read DESC
|
||||
${limitAndOffset(endless, isResuming, offset)}
|
||||
"""
|
||||
|
||||
fun getHistoryByMangaId() =
|
||||
"""
|
||||
SELECT ${History.TABLE}.*
|
||||
FROM ${History.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||
"""
|
||||
|
||||
fun getHistoryByChapterUrl() =
|
||||
"""
|
||||
SELECT ${History.TABLE}.*
|
||||
FROM ${History.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||
WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||
"""
|
||||
|
||||
fun getLastReadMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
JOIN ${History.TABLE}
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||
WHERE ${Manga.TABLE}.${Manga.COL_FAVORITE} = 1
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER BY max DESC
|
||||
"""
|
||||
|
||||
fun getLastFetchedMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) AS max
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
WHERE ${Manga.TABLE}.${Manga.COL_FAVORITE} = 1
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER BY max DESC
|
||||
"""
|
||||
|
||||
fun getTotalChapterMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER by COUNT(*)
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the categories for a manga.
|
||||
*/
|
||||
fun getCategoriesForMangaQuery() =
|
||||
"""
|
||||
SELECT ${Category.TABLE}.* FROM ${Category.TABLE}
|
||||
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
|
||||
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
||||
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
||||
"""
|
||||
|
||||
/** Query to get the list of sources in the database that have
|
||||
* non-library manga, and how many
|
||||
*/
|
||||
fun getSourceIdsWithNonLibraryMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.COL_SOURCE}, COUNT(*) as ${SourceIdMangaCountGetResolver.COL_COUNT}
|
||||
FROM ${Manga.TABLE}
|
||||
WHERE ${Manga.COL_FAVORITE} = 0
|
||||
GROUP BY ${Manga.COL_SOURCE}
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get manga that are not in library, but have read chapters
|
||||
*/
|
||||
fun getReadMangaNotInLibraryQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*
|
||||
FROM ${Manga.TABLE}
|
||||
WHERE ${Manga.COL_FAVORITE} = 0 AND ${Manga.COL_ID} IN(
|
||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} FROM ${Chapter.TABLE} WHERE ${Chapter.COL_READ} = 1 OR ${Chapter.COL_LAST_PAGE_READ} != 0
|
||||
)
|
||||
"""
|
|
@ -1,10 +1,7 @@
|
|||
package eu.kanade.tachiyomi.data.database.queries
|
||||
/* Unused, seems to be a relic of the past?
|
||||
FIXME: Delete `search_metadata` from sqldelight migration
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.SearchMetadata
|
||||
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
interface SearchMetadataQueries : DbProvider {
|
||||
|
||||
|
@ -50,3 +47,4 @@ interface SearchMetadataQueries : DbProvider {
|
|||
)
|
||||
.prepare()
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
||||
interface TrackQueries : DbProvider {
|
||||
|
||||
fun getTracks(manga: Manga) = getTracks(manga.id)
|
||||
|
||||
fun getTracks(mangaId: Long?) = db.get()
|
||||
.listOfObjects(Track::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(mangaId)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
|
||||
|
||||
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
|
||||
|
||||
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||
.whereArgs(manga.id, sync.id)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
|
||||
class ChapterBackupPutResolver : 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_URL} = ?")
|
||||
.whereArgs(chapter.url)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
||||
put(ChapterTable.COL_READ, chapter.read)
|
||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
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,34 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
|
||||
class ChapterProgressPutResolver : 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) = ContentValues(3).apply {
|
||||
put(ChapterTable.COL_READ, chapter.read)
|
||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||
put(ChapterTable.COL_PAGES_LEFT, chapter.pages_left)
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
|
||||
class ChapterSourceOrderPutResolver : 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_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(chapter.url, chapter.manga_id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
|
||||
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
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.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.mappers.HistoryPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||
|
||||
class HistoryUpsertResolver : HistoryPutResolver() {
|
||||
|
||||
/**
|
||||
* Updates last_read time of chapter
|
||||
*/
|
||||
override fun performPut(db: StorIOSQLite, history: History): PutResult {
|
||||
val updateQuery = mapToUpdateQuery(history)
|
||||
|
||||
val cursor = db.lowLevel().query(
|
||||
Query.builder()
|
||||
.table(updateQuery.table())
|
||||
.where(updateQuery.where())
|
||||
.whereArgs(updateQuery.whereArgs())
|
||||
.build(),
|
||||
)
|
||||
|
||||
return cursor.use { putCursor ->
|
||||
if (putCursor.count == 0) {
|
||||
val insertQuery = mapToInsertQuery(history)
|
||||
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
||||
PutResult.newInsertResult(insertedId, insertQuery.table())
|
||||
} else {
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
||||
.whereArgs(obj.chapter_id)
|
||||
.build()
|
||||
|
||||
private fun mapToUpdateContentValues(history: History) =
|
||||
contentValuesOf(
|
||||
HistoryTable.COL_LAST_READ to history.last_read,
|
||||
HistoryTable.COL_TIME_READ to history.time_read,
|
||||
)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||
|
||||
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
||||
|
||||
companion object {
|
||||
val INSTANCE = MangaChapterGetResolver()
|
||||
}
|
||||
|
||||
private val mangaGetResolver = MangaGetResolver()
|
||||
|
||||
private val chapterGetResolver = ChapterGetResolver()
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): MangaChapter {
|
||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
||||
manga.id = chapter.manga_id
|
||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
||||
|
||||
return MangaChapter(manga, chapter)
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.HistoryImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||
|
||||
class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() {
|
||||
companion object {
|
||||
val INSTANCE = MangaChapterHistoryGetResolver()
|
||||
}
|
||||
|
||||
/**
|
||||
* Manga get resolver
|
||||
*/
|
||||
private val mangaGetResolver = MangaGetResolver()
|
||||
|
||||
/**
|
||||
* Chapter get resolver
|
||||
*/
|
||||
private val chapterResolver = ChapterGetResolver()
|
||||
|
||||
/**
|
||||
* History get resolver
|
||||
*/
|
||||
private val historyGetResolver = HistoryGetResolver()
|
||||
|
||||
/**
|
||||
* Map correct objects from cursor result
|
||||
*/
|
||||
override fun mapFromCursor(cursor: Cursor): MangaChapterHistory {
|
||||
// Get manga object
|
||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||
|
||||
// Get chapter object
|
||||
val chapter =
|
||||
if (!cursor.isNull(cursor.getColumnIndex(ChapterTable.COL_MANGA_ID))) {
|
||||
chapterResolver.mapFromCursor(cursor)
|
||||
} else {
|
||||
ChapterImpl()
|
||||
}
|
||||
|
||||
// Get history object
|
||||
val history =
|
||||
if (!cursor.isNull(cursor.getColumnIndex(HistoryTable.COL_ID))) {
|
||||
historyGetResolver.mapFromCursor(cursor)
|
||||
} else {
|
||||
HistoryImpl().apply {
|
||||
last_read = try {
|
||||
cursor.getLong(cursor.getColumnIndex(HistoryTable.COL_LAST_READ))
|
||||
} catch (e: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make certain column conflicts are dealt with
|
||||
if (chapter.id != null) {
|
||||
manga.id = chapter.manga_id
|
||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
||||
}
|
||||
if (history.id != null) chapter.id = history.chapter_id
|
||||
|
||||
// Return result
|
||||
return MangaChapterHistory(manga, chapter, history)
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
||||
class MangaDateAddedPutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_DATE_ADDED, manga.date_added)
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
||||
class MangaFavoritePutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_FAVORITE, manga.favorite)
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
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.tables.MangaTable
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
||||
class MangaFilteredScanlatorsPutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||
MangaTable.COL_FILTERED_SCANLATORS to manga.filtered_scanlators,
|
||||
)
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
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.tables.MangaTable
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import kotlin.reflect.KProperty1
|
||||
|
||||
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>, private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga): UpdateQuery {
|
||||
val builder = UpdateQuery.builder()
|
||||
|
||||
return if (updateAll) {
|
||||
builder
|
||||
.table(MangaTable.TABLE)
|
||||
.build()
|
||||
} else {
|
||||
builder
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
colName to fieldGetter.get(manga),
|
||||
)
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
||||
class MangaInfoPutResolver() : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_TITLE, manga.originalTitle)
|
||||
put(MangaTable.COL_GENRE, manga.originalGenre)
|
||||
put(MangaTable.COL_AUTHOR, manga.originalAuthor)
|
||||
put(MangaTable.COL_ARTIST, manga.originalArtist)
|
||||
put(MangaTable.COL_DESCRIPTION, manga.originalDescription)
|
||||
put(MangaTable.COL_STATUS, manga.originalStatus)
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
||||
class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
||||
class MangaTitlePutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_TITLE, manga.title)
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.database.Cursor
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
class SourceIdMangaCountGetResolver : DefaultGetResolver<SourceIdMangaCount>() {
|
||||
|
||||
companion object {
|
||||
val INSTANCE = SourceIdMangaCountGetResolver()
|
||||
const val COL_COUNT = "manga_count"
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
override fun mapFromCursor(cursor: Cursor): SourceIdMangaCount {
|
||||
val sourceID = cursor.getLong(cursor.getColumnIndex(MangaTable.COL_SOURCE))
|
||||
val count = cursor.getInt(cursor.getColumnIndex(COL_COUNT))
|
||||
|
||||
return SourceIdMangaCount(sourceID, count)
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object CategoryTable {
|
||||
|
||||
const val TABLE = "categories"
|
||||
|
||||
const val COL_ID = "_id"
|
||||
|
||||
const val COL_NAME = "name"
|
||||
|
||||
const val COL_ORDER = "sort"
|
||||
|
||||
const val COL_FLAGS = "flags"
|
||||
|
||||
const val COL_MANGA_ORDER = "manga_order"
|
||||
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object ChapterTable {
|
||||
|
||||
const val TABLE = "chapters"
|
||||
|
||||
const val COL_ID = "_id"
|
||||
|
||||
const val COL_MANGA_ID = "manga_id"
|
||||
|
||||
const val COL_URL = "url"
|
||||
|
||||
const val COL_NAME = "name"
|
||||
|
||||
const val COL_READ = "read"
|
||||
|
||||
const val COL_SCANLATOR = "scanlator"
|
||||
|
||||
const val COL_BOOKMARK = "bookmark"
|
||||
|
||||
const val COL_DATE_FETCH = "date_fetch"
|
||||
|
||||
const val COL_DATE_UPLOAD = "date_upload"
|
||||
|
||||
const val COL_LAST_PAGE_READ = "last_page_read"
|
||||
|
||||
const val COL_PAGES_LEFT = "pages_left"
|
||||
|
||||
const val COL_CHAPTER_NUMBER = "chapter_number"
|
||||
|
||||
const val COL_SOURCE_ORDER = "source_order"
|
||||
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object HistoryTable {
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
const val TABLE = "history"
|
||||
|
||||
/**
|
||||
* Id column name
|
||||
*/
|
||||
const val COL_ID = "history_id"
|
||||
|
||||
/**
|
||||
* Chapter id column name
|
||||
*/
|
||||
const val COL_CHAPTER_ID = "history_chapter_id"
|
||||
|
||||
/**
|
||||
* Last read column name
|
||||
*/
|
||||
const val COL_LAST_READ = "history_last_read"
|
||||
|
||||
/**
|
||||
* Time read column name
|
||||
*/
|
||||
const val COL_TIME_READ = "history_time_read"
|
||||
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object MangaCategoryTable {
|
||||
|
||||
const val TABLE = "mangas_categories"
|
||||
|
||||
const val COL_ID = "_id"
|
||||
|
||||
const val COL_MANGA_ID = "manga_id"
|
||||
|
||||
const val COL_CATEGORY_ID = "category_id"
|
||||
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object MangaTable {
|
||||
|
||||
const val TABLE = "mangas"
|
||||
|
||||
const val COL_ID = "_id"
|
||||
|
||||
const val COL_SOURCE = "source"
|
||||
|
||||
const val COL_URL = "url"
|
||||
|
||||
const val COL_ARTIST = "artist"
|
||||
|
||||
const val COL_AUTHOR = "author"
|
||||
|
||||
const val COL_DESCRIPTION = "description"
|
||||
|
||||
const val COL_GENRE = "genre"
|
||||
|
||||
const val COL_TITLE = "title"
|
||||
|
||||
const val COL_STATUS = "status"
|
||||
|
||||
const val COL_THUMBNAIL_URL = "thumbnail_url"
|
||||
|
||||
const val COL_FAVORITE = "favorite"
|
||||
|
||||
const val COL_LAST_UPDATE = "last_update"
|
||||
|
||||
const val COL_INITIALIZED = "initialized"
|
||||
|
||||
const val COL_VIEWER = "viewer"
|
||||
|
||||
const val COL_CHAPTER_FLAGS = "chapter_flags"
|
||||
|
||||
const val COL_HIDE_TITLE = "hide_title"
|
||||
|
||||
const val COL_DATE_ADDED = "date_added"
|
||||
|
||||
const val COL_FILTERED_SCANLATORS = "filtered_scanlators"
|
||||
|
||||
const val COL_UPDATE_STRATEGY = "update_strategy"
|
||||
|
||||
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object SearchMetadataTable {
|
||||
const val TABLE = "search_metadata"
|
||||
|
||||
const val COL_MANGA_ID = "manga_id"
|
||||
|
||||
const val COL_UPLOADER = "uploader"
|
||||
|
||||
const val COL_EXTRA = "extra"
|
||||
|
||||
const val COL_INDEXED_EXTRA = "indexed_extra"
|
||||
|
||||
const val COL_EXTRA_VERSION = "extra_version"
|
||||
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object TrackTable {
|
||||
|
||||
const val TABLE = "manga_sync"
|
||||
|
||||
const val COL_ID = "_id"
|
||||
|
||||
const val COL_MANGA_ID = "manga_id"
|
||||
|
||||
const val COL_SYNC_ID = "sync_id"
|
||||
|
||||
const val COL_MEDIA_ID = "remote_id"
|
||||
|
||||
const val COL_LIBRARY_ID = "library_id"
|
||||
|
||||
const val COL_TITLE = "title"
|
||||
|
||||
const val COL_LAST_CHAPTER_READ = "last_chapter_read"
|
||||
|
||||
const val COL_STATUS = "status"
|
||||
|
||||
const val COL_SCORE = "score"
|
||||
|
||||
const val COL_TOTAL_CHAPTERS = "total_chapters"
|
||||
|
||||
const val COL_TRACKING_URL = "remote_url"
|
||||
|
||||
const val COL_START_DATE = "start_date"
|
||||
|
||||
const val COL_FINISH_DATE = "finish_date"
|
||||
|
||||
}
|
|
@ -1,23 +1,53 @@
|
|||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.system.extension
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import eu.kanade.tachiyomi.util.system.launchNonCancellableIO
|
||||
import eu.kanade.tachiyomi.util.system.nameWithoutExtension
|
||||
import java.io.File
|
||||
import java.util.concurrent.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.manga.interactor.GetManga
|
||||
import yokai.domain.storage.StorageManager
|
||||
import java.util.concurrent.*
|
||||
|
||||
/**
|
||||
* Cache where we dump the downloads directory from the filesystem. This class is needed because
|
||||
|
@ -32,11 +62,18 @@ import java.util.concurrent.*
|
|||
*/
|
||||
class DownloadCache(
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val sourceManager: SourceManager,
|
||||
private val provider: DownloadProvider = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val storageManager: StorageManager = Injekt.get(),
|
||||
) {
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED)
|
||||
val changes = _changes.receiveAsFlow()
|
||||
.onStart { emit(Unit) }
|
||||
.shareIn(scope, SharingStarted.Lazily, 1)
|
||||
|
||||
/**
|
||||
* The interval after which this cache should be invalidated. 1 hour shouldn't cause major
|
||||
* issues, as the cache is only used for UI feedback.
|
||||
|
@ -47,12 +84,38 @@ class DownloadCache(
|
|||
* The last time the cache was refreshed.
|
||||
*/
|
||||
private var lastRenew = 0L
|
||||
private var renewalJob: Job? = null
|
||||
|
||||
private var mangaFiles: MutableMap<Long, MutableSet<String>> = mutableMapOf()
|
||||
private val _isInitializing = MutableStateFlow(false)
|
||||
val isInitializing = _isInitializing
|
||||
.debounce(1000L) // Don't notify if it finishes quickly enough
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
private val diskCacheFile: File
|
||||
get() = File(context.cacheDir, "dl_index_cache_v3")
|
||||
|
||||
private val rootDownloadsDirLock = Mutex()
|
||||
private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
|
||||
|
||||
init {
|
||||
// Attempt to read cache file
|
||||
scope.launch {
|
||||
rootDownloadsDirLock.withLock {
|
||||
try {
|
||||
if (diskCacheFile.exists()) {
|
||||
val diskCache = diskCacheFile.inputStream().use {
|
||||
ProtoBuf.decodeFromByteArray<RootDirectory>(it.readBytes())
|
||||
}
|
||||
rootDownloadsDir = diskCache
|
||||
lastRenew = System.currentTimeMillis()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(e) { "Failed to initialize disk cache" }
|
||||
diskCacheFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
storageManager.changes
|
||||
.onEach { forceRenewCache() } // invalidate cache
|
||||
.launchIn(scope)
|
||||
|
@ -71,12 +134,18 @@ class DownloadCache(
|
|||
return provider.findChapterDir(chapter, manga, source) != null
|
||||
}
|
||||
|
||||
checkRenew()
|
||||
renewCache()
|
||||
|
||||
val files = mangaFiles[manga.id]?.toHashSet() ?: return false
|
||||
return provider.getValidChapterDirNames(chapter).any { chapName ->
|
||||
files.any { chapName.equals(it, true) || "$chapName.cbz".equals(it, true) }
|
||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source]
|
||||
if (sourceDir != null) {
|
||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga)]
|
||||
if (mangaDir != null) {
|
||||
return provider.getValidChapterDirNames(
|
||||
chapter,
|
||||
).any { it in mangaDir.chapterDirs }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -85,84 +154,138 @@ class DownloadCache(
|
|||
* @param manga the manga to check.
|
||||
*/
|
||||
fun getDownloadCount(manga: Manga, forceCheckFolder: Boolean = false): Int {
|
||||
checkRenew()
|
||||
renewCache()
|
||||
|
||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source]
|
||||
if (forceCheckFolder) {
|
||||
val source = sourceManager.get(manga.source) ?: return 0
|
||||
val mangaDir = provider.findMangaDir(manga, source)
|
||||
|
||||
if (mangaDir != null) {
|
||||
val listFiles =
|
||||
mangaDir.listFiles { _, filename -> !filename.endsWith(Downloader.TMP_DIR_SUFFIX) }
|
||||
val listFiles = mangaDir.listFiles { _, filename -> !filename.endsWith(Downloader.TMP_DIR_SUFFIX) }
|
||||
if (!listFiles.isNullOrEmpty()) {
|
||||
return listFiles.size
|
||||
}
|
||||
}
|
||||
return 0
|
||||
} else {
|
||||
val files = mangaFiles[manga.id] ?: return 0
|
||||
return files.filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) }.size
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cache needs a renewal and performs it if needed.
|
||||
*/
|
||||
@Synchronized
|
||||
private fun checkRenew() {
|
||||
if (lastRenew + renewInterval < System.currentTimeMillis()) {
|
||||
renew()
|
||||
lastRenew = System.currentTimeMillis()
|
||||
if (sourceDir != null) {
|
||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga)]
|
||||
if (mangaDir != null) {
|
||||
return mangaDir.chapterDirs.size
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fun forceRenewCache() {
|
||||
renew()
|
||||
lastRenew = System.currentTimeMillis()
|
||||
lastRenew = 0L
|
||||
renewalJob?.cancel()
|
||||
diskCacheFile.delete()
|
||||
renewCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* Renews the downloads cache.
|
||||
*/
|
||||
private fun renew() {
|
||||
val onlineSources = sourceManager.getOnlineSources()
|
||||
private fun renewCache() {
|
||||
if (lastRenew + renewInterval >= System.currentTimeMillis() || renewalJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
|
||||
val sourceDirs = storageManager.getDownloadsDirectory()?.listFiles().orEmpty()
|
||||
.associate { it.name to SourceDirectory(it) }.mapNotNullKeys { entry ->
|
||||
onlineSources.find { provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) }?.id
|
||||
renewalJob = scope.launchIO {
|
||||
if (lastRenew == 0L) {
|
||||
_isInitializing.emit(true)
|
||||
}
|
||||
|
||||
val getManga: GetManga by injectLazy()
|
||||
val mangas = runBlocking(Dispatchers.IO) { getManga.awaitAll().groupBy { it.source } }
|
||||
// FIXME: Wait for SourceManager to be initialized
|
||||
val sources = getSources()
|
||||
|
||||
sourceDirs.forEach { sourceValue ->
|
||||
val sourceMangaRaw = mangas[sourceValue.key]?.toMutableSet() ?: return@forEach
|
||||
val sourceMangaPair = sourceMangaRaw.partition { it.favorite }
|
||||
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
|
||||
|
||||
val sourceDir = sourceValue.value
|
||||
rootDownloadsDirLock.withLock {
|
||||
rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
|
||||
|
||||
val mangaDirs = sourceDir.dir.listFiles().orEmpty().mapNotNull { mangaDir ->
|
||||
val name = mangaDir.name ?: return@mapNotNull null
|
||||
val chapterDirs = mangaDir.listFiles().orEmpty().mapNotNull { chapterFile -> chapterFile.name?.substringBeforeLast(".cbz") }.toHashSet()
|
||||
name to MangaDirectory(mangaDir, chapterDirs)
|
||||
}.toMap()
|
||||
val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty()
|
||||
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||
.mapNotNull { dir ->
|
||||
val sourceId = sourceMap[dir.name!!.lowercase()]
|
||||
sourceId?.let { it to SourceDirectory(dir) }
|
||||
}
|
||||
.toMap()
|
||||
|
||||
val trueMangaDirs = mangaDirs.mapNotNull { mangaDir ->
|
||||
val manga = findManga(sourceMangaPair.first, mangaDir.key, sourceValue.key) ?: findManga(sourceMangaPair.second, mangaDir.key, sourceValue.key)
|
||||
val id = manga?.id ?: return@mapNotNull null
|
||||
id to mangaDir.value.files
|
||||
}.toMap()
|
||||
rootDownloadsDir.sourceDirs = sourceDirs
|
||||
|
||||
mangaFiles.putAll(trueMangaDirs)
|
||||
sourceDirs.values
|
||||
.map { sourceDir ->
|
||||
async {
|
||||
sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty()
|
||||
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||
.associate { it.name!! to MangaDirectory(it) }
|
||||
|
||||
sourceDir.mangaDirs.values.forEach { mangaDir ->
|
||||
val chapterDirs = mangaDir.dir?.listFiles().orEmpty()
|
||||
.mapNotNull {
|
||||
when {
|
||||
// Ignore incomplete downloads
|
||||
it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null
|
||||
// Folder of images
|
||||
it.isDirectory -> it.name
|
||||
// CBZ files
|
||||
it.isFile && it.extension == "cbz" -> it.nameWithoutExtension
|
||||
// Anything else is irrelevant
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
.toMutableSet()
|
||||
|
||||
mangaDir.chapterDirs = chapterDirs
|
||||
}
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
|
||||
_isInitializing.emit(false)
|
||||
}
|
||||
}.also {
|
||||
it.invokeOnCompletion(onCancelling = true) { exception ->
|
||||
if (exception != null && exception !is CancellationException) {
|
||||
Logger.e(exception) { "DownloadCache: failed to create cache" }
|
||||
}
|
||||
lastRenew = System.currentTimeMillis()
|
||||
notifyChanges()
|
||||
}
|
||||
}
|
||||
|
||||
// Mainly to notify the indexing notifier UI
|
||||
notifyChanges()
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a manga list and matches the given mangakey and source key
|
||||
*/
|
||||
private fun findManga(mangaList: List<Manga>, mangaKey: String, sourceKey: Long): Manga? {
|
||||
return mangaList.find {
|
||||
DiskUtil.buildValidFilename(it.originalTitle).equals(mangaKey, ignoreCase = true) && it.source == sourceKey
|
||||
private fun getSources(): List<Source> {
|
||||
return sourceManager.getOnlineSources()
|
||||
}
|
||||
|
||||
private fun notifyChanges() {
|
||||
scope.launchNonCancellableIO {
|
||||
_changes.send(Unit)
|
||||
}
|
||||
updateDiskCache()
|
||||
}
|
||||
|
||||
private var updateDiskCacheJob: Job? = null
|
||||
private fun updateDiskCache() {
|
||||
updateDiskCacheJob?.cancel()
|
||||
updateDiskCacheJob = scope.launchIO {
|
||||
delay(1000)
|
||||
ensureActive()
|
||||
val bytes = ProtoBuf.encodeToByteArray(rootDownloadsDir)
|
||||
ensureActive()
|
||||
try {
|
||||
diskCacheFile.writeBytes(bytes)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(e) { "Failed to write disk cache file" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,15 +296,30 @@ class DownloadCache(
|
|||
* @param mangaUniFile the directory of the manga.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
@Synchronized
|
||||
fun addChapter(chapterDirName: String, manga: Manga) {
|
||||
val id = manga.id ?: return
|
||||
val files = mangaFiles[id]
|
||||
if (files == null) {
|
||||
mangaFiles[id] = mutableSetOf(chapterDirName)
|
||||
} else {
|
||||
mangaFiles[id]?.add(chapterDirName)
|
||||
suspend fun addChapter(chapterDirName: String, mangaUniFile: UniFile?, manga: Manga) {
|
||||
rootDownloadsDirLock.withLock {
|
||||
// Retrieve the cached source directory or cache a new one
|
||||
var sourceDir = rootDownloadsDir.sourceDirs[manga.source]
|
||||
if (sourceDir == null) {
|
||||
val source = sourceManager.get(manga.source) ?: return
|
||||
val sourceUniFile = provider.findSourceDir(source) ?: return
|
||||
sourceDir = SourceDirectory(sourceUniFile)
|
||||
rootDownloadsDir.sourceDirs += manga.source to sourceDir
|
||||
}
|
||||
|
||||
// Retrieve the cached manga directory or cache a new one
|
||||
val mangaDirName = provider.getMangaDirName(manga)
|
||||
var mangaDir = sourceDir.mangaDirs[mangaDirName]
|
||||
if (mangaDir == null) {
|
||||
mangaDir = MangaDirectory(mangaUniFile)
|
||||
sourceDir.mangaDirs += mangaDirName to mangaDir
|
||||
}
|
||||
|
||||
// Save the chapter directory
|
||||
mangaDir.chapterDirs += chapterDirName
|
||||
}
|
||||
|
||||
notifyChanges()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -190,26 +328,35 @@ class DownloadCache(
|
|||
* @param chapters the list of chapter to remove.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
@Synchronized
|
||||
fun removeChapters(chapters: List<Chapter>, manga: Manga) {
|
||||
val id = manga.id ?: return
|
||||
for (chapter in chapters) {
|
||||
val list = provider.getValidChapterDirNames(chapter)
|
||||
list.forEach { fileName ->
|
||||
mangaFiles[id]?.firstOrNull { fileName.equals(it, true) }?.let { chapterFile ->
|
||||
mangaFiles[id]?.remove(chapterFile)
|
||||
suspend fun removeChapters(chapters: List<Chapter>, manga: Manga) {
|
||||
rootDownloadsDirLock.withLock {
|
||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga)] ?: return
|
||||
chapters.forEach { chapter ->
|
||||
provider.getValidChapterDirNames(chapter).forEach {
|
||||
if (it in mangaDir.chapterDirs) {
|
||||
mangaDir.chapterDirs -= it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyChanges()
|
||||
}
|
||||
|
||||
fun removeFolders(folders: List<String>, manga: Manga) {
|
||||
val id = manga.id ?: return
|
||||
for (chapter in folders) {
|
||||
if (mangaFiles[id] != null && chapter in mangaFiles[id]!!) {
|
||||
mangaFiles[id]?.remove(chapter)
|
||||
suspend fun removeChapterFolders(folders: List<String>, manga: Manga) {
|
||||
rootDownloadsDirLock.withLock {
|
||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga)] ?: return
|
||||
|
||||
folders.forEach { chapter ->
|
||||
if (chapter in mangaDir.chapterDirs) {
|
||||
mangaDir.chapterDirs -= chapter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyChanges()
|
||||
}
|
||||
|
||||
/*fun renameFolder(from: String, to: String, source: Long) {
|
||||
|
@ -230,34 +377,25 @@ class DownloadCache(
|
|||
*
|
||||
* @param manga the manga to remove.
|
||||
*/
|
||||
@Synchronized
|
||||
fun removeManga(manga: Manga) {
|
||||
mangaFiles.remove(manga.id)
|
||||
suspend fun removeManga(manga: Manga) {
|
||||
rootDownloadsDirLock.withLock {
|
||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||
val mangaDirName = provider.getMangaDirName(manga)
|
||||
if (sourceDir.mangaDirs.containsKey(mangaDirName)) {
|
||||
sourceDir.mangaDirs -= mangaDirName
|
||||
}
|
||||
}
|
||||
|
||||
notifyChanges()
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to store the files under the root downloads directory.
|
||||
*/
|
||||
private class RootDirectory(
|
||||
val dir: UniFile,
|
||||
var files: Map<Long, SourceDirectory> = hashMapOf(),
|
||||
)
|
||||
suspend fun removeSource(source: Source) {
|
||||
rootDownloadsDirLock.withLock {
|
||||
rootDownloadsDir.sourceDirs -= source.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to store the files under a source directory.
|
||||
*/
|
||||
private class SourceDirectory(
|
||||
val dir: UniFile,
|
||||
var files: Map<Long, MutableSet<String>> = hashMapOf(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Class to store the files under a manga directory.
|
||||
*/
|
||||
private class MangaDirectory(
|
||||
val dir: UniFile,
|
||||
var files: MutableSet<String> = hashSetOf(),
|
||||
)
|
||||
notifyChanges()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new map containing only the key entries of [transform] that are not null.
|
||||
|
@ -282,3 +420,53 @@ class DownloadCache(
|
|||
return destination
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to store the files under the root downloads directory.
|
||||
*/
|
||||
@Serializable
|
||||
private class RootDirectory(
|
||||
@Serializable(with = UniFileAsStringSerializer::class)
|
||||
val dir: UniFile?,
|
||||
var sourceDirs: Map<Long, SourceDirectory> = hashMapOf(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Class to store the files under a source directory.
|
||||
*/
|
||||
@Serializable
|
||||
private class SourceDirectory(
|
||||
@Serializable(with = UniFileAsStringSerializer::class)
|
||||
val dir: UniFile?,
|
||||
var mangaDirs: Map<String, MangaDirectory> = hashMapOf(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Class to store the files under a manga directory.
|
||||
*/
|
||||
@Serializable
|
||||
private class MangaDirectory(
|
||||
@Serializable(with = UniFileAsStringSerializer::class)
|
||||
val dir: UniFile?,
|
||||
var chapterDirs: MutableSet<String> = hashSetOf(),
|
||||
)
|
||||
|
||||
private object UniFileAsStringSerializer : KSerializer<UniFile?> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UniFile", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: UniFile?) {
|
||||
return if (value == null) {
|
||||
encoder.encodeNull()
|
||||
} else {
|
||||
encoder.encodeString(value.uri.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): UniFile? {
|
||||
return if (decoder.decodeNotNullMark()) {
|
||||
UniFile.fromUri(Injekt.get<Application>(), Uri.parse(decoder.decodeString()))
|
||||
} else {
|
||||
decoder.decodeNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import androidx.work.NetworkType
|
|||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.OutOfQuotaPolicy
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
|
@ -19,11 +18,13 @@ import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
|||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||
import eu.kanade.tachiyomi.util.system.isOnline
|
||||
import eu.kanade.tachiyomi.util.system.tryToSetForeground
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.i18n.MR
|
||||
|
@ -39,7 +40,7 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout
|
|||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
val firstDL = downloadManager.queue.firstOrNull()
|
||||
val firstDL = downloadManager.queueState.value.firstOrNull()
|
||||
val notification = DownloadNotifier(context).setPlaceholder(firstDL).build()
|
||||
val id = Notifications.ID_DOWNLOAD_CHAPTER
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
|
@ -70,7 +71,6 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout
|
|||
} catch (_: CancellationException) {
|
||||
Result.success()
|
||||
} finally {
|
||||
callListeners(false, downloadManager)
|
||||
if (runExtJobAfter) {
|
||||
ExtensionUpdateJob.runJobAgain(applicationContext, NetworkType.CONNECTED)
|
||||
}
|
||||
|
@ -96,12 +96,6 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout
|
|||
private const val TAG = "Downloader"
|
||||
private const val START_EXT_JOB_AFTER = "StartExtJobAfter"
|
||||
|
||||
private val downloadChannel = MutableSharedFlow<Boolean>(
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
val downloadFlow = downloadChannel.asSharedFlow()
|
||||
|
||||
fun start(context: Context, alsoStartExtJob: Boolean = false) {
|
||||
val request = OneTimeWorkRequestBuilder<DownloadJob>()
|
||||
.addTag(TAG)
|
||||
|
@ -111,24 +105,24 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout
|
|||
}
|
||||
}
|
||||
.build()
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
WorkManager.getInstance(context).cancelUniqueWork(TAG)
|
||||
}
|
||||
|
||||
fun callListeners(downloading: Boolean? = null, downloadManager: DownloadManager? = null) {
|
||||
val dManager by lazy { downloadManager ?: Injekt.get() }
|
||||
downloadChannel.tryEmit(downloading ?: !dManager.isPaused())
|
||||
context.workManager.cancelUniqueWork(TAG)
|
||||
}
|
||||
|
||||
fun isRunning(context: Context): Boolean {
|
||||
return WorkManager.getInstance(context)
|
||||
return context.workManager
|
||||
.getWorkInfosForUniqueWork(TAG)
|
||||
.get()
|
||||
.let { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 }
|
||||
}
|
||||
|
||||
fun isRunningFlow(context: Context): Flow<Boolean> {
|
||||
return context.workManager
|
||||
.getWorkInfosForUniqueWorkFlow(TAG)
|
||||
.map { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,17 +5,23 @@ import co.touchlab.kermit.Logger
|
|||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.domain.download.DownloadPreferences
|
||||
import yokai.i18n.MR
|
||||
|
@ -28,31 +34,21 @@ import yokai.util.lang.getString
|
|||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class DownloadManager(val context: Context) {
|
||||
|
||||
/**
|
||||
* The sources manager.
|
||||
*/
|
||||
private val sourceManager by injectLazy<SourceManager>()
|
||||
class DownloadManager(
|
||||
val context: Context,
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val provider: DownloadProvider = Injekt.get(),
|
||||
private val cache: DownloadCache = Injekt.get(),
|
||||
) {
|
||||
|
||||
private val preferences by injectLazy<PreferencesHelper>()
|
||||
|
||||
private val downloadPreferences by injectLazy<DownloadPreferences>()
|
||||
|
||||
/**
|
||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
||||
*/
|
||||
private val provider = DownloadProvider(context)
|
||||
|
||||
/**
|
||||
* Cache of downloaded chapters.
|
||||
*/
|
||||
private val cache = DownloadCache(context, provider, sourceManager)
|
||||
|
||||
/**
|
||||
* Downloader whose only task is to download chapters.
|
||||
*/
|
||||
private val downloader = Downloader(context, provider, cache, sourceManager)
|
||||
private val downloader = Downloader(context)
|
||||
|
||||
val isRunning: Boolean get() = downloader.isRunning
|
||||
|
||||
|
@ -64,8 +60,11 @@ class DownloadManager(val context: Context) {
|
|||
/**
|
||||
* Downloads queue, where the pending chapters are stored.
|
||||
*/
|
||||
val queue: DownloadQueue
|
||||
get() = downloader.queue
|
||||
val queueState
|
||||
get() = downloader.queueState
|
||||
|
||||
val isDownloaderRunning
|
||||
get() = DownloadJob.isRunningFlow(context)
|
||||
|
||||
/**
|
||||
* Tells the downloader to begin downloads.
|
||||
|
@ -74,7 +73,6 @@ class DownloadManager(val context: Context) {
|
|||
*/
|
||||
fun startDownloads(): Boolean {
|
||||
val hasStarted = downloader.start()
|
||||
DownloadJob.callListeners(downloadManager = this)
|
||||
return hasStarted
|
||||
}
|
||||
|
||||
|
@ -98,22 +96,21 @@ class DownloadManager(val context: Context) {
|
|||
*
|
||||
* @param isNotification value that determines if status is set (needed for view updates)
|
||||
*/
|
||||
fun clearQueue(isNotification: Boolean = false) {
|
||||
deletePendingDownloads(*downloader.queue.toTypedArray())
|
||||
downloader.clearQueue(isNotification)
|
||||
DownloadJob.callListeners(false, this)
|
||||
fun clearQueue() {
|
||||
deletePendingDownloads(*queueState.value.toTypedArray())
|
||||
downloader.clearQueue()
|
||||
downloader.stop()
|
||||
}
|
||||
|
||||
fun startDownloadNow(chapter: Chapter) {
|
||||
val download = downloader.queue.find { it.chapter.id == chapter.id } ?: return
|
||||
val queue = downloader.queue.toMutableList()
|
||||
val download = queueState.value.find { it.chapter.id == chapter.id } ?: return
|
||||
val queue = queueState.value.toMutableList()
|
||||
queue.remove(download)
|
||||
queue.add(0, download)
|
||||
reorderQueue(queue)
|
||||
if (isPaused()) {
|
||||
if (DownloadJob.isRunning(context)) {
|
||||
downloader.start()
|
||||
DownloadJob.callListeners(true, this)
|
||||
} else {
|
||||
DownloadJob.start(context)
|
||||
}
|
||||
|
@ -126,24 +123,12 @@ class DownloadManager(val context: Context) {
|
|||
* @param downloads value to set the download queue to
|
||||
*/
|
||||
fun reorderQueue(downloads: List<Download>) {
|
||||
val wasPaused = isPaused()
|
||||
if (downloads.isEmpty()) {
|
||||
DownloadJob.stop(context)
|
||||
downloader.queue.clear()
|
||||
return
|
||||
}
|
||||
downloader.pause()
|
||||
downloader.queue.clear()
|
||||
downloader.queue.addAll(downloads)
|
||||
if (!wasPaused) {
|
||||
downloader.start()
|
||||
DownloadJob.callListeners(true, this)
|
||||
}
|
||||
downloader.updateQueue(downloads)
|
||||
}
|
||||
|
||||
fun isPaused() = !downloader.isRunning
|
||||
|
||||
fun hasQueue() = downloader.queue.isNotEmpty()
|
||||
fun hasQueue() = queueState.value.isNotEmpty()
|
||||
|
||||
/**
|
||||
* Tells the downloader to enqueue the given list of chapters.
|
||||
|
@ -163,10 +148,7 @@ class DownloadManager(val context: Context) {
|
|||
*/
|
||||
fun addDownloadsToStartOfQueue(downloads: List<Download>) {
|
||||
if (downloads.isEmpty()) return
|
||||
queue.toMutableList().apply {
|
||||
addAll(0, downloads)
|
||||
reorderQueue(this)
|
||||
}
|
||||
reorderQueue(downloads + queueState.value)
|
||||
if (!DownloadJob.isRunning(context)) DownloadJob.start(context)
|
||||
}
|
||||
|
||||
|
@ -189,7 +171,7 @@ class DownloadManager(val context: Context) {
|
|||
|
||||
return files.sortedBy { it.name }
|
||||
.mapIndexed { i, file ->
|
||||
Page(i, uri = file.uri).apply { status = Page.State.READY }
|
||||
Page(i, uri = file.uri).apply { status = Page.State.Ready }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,7 +193,7 @@ class DownloadManager(val context: Context) {
|
|||
* @param chapter the chapter to check.
|
||||
*/
|
||||
fun getChapterDownloadOrNull(chapter: Chapter): Download? {
|
||||
return downloader.queue
|
||||
return queueState.value
|
||||
.firstOrNull { it.chapter.id == chapter.id && it.chapter.manga_id == chapter.manga_id }
|
||||
}
|
||||
|
||||
|
@ -248,27 +230,15 @@ class DownloadManager(val context: Context) {
|
|||
* @param manga the manga of the chapters.
|
||||
* @param source the source of the chapters.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source, force: Boolean = false) {
|
||||
val filteredChapters = if (force) chapters else getChaptersToDelete(chapters, manga)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val wasPaused = isPaused()
|
||||
launchIO {
|
||||
val filteredChapters = if (force) chapters else getChaptersToDelete(chapters, manga)
|
||||
if (filteredChapters.isEmpty()) {
|
||||
return@launch
|
||||
return@launchIO
|
||||
}
|
||||
downloader.pause()
|
||||
downloader.queue.remove(filteredChapters)
|
||||
if (!wasPaused && downloader.queue.isNotEmpty()) {
|
||||
downloader.start()
|
||||
DownloadJob.callListeners(true)
|
||||
} else if (downloader.queue.isEmpty() && DownloadJob.isRunning(context)) {
|
||||
DownloadJob.callListeners(false)
|
||||
DownloadJob.stop(context)
|
||||
} else if (downloader.queue.isEmpty()) {
|
||||
DownloadJob.callListeners(false)
|
||||
downloader.stop()
|
||||
}
|
||||
queue.remove(filteredChapters)
|
||||
|
||||
removeFromDownloadQueue(filteredChapters)
|
||||
|
||||
val chapterDirs =
|
||||
provider.findChapterDirs(filteredChapters, manga, source) + provider.findTempChapterDirs(
|
||||
filteredChapters,
|
||||
|
@ -277,10 +247,27 @@ class DownloadManager(val context: Context) {
|
|||
)
|
||||
chapterDirs.forEach { it.delete() }
|
||||
cache.removeChapters(filteredChapters, manga)
|
||||
|
||||
if (cache.getDownloadCount(manga, true) == 0) { // Delete manga directory if empty
|
||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||
}
|
||||
queue.updateListeners()
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeFromDownloadQueue(chapters: List<Chapter>) {
|
||||
val wasRunning = downloader.isRunning
|
||||
if (wasRunning) {
|
||||
downloader.pause()
|
||||
}
|
||||
|
||||
downloader.removeFromQueue(chapters)
|
||||
|
||||
if (wasRunning) {
|
||||
if (queueState.value.isEmpty()) {
|
||||
downloader.stop()
|
||||
} else if (queueState.value.isNotEmpty()) {
|
||||
downloader.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -298,7 +285,7 @@ class DownloadManager(val context: Context) {
|
|||
* @param manga the manga of the chapters.
|
||||
* @param source the source of the chapters.
|
||||
*/
|
||||
fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source, removeRead: Boolean, removeNonFavorite: Boolean): Int {
|
||||
suspend fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source, removeRead: Boolean, removeNonFavorite: Boolean): Int {
|
||||
var cleaned = 0
|
||||
|
||||
if (removeNonFavorite && !manga.favorite) {
|
||||
|
@ -311,7 +298,7 @@ class DownloadManager(val context: Context) {
|
|||
|
||||
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
|
||||
cleaned += filesWithNoChapter.size
|
||||
cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga)
|
||||
cache.removeChapterFolders(filesWithNoChapter.mapNotNull { it.name }, manga)
|
||||
filesWithNoChapter.forEach { it.delete() }
|
||||
|
||||
if (removeRead) {
|
||||
|
@ -341,12 +328,21 @@ class DownloadManager(val context: Context) {
|
|||
* @param manga the manga to delete.
|
||||
* @param source the source of the manga.
|
||||
*/
|
||||
fun deleteManga(manga: Manga, source: Source) {
|
||||
downloader.clearQueue(manga, true)
|
||||
queue.remove(manga)
|
||||
provider.findMangaDir(manga, source)?.delete()
|
||||
cache.removeManga(manga)
|
||||
queue.updateListeners()
|
||||
fun deleteManga(manga: Manga, source: Source, removeQueued: Boolean = true) {
|
||||
launchIO {
|
||||
if (removeQueued) {
|
||||
downloader.removeFromQueue(manga)
|
||||
}
|
||||
provider.findMangaDir(manga, source)?.delete()
|
||||
cache.removeManga(manga)
|
||||
|
||||
// Delete source directory if empty
|
||||
val sourceDir = provider.findSourceDir(source)
|
||||
if (sourceDir?.listFiles()?.isEmpty() == true) {
|
||||
sourceDir.delete()
|
||||
cache.removeSource(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -377,7 +373,7 @@ class DownloadManager(val context: Context) {
|
|||
* @param oldChapter the existing chapter with the old name.
|
||||
* @param newChapter the target chapter with the new name.
|
||||
*/
|
||||
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||
suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||
val oldNames = provider.getValidChapterDirNames(oldChapter).map { listOf(it, "$it.cbz") }.flatten()
|
||||
var newName = provider.getChapterDirName(newChapter, includeId = downloadPreferences.downloadWithId().get())
|
||||
val mangaDir = provider.getMangaDir(manga, source)
|
||||
|
@ -395,7 +391,7 @@ class DownloadManager(val context: Context) {
|
|||
|
||||
if (oldDownload.renameTo(newName)) {
|
||||
cache.removeChapters(listOf(oldChapter), manga)
|
||||
cache.addChapter(newName, manga)
|
||||
cache.addChapter(newName, mangaDir, manga)
|
||||
} else {
|
||||
Logger.e { "Could not rename downloaded chapter: ${oldNames.joinToString()}" }
|
||||
}
|
||||
|
@ -406,9 +402,6 @@ class DownloadManager(val context: Context) {
|
|||
cache.forceRenewCache()
|
||||
}
|
||||
|
||||
fun addListener(listener: DownloadQueue.DownloadListener) = queue.addListener(listener)
|
||||
fun removeListener(listener: DownloadQueue.DownloadListener) = queue.removeListener(listener)
|
||||
|
||||
private fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
|
||||
// Retrieve the categories that are set to exclude from being deleted on read
|
||||
return if (!preferences.removeBookmarkedChapters().get()) {
|
||||
|
@ -417,4 +410,33 @@ class DownloadManager(val context: Context) {
|
|||
chapters
|
||||
}
|
||||
}
|
||||
|
||||
fun statusFlow(): Flow<Download> = queueState
|
||||
.flatMapLatest { downloads ->
|
||||
downloads
|
||||
.map { download ->
|
||||
download.statusFlow.drop(1).map { download }
|
||||
}
|
||||
.merge()
|
||||
}
|
||||
.onStart {
|
||||
emitAll(
|
||||
queueState.value.filter { download -> download.status == Download.State.DOWNLOADING }.asFlow(),
|
||||
)
|
||||
}
|
||||
|
||||
fun progressFlow(): Flow<Download> = queueState
|
||||
.flatMapLatest { downloads ->
|
||||
downloads
|
||||
.map { download ->
|
||||
download.progressFlow.drop(1).map { download }
|
||||
}
|
||||
.merge()
|
||||
}
|
||||
.onStart {
|
||||
emitAll(
|
||||
queueState.value.filter { download -> download.status == Download.State.DOWNLOADING }
|
||||
.asFlow(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -155,9 +155,10 @@ internal class DownloadNotifier(private val context: Context) {
|
|||
}
|
||||
setStyle(null)
|
||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||
|
||||
// Displays the progress bar on notification
|
||||
show()
|
||||
}
|
||||
// Displays the progress bar on notification
|
||||
notification.show()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -212,8 +213,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||
clearActions()
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
}
|
||||
notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
|
||||
// Reset download information
|
||||
isDownloading = false
|
||||
|
@ -291,8 +293,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||
}
|
||||
color = ContextCompat.getColor(context, R.color.secondaryTachiyomi)
|
||||
setProgress(0, 0, false)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
}
|
||||
notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
|
||||
// Reset download information
|
||||
errorThrown = true
|
||||
|
|
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.download
|
|||
|
||||
import android.content.Context
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
@ -118,8 +117,8 @@ class DownloadProvider(private val context: Context) {
|
|||
* @param manga the manga of the chapter.
|
||||
* @param source the source of the chapter.
|
||||
*/
|
||||
/*
|
||||
fun renameChapters() {
|
||||
val db by injectLazy<DatabaseHelper>()
|
||||
val sourceManager by injectLazy<SourceManager>()
|
||||
val mangas = db.getFavoriteMangas().executeAsBlocking()
|
||||
mangas.forEach sfor@{ manga ->
|
||||
|
@ -136,6 +135,7 @@ class DownloadProvider(private val context: Context) {
|
|||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
fun renameMangaFolder(from: String, to: String, sourceId: Long) {
|
||||
val sourceManager by injectLazy<SourceManager>()
|
||||
|
|
|
@ -59,6 +59,12 @@ class DownloadStore(
|
|||
}
|
||||
}
|
||||
|
||||
fun removeAll(downloads: List<Download>) {
|
||||
preferences.edit {
|
||||
downloads.forEach { remove(getKey(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all the downloads from the store.
|
||||
*/
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
|
@ -29,60 +24,63 @@ import eu.kanade.tachiyomi.util.system.launchNow
|
|||
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||
import eu.kanade.tachiyomi.util.system.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.writeText
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.zip.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.core.archive.ZipWriter
|
||||
import yokai.core.metadata.COMIC_INFO_FILE
|
||||
import yokai.core.metadata.ComicInfo
|
||||
import yokai.core.metadata.getComicInfo
|
||||
import yokai.domain.category.interactor.GetCategories
|
||||
import yokai.domain.download.DownloadPreferences
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.zip.*
|
||||
|
||||
/**
|
||||
* This class is the one in charge of downloading chapters.
|
||||
*
|
||||
* Its [queue] contains the list of chapters to download. In order to download them, the downloader
|
||||
* subscriptions must be running and the list of chapters must be sent to them by [downloadsRelay].
|
||||
*
|
||||
* The queue manipulation must be done in one thread (currently the main thread) to avoid unexpected
|
||||
* behavior, but it's safe to read it from multiple threads.
|
||||
*
|
||||
* @param context the application context.
|
||||
* @param provider the downloads directory provider.
|
||||
* @param cache the downloads cache, used to add the downloads to the cache after their completion.
|
||||
* @param sourceManager the source manager.
|
||||
* Its queue contains the list of chapters to download.
|
||||
*/
|
||||
class Downloader(
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val cache: DownloadCache,
|
||||
private val sourceManager: SourceManager,
|
||||
private val provider: DownloadProvider = Injekt.get(),
|
||||
private val cache: DownloadCache = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
) {
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
private val downloadPreferences: DownloadPreferences by injectLazy()
|
||||
private val chapterCache: ChapterCache by injectLazy()
|
||||
private val xml: XML by injectLazy()
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
private val getCategories: GetCategories by injectLazy()
|
||||
|
||||
/**
|
||||
* Store for persisting downloads across restarts.
|
||||
|
@ -92,30 +90,22 @@ class Downloader(
|
|||
/**
|
||||
* Queue where active downloads are kept.
|
||||
*/
|
||||
val queue = DownloadQueue(store)
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val _queueState = MutableStateFlow<List<Download>>(emptyList())
|
||||
val queueState = _queueState.asStateFlow()
|
||||
|
||||
/**
|
||||
* Notifier for the downloader state and progress.
|
||||
*/
|
||||
private val notifier by lazy { DownloadNotifier(context) }
|
||||
|
||||
/**
|
||||
* Downloader subscription.
|
||||
*/
|
||||
private var subscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Relay to send a list of downloads to the downloader.
|
||||
*/
|
||||
private val downloadsRelay = PublishRelay.create<List<Download>>()
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private var downloaderJob: Job? = null
|
||||
|
||||
/**
|
||||
* Whether the downloader is running.
|
||||
*/
|
||||
val isRunning: Boolean
|
||||
get() = subscription != null
|
||||
get() = downloaderJob?.isActive ?: false
|
||||
|
||||
/**
|
||||
* Whether the downloader is paused
|
||||
|
@ -126,8 +116,7 @@ class Downloader(
|
|||
init {
|
||||
launchNow {
|
||||
val chapters = async { store.restore() }
|
||||
queue.addAll(chapters.await())
|
||||
DownloadJob.callListeners()
|
||||
addAllToQueue(chapters.await())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,17 +127,17 @@ class Downloader(
|
|||
* @return true if the downloader is started, false otherwise.
|
||||
*/
|
||||
fun start(): Boolean {
|
||||
if (subscription != null || queue.isEmpty()) {
|
||||
if (isRunning || queueState.value.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
initializeSubscription()
|
||||
|
||||
val pending = queue.filter { it.status != Download.State.DOWNLOADED }
|
||||
val pending = queueState.value.filter { it.status != Download.State.DOWNLOADED }
|
||||
pending.forEach { if (it.status != Download.State.QUEUE) it.status = Download.State.QUEUE }
|
||||
|
||||
isPaused = false
|
||||
|
||||
downloadsRelay.call(pending)
|
||||
launchDownloaderJob()
|
||||
|
||||
return pending.isNotEmpty()
|
||||
}
|
||||
|
||||
|
@ -156,8 +145,8 @@ class Downloader(
|
|||
* Stops the downloader.
|
||||
*/
|
||||
fun stop(reason: String? = null) {
|
||||
destroySubscription()
|
||||
queue
|
||||
cancelDownloaderJob()
|
||||
queueState.value
|
||||
.filter { it.status == Download.State.DOWNLOADING }
|
||||
.forEach { it.status = Download.State.ERROR }
|
||||
|
||||
|
@ -166,104 +155,109 @@ class Downloader(
|
|||
return
|
||||
}
|
||||
|
||||
DownloadJob.stop(context)
|
||||
if (isPaused && queue.isNotEmpty()) {
|
||||
handler.postDelayed({ notifier.onDownloadPaused() }, 150)
|
||||
if (isPaused && queueState.value.isNotEmpty()) {
|
||||
notifier.onDownloadPaused()
|
||||
} else {
|
||||
notifier.dismiss()
|
||||
}
|
||||
DownloadJob.callListeners(false)
|
||||
|
||||
isPaused = false
|
||||
|
||||
DownloadJob.stop(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the downloader
|
||||
*/
|
||||
fun pause() {
|
||||
destroySubscription()
|
||||
queue
|
||||
cancelDownloaderJob()
|
||||
queueState.value
|
||||
.filter { it.status == Download.State.DOWNLOADING }
|
||||
.forEach { it.status = Download.State.QUEUE }
|
||||
isPaused = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes everything from the queue.
|
||||
*
|
||||
* @param isNotification value that determines if status is set (needed for view updates)
|
||||
*/
|
||||
fun clearQueue(isNotification: Boolean = false) {
|
||||
destroySubscription()
|
||||
fun clearQueue() {
|
||||
cancelDownloaderJob()
|
||||
|
||||
// Needed to update the chapter view
|
||||
if (isNotification) {
|
||||
queue
|
||||
.filter { it.status == Download.State.QUEUE }
|
||||
.forEach { it.status = Download.State.NOT_DOWNLOADED }
|
||||
}
|
||||
queue.clear()
|
||||
internalClearQueue()
|
||||
notifier.dismiss()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes everything from the queue for a certain manga
|
||||
*
|
||||
* @param isNotification value that determines if status is set (needed for view updates)
|
||||
*/
|
||||
fun clearQueue(manga: Manga, isNotification: Boolean = false) {
|
||||
// Needed to update the chapter view
|
||||
if (isNotification) {
|
||||
queue.filter { it.status == Download.State.QUEUE && it.manga.id == manga.id }
|
||||
.forEach { it.status = Download.State.NOT_DOWNLOADED }
|
||||
}
|
||||
queue.remove(manga)
|
||||
if (queue.isEmpty()) {
|
||||
if (DownloadJob.isRunning(context)) DownloadJob.stop(context)
|
||||
stop()
|
||||
}
|
||||
notifier.dismiss()
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the subscriptions to start downloading.
|
||||
*/
|
||||
private fun initializeSubscription() {
|
||||
private fun launchDownloaderJob() {
|
||||
if (isRunning) return
|
||||
|
||||
subscription = downloadsRelay.concatMapIterable { it }
|
||||
// Concurrently download from 5 different sources
|
||||
.groupBy { it.source }
|
||||
.flatMap(
|
||||
{ bySource ->
|
||||
bySource.concatMap { download ->
|
||||
Observable.fromCallable {
|
||||
runBlocking { downloadChapter(download) }
|
||||
download
|
||||
}.subscribeOn(Schedulers.io())
|
||||
downloaderJob = scope.launch {
|
||||
val activeDownloadsFlow = queueState.transformLatest { queue ->
|
||||
while (true) {
|
||||
val activeDownloads = queue.asSequence()
|
||||
// Ignore completed downloads, leave them in the queue
|
||||
.filter {
|
||||
val statusValue = it.status.value
|
||||
Download.State.NOT_DOWNLOADED.value <= statusValue && statusValue <= Download.State.DOWNLOADING.value
|
||||
}
|
||||
.groupBy { it.source }
|
||||
.toList()
|
||||
// Concurrently download from 5 different sources
|
||||
.take(5)
|
||||
.map { (_, downloads) -> downloads.first() }
|
||||
emit(activeDownloads)
|
||||
|
||||
if (activeDownloads.isEmpty()) break
|
||||
// Suspend until a download enters the ERROR state
|
||||
val activeDownloadsErroredFlow =
|
||||
combine(activeDownloads.map(Download::statusFlow)) { states ->
|
||||
states.contains(Download.State.ERROR)
|
||||
}.filter { it }
|
||||
activeDownloadsErroredFlow.first()
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
// Use supervisorScope to cancel child jobs when the downloader job is cancelled
|
||||
supervisorScope {
|
||||
val downloadJobs = mutableMapOf<Download, Job>()
|
||||
|
||||
activeDownloadsFlow.collectLatest { activeDownloads ->
|
||||
val downloadJobsToStop = downloadJobs.filter { it.key !in activeDownloads }
|
||||
downloadJobsToStop.forEach { (download, job) ->
|
||||
job.cancel()
|
||||
downloadJobs.remove(download)
|
||||
}
|
||||
},
|
||||
5,
|
||||
)
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
completeDownload(it)
|
||||
},
|
||||
{ error ->
|
||||
Logger.e(error)
|
||||
notifier.onError(error.message)
|
||||
stop()
|
||||
},
|
||||
)
|
||||
|
||||
val downloadsToStart = activeDownloads.filter { it !in downloadJobs }
|
||||
downloadsToStart.forEach { download ->
|
||||
downloadJobs[download] = launchDownloadJob(download)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.launchDownloadJob(download: Download) = launchIO {
|
||||
try {
|
||||
downloadChapter(download)
|
||||
|
||||
// Remove successful download from queue
|
||||
if (download.status == Download.State.DOWNLOADED) {
|
||||
removeFromQueue(download)
|
||||
}
|
||||
if (areAllDownloadsFinished()) {
|
||||
stop()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Logger.e(e)
|
||||
notifier.onError(e.message)
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the downloader subscriptions.
|
||||
*/
|
||||
private fun destroySubscription() {
|
||||
subscription?.unsubscribe()
|
||||
subscription = null
|
||||
private fun cancelDownloaderJob() {
|
||||
downloaderJob?.cancel()
|
||||
downloaderJob = null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -279,7 +273,7 @@ class Downloader(
|
|||
}
|
||||
|
||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO
|
||||
val wasEmpty = queue.isEmpty()
|
||||
val wasEmpty = queueState.value.isEmpty()
|
||||
// Called in background thread, the operation can be slow with SAF.
|
||||
val chaptersWithoutDir = async {
|
||||
chapters
|
||||
|
@ -292,22 +286,17 @@ class Downloader(
|
|||
// Runs in main thread (synchronization needed).
|
||||
val chaptersToQueue = chaptersWithoutDir.await()
|
||||
// Filter out those already enqueued.
|
||||
.filter { chapter -> queue.none { it.chapter.id == chapter.id } }
|
||||
.filter { chapter -> queueState.value.none { it.chapter.id == chapter.id } }
|
||||
// Create a download for each one.
|
||||
.map { Download(source, manga, it) }
|
||||
|
||||
if (chaptersToQueue.isNotEmpty()) {
|
||||
queue.addAll(chaptersToQueue)
|
||||
|
||||
if (isRunning) {
|
||||
// Send the list of downloads to the downloader.
|
||||
downloadsRelay.call(chaptersToQueue)
|
||||
}
|
||||
addAllToQueue(chaptersToQueue)
|
||||
|
||||
// Start downloader if needed
|
||||
if (autoStart && wasEmpty) {
|
||||
val queuedDownloads = queue.count { it.source !is UnmeteredSource }
|
||||
val maxDownloadsFromSource = queue
|
||||
val queuedDownloads = queueState.value.count { it.source !is UnmeteredSource }
|
||||
val maxDownloadsFromSource = queueState.value
|
||||
.groupBy { it.source }
|
||||
.filterKeys { it !is UnmeteredSource }
|
||||
.maxOfOrNull { it.value.size } ?: 0
|
||||
|
@ -375,11 +364,11 @@ class Downloader(
|
|||
|
||||
// Get all the URLs to the source images, fetch pages if necessary
|
||||
pageList.filter { it.imageUrl.isNullOrEmpty() }.forEach { page ->
|
||||
page.status = Page.State.LOAD_PAGE
|
||||
page.status = Page.State.LoadPage
|
||||
try {
|
||||
page.imageUrl = download.source.getImageUrl(page)
|
||||
} catch (e: Throwable) {
|
||||
page.status = Page.State.ERROR
|
||||
page.status = Page.State.Error
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -398,7 +387,30 @@ class Downloader(
|
|||
}
|
||||
|
||||
// Do after download completes
|
||||
ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname)
|
||||
|
||||
if (!isDownloadSuccessful(download, tmpDir)) {
|
||||
download.status = Download.State.ERROR
|
||||
return
|
||||
}
|
||||
|
||||
createComicInfoFile(
|
||||
tmpDir,
|
||||
download.manga,
|
||||
download.chapter,
|
||||
download.source,
|
||||
)
|
||||
|
||||
// Only rename the directory if it's downloaded
|
||||
if (preferences.saveChaptersAsCBZ().get()) {
|
||||
archiveChapter(mangaDir, chapterDirname, tmpDir)
|
||||
} else {
|
||||
tmpDir.renameTo(chapterDirname)
|
||||
}
|
||||
cache.addChapter(chapterDirname, mangaDir, download.manga)
|
||||
|
||||
DiskUtil.createNoMediaFile(tmpDir, context)
|
||||
|
||||
download.status = Download.State.DOWNLOADED
|
||||
} catch (error: Throwable) {
|
||||
if (error is CancellationException) throw error
|
||||
// If the page list threw, it will resume here
|
||||
|
@ -408,6 +420,31 @@ class Downloader(
|
|||
}
|
||||
}
|
||||
|
||||
private fun isDownloadSuccessful(
|
||||
download: Download,
|
||||
tmpDir: UniFile,
|
||||
): Boolean {
|
||||
// Page list hasn't been initialized
|
||||
val downloadPageCount = download.pages?.size ?: return false
|
||||
|
||||
// Ensure that all pages has been downloaded
|
||||
if (download.downloadedImages != downloadPageCount) return false
|
||||
|
||||
// Ensure that the chapter folder has all the pages
|
||||
val downloadedImagesCount = tmpDir.listFiles().orEmpty().count {
|
||||
val fileName = it.name.orEmpty()
|
||||
when {
|
||||
fileName in listOf(COMIC_INFO_FILE, NOMEDIA_FILE) -> false
|
||||
fileName.endsWith(".tmp") -> false
|
||||
// Only count the first split page and not the others
|
||||
fileName.contains("__") && !fileName.endsWith("__001.jpg") -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
return downloadedImagesCount == downloadPageCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the observable which gets the image from the filesystem if it exists or downloads it
|
||||
* otherwise.
|
||||
|
@ -456,12 +493,12 @@ class Downloader(
|
|||
|
||||
page.uri = file.uri
|
||||
page.progress = 100
|
||||
page.status = Page.State.READY
|
||||
page.status = Page.State.Ready
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
// Mark this page as error and allow to download the remaining
|
||||
page.progress = 0
|
||||
page.status = Page.State.ERROR
|
||||
page.status = Page.State.Error
|
||||
notifier.onError(e.message, chapName, download.manga.title)
|
||||
}
|
||||
}
|
||||
|
@ -480,7 +517,7 @@ class Downloader(
|
|||
tmpDir: UniFile,
|
||||
filename: String,
|
||||
): UniFile {
|
||||
page.status = Page.State.DOWNLOAD_IMAGE
|
||||
page.status = Page.State.DownloadImage
|
||||
page.progress = 0
|
||||
return flow {
|
||||
val response = source.getImage(page)
|
||||
|
@ -558,60 +595,6 @@ class Downloader(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the download was successful.
|
||||
*
|
||||
* @param download the download to check.
|
||||
* @param mangaDir the manga directory of the download.
|
||||
* @param tmpDir the directory where the download is currently stored.
|
||||
* @param dirname the real (non temporary) directory name of the download.
|
||||
*/
|
||||
private fun ensureSuccessfulDownload(
|
||||
download: Download,
|
||||
mangaDir: UniFile,
|
||||
tmpDir: UniFile,
|
||||
dirname: String,
|
||||
) {
|
||||
// Page list hasn't been initialized
|
||||
val downloadPageCount = download.pages?.size ?: return
|
||||
// Ensure that all pages has been downloaded
|
||||
if (download.downloadedImages < downloadPageCount) return
|
||||
// Ensure that the chapter folder has all the pages
|
||||
val downloadedImagesCount = tmpDir.listFiles().orEmpty().count {
|
||||
val fileName = it.name.orEmpty()
|
||||
when {
|
||||
fileName in listOf(COMIC_INFO_FILE, NOMEDIA_FILE) -> false
|
||||
fileName.endsWith(".tmp") -> false
|
||||
// Only count the first split page and not the others
|
||||
fileName.contains("__") && !fileName.endsWith("__001.jpg") -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
download.status = if (downloadedImagesCount == downloadPageCount) {
|
||||
createComicInfoFile(
|
||||
tmpDir,
|
||||
download.manga,
|
||||
download.chapter,
|
||||
download.source,
|
||||
)
|
||||
|
||||
// Only rename the directory if it's downloaded
|
||||
if (preferences.saveChaptersAsCBZ().get()) {
|
||||
archiveChapter(mangaDir, dirname, tmpDir)
|
||||
} else {
|
||||
tmpDir.renameTo(dirname)
|
||||
}
|
||||
cache.addChapter(dirname, download.manga)
|
||||
|
||||
DiskUtil.createNoMediaFile(tmpDir, context)
|
||||
|
||||
Download.State.DOWNLOADED
|
||||
} else {
|
||||
Download.State.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive the chapter pages as a CBZ.
|
||||
*/
|
||||
|
@ -620,8 +603,9 @@ class Downloader(
|
|||
dirname: String,
|
||||
tmpDir: UniFile,
|
||||
) {
|
||||
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX") ?: return
|
||||
ZipWriter(context, zip).use { writer ->
|
||||
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")
|
||||
if (zip?.isFile != true) throw Exception("Failed to create CBZ file for downloaded chapter")
|
||||
ZipWriter(context, zip!!).use { writer ->
|
||||
tmpDir.listFiles()?.forEach { file ->
|
||||
writer.write(file)
|
||||
}
|
||||
|
@ -644,8 +628,15 @@ class Downloader(
|
|||
chapter: Chapter,
|
||||
source: HttpSource,
|
||||
) {
|
||||
val categories =
|
||||
db.getCategoriesForManga(manga).executeAsBlocking().map { it.name.trim() }.takeUnless { it.isEmpty() }
|
||||
val categories = manga.id?.let { mangaId ->
|
||||
// FIXME: Don't do blocking
|
||||
runBlocking {
|
||||
getCategories.awaitByMangaId(mangaId)
|
||||
}
|
||||
}
|
||||
.orEmpty()
|
||||
.map { it.name.trim() }
|
||||
.takeUnless { it.isEmpty() }
|
||||
val url = try { source.getChapterUrl(chapter) } catch (_: Exception) { null }
|
||||
?: source.getChapterUrl(manga, chapter).takeIf { !it.isNullOrBlank() } // FIXME: Not sure if this is necessary
|
||||
|
||||
|
@ -663,25 +654,86 @@ class Downloader(
|
|||
dir.createFile(COMIC_INFO_FILE)?.writeText(xml.encodeToString(ComicInfo.serializer(), comicInfo))
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes a download. This method is called in the main thread.
|
||||
*/
|
||||
private fun completeDownload(download: Download) {
|
||||
// Delete successful downloads from queue
|
||||
if (download.status == Download.State.DOWNLOADED) {
|
||||
// Remove downloaded chapter from queue
|
||||
queue.remove(download)
|
||||
}
|
||||
if (areAllDownloadsFinished()) {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all the queued downloads are in DOWNLOADED or ERROR state.
|
||||
*/
|
||||
private fun areAllDownloadsFinished(): Boolean {
|
||||
return queue.none { it.status <= Download.State.DOWNLOADING }
|
||||
return queueState.value.none { it.status <= Download.State.DOWNLOADING }
|
||||
}
|
||||
|
||||
private fun addAllToQueue(downloads: List<Download>) {
|
||||
_queueState.update {
|
||||
downloads.forEach { download ->
|
||||
download.status = Download.State.QUEUE
|
||||
}
|
||||
store.addAll(downloads)
|
||||
it + downloads
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromQueue(download: Download) {
|
||||
_queueState.update {
|
||||
store.remove(download)
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
download.status = Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
it - download
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun removeFromQueueIf(predicate: (Download) -> Boolean) {
|
||||
_queueState.update { queue ->
|
||||
val downloads = queue.filter { predicate(it) }
|
||||
store.removeAll(downloads)
|
||||
downloads.forEach { download ->
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
download.status = Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
}
|
||||
queue - downloads
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromQueue(chapter: Chapter) {
|
||||
removeFromQueueIf { it.chapter.id == chapter.id }
|
||||
}
|
||||
|
||||
fun removeFromQueue(chapters: List<Chapter>) {
|
||||
removeFromQueueIf { it.chapter.id in chapters.map { it.id } }
|
||||
}
|
||||
|
||||
fun removeFromQueue(manga: Manga) {
|
||||
removeFromQueueIf { it.manga.id == manga.id }
|
||||
}
|
||||
|
||||
private fun internalClearQueue() {
|
||||
_queueState.update {
|
||||
it.forEach { download ->
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
download.status = Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
}
|
||||
store.clear()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateQueue(downloads: List<Download>) {
|
||||
val wasRunning = isRunning
|
||||
|
||||
if (downloads.isEmpty()) {
|
||||
clearQueue()
|
||||
DownloadJob.stop(context)
|
||||
return
|
||||
}
|
||||
|
||||
pause()
|
||||
internalClearQueue()
|
||||
addAllToQueue(downloads)
|
||||
|
||||
if (wasRunning) {
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -4,8 +4,15 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import rx.subjects.PublishSubject
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
||||
|
||||
|
@ -15,19 +22,33 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
|||
get() = pages?.sumOf(Page::progress) ?: 0
|
||||
|
||||
val downloadedImages: Int
|
||||
get() = pages?.count { it.status == Page.State.READY } ?: 0
|
||||
get() = pages?.count { it.status is Page.State.Ready } ?: 0
|
||||
|
||||
@Volatile @Transient
|
||||
var status: State = State.default
|
||||
@Transient
|
||||
private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED)
|
||||
|
||||
@Transient
|
||||
val statusFlow = _statusFlow.asStateFlow()
|
||||
var status: State
|
||||
get() = _statusFlow.value
|
||||
set(status) {
|
||||
field = status
|
||||
statusSubject?.onNext(this)
|
||||
statusCallback?.invoke(this)
|
||||
_statusFlow.value = status
|
||||
}
|
||||
|
||||
@Transient private var statusSubject: PublishSubject<Download>? = null
|
||||
@Transient
|
||||
val progressFlow = flow {
|
||||
if (pages == null) {
|
||||
emit(0)
|
||||
while (pages == null) {
|
||||
delay(50)
|
||||
}
|
||||
}
|
||||
|
||||
@Transient private var statusCallback: ((Download) -> Unit)? = null
|
||||
val progressFlows = pages!!.map(Page::progressFlow)
|
||||
emitAll(combine(progressFlows) { it.average().roundToInt() })
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.debounce(50)
|
||||
|
||||
val pageProgress: Int
|
||||
get() {
|
||||
|
@ -41,21 +62,13 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
|||
return pages.map(Page::progress).average().roundToInt()
|
||||
}
|
||||
|
||||
fun setStatusSubject(subject: PublishSubject<Download>?) {
|
||||
statusSubject = subject
|
||||
}
|
||||
|
||||
fun setStatusCallback(f: ((Download) -> Unit)?) {
|
||||
statusCallback = f
|
||||
}
|
||||
|
||||
enum class State {
|
||||
CHECKED,
|
||||
NOT_DOWNLOADED,
|
||||
QUEUE,
|
||||
DOWNLOADING,
|
||||
DOWNLOADED,
|
||||
ERROR,
|
||||
enum class State(val value: Int) {
|
||||
CHECKED(-1),
|
||||
NOT_DOWNLOADED(0),
|
||||
QUEUE(1),
|
||||
DOWNLOADING(2),
|
||||
DOWNLOADED(3),
|
||||
ERROR(4),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -1,128 +1,84 @@
|
|||
package eu.kanade.tachiyomi.data.download.model
|
||||
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.download.DownloadStore
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import kotlinx.coroutines.MainScope
|
||||
import androidx.annotation.CallSuper
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import rx.subjects.PublishSubject
|
||||
import java.util.concurrent.*
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
class DownloadQueue(
|
||||
private val store: DownloadStore,
|
||||
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>(),
|
||||
) :
|
||||
List<Download> by queue {
|
||||
sealed class DownloadQueue {
|
||||
interface Listener {
|
||||
val progressJobs: MutableMap<Download, Job>
|
||||
|
||||
private val statusSubject = PublishSubject.create<Download>()
|
||||
// Override with presenterScope or viewScope
|
||||
val queueListenerScope: CoroutineScope
|
||||
|
||||
private val updatedRelay = PublishRelay.create<Unit>()
|
||||
|
||||
private val downloadListeners = mutableListOf<DownloadListener>()
|
||||
|
||||
private var scope = MainScope()
|
||||
|
||||
fun addAll(downloads: List<Download>) {
|
||||
downloads.forEach { download ->
|
||||
download.setStatusSubject(statusSubject)
|
||||
download.setStatusCallback(::setPagesFor)
|
||||
download.status = Download.State.QUEUE
|
||||
fun onPageProgressUpdate(download: Download) {
|
||||
onProgressUpdate(download)
|
||||
}
|
||||
queue.addAll(downloads)
|
||||
store.addAll(downloads)
|
||||
updatedRelay.call(Unit)
|
||||
}
|
||||
fun onProgressUpdate(download: Download)
|
||||
fun onQueueUpdate(download: Download)
|
||||
|
||||
fun remove(download: Download) {
|
||||
val removed = queue.remove(download)
|
||||
store.remove(download)
|
||||
download.setStatusSubject(null)
|
||||
download.setStatusCallback(null)
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
download.status = Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
downloadListeners.forEach { it.updateDownload(download) }
|
||||
if (removed) {
|
||||
updatedRelay.call(Unit)
|
||||
}
|
||||
}
|
||||
// Subscribe on presenter/controller creation on UI thread
|
||||
@CallSuper
|
||||
fun onStatusChange(download: Download) {
|
||||
when (download.status) {
|
||||
Download.State.DOWNLOADING -> {
|
||||
launchProgressJob(download)
|
||||
// Initial update of the downloaded pages
|
||||
onQueueUpdate(download)
|
||||
}
|
||||
Download.State.DOWNLOADED -> {
|
||||
cancelProgressJob(download)
|
||||
|
||||
fun updateListeners() {
|
||||
val listeners = downloadListeners.toList()
|
||||
listeners.forEach { it.updateDownloads() }
|
||||
}
|
||||
|
||||
fun remove(chapter: Chapter) {
|
||||
find { it.chapter.id == chapter.id }?.let { remove(it) }
|
||||
}
|
||||
|
||||
fun remove(chapters: List<Chapter>) {
|
||||
for (chapter in chapters) { remove(chapter) }
|
||||
}
|
||||
|
||||
fun remove(manga: Manga) {
|
||||
filter { it.manga.id == manga.id }.forEach { remove(it) }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
queue.forEach { download ->
|
||||
download.setStatusSubject(null)
|
||||
download.setStatusCallback(null)
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
download.status = Download.State.NOT_DOWNLOADED
|
||||
onProgressUpdate(download)
|
||||
onQueueUpdate(download)
|
||||
}
|
||||
Download.State.ERROR -> cancelProgressJob(download)
|
||||
else -> {
|
||||
/* unused */
|
||||
}
|
||||
}
|
||||
downloadListeners.forEach { it.updateDownload(download) }
|
||||
}
|
||||
queue.clear()
|
||||
store.clear()
|
||||
updatedRelay.call(Unit)
|
||||
}
|
||||
|
||||
private fun setPagesFor(download: Download) {
|
||||
if (download.status == Download.State.DOWNLOADING) {
|
||||
if (download.pages != null) {
|
||||
for (page in download.pages!!)
|
||||
scope.launch {
|
||||
page.statusFlow.collectLatest {
|
||||
callListeners(download)
|
||||
}
|
||||
/**
|
||||
* Observe the progress of a download and notify the view.
|
||||
*
|
||||
* @param download the download to observe its progress.
|
||||
*/
|
||||
private fun launchProgressJob(download: Download) {
|
||||
val job = queueListenerScope.launchUI {
|
||||
while (download.pages == null) {
|
||||
delay(50)
|
||||
}
|
||||
|
||||
val progressFlows = download.pages!!.map(Page::progressFlow)
|
||||
combine(progressFlows, Array<Int>::sum)
|
||||
.distinctUntilChanged()
|
||||
.debounce(50)
|
||||
.collectLatest {
|
||||
onPageProgressUpdate(download)
|
||||
}
|
||||
}
|
||||
callListeners(download)
|
||||
} else if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
|
||||
// setPagesSubject(download.pages, null)
|
||||
if (download.status == Download.State.ERROR) {
|
||||
callListeners(download)
|
||||
}
|
||||
} else {
|
||||
callListeners(download)
|
||||
|
||||
// Avoid leaking jobs
|
||||
progressJobs.remove(download)?.cancel()
|
||||
|
||||
progressJobs[download] = job
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes the given download from the progress subscriptions.
|
||||
*
|
||||
* @param download the download to unsubscribe.
|
||||
*/
|
||||
private fun cancelProgressJob(download: Download) {
|
||||
progressJobs.remove(download)?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun callListeners(download: Download) {
|
||||
downloadListeners.forEach { it.updateDownload(download) }
|
||||
}
|
||||
|
||||
// private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
||||
// if (pages != null) {
|
||||
// for (page in pages) {
|
||||
// page.setStatusSubject(subject)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
fun addListener(listener: DownloadListener) {
|
||||
downloadListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: DownloadListener) {
|
||||
downloadListeners.remove(listener)
|
||||
}
|
||||
|
||||
interface DownloadListener {
|
||||
fun updateDownload(download: Download)
|
||||
fun updateDownloads()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.domain.manga.models.Manga
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
@ -11,7 +12,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import nl.adaptivity.xmlutil.AndroidXmlReader
|
||||
import nl.adaptivity.xmlutil.core.AndroidXmlReader
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
||||
import nl.adaptivity.xmlutil.serialization.XmlValue
|
||||
|
@ -26,7 +27,6 @@ import yokai.domain.library.custom.interactor.GetCustomManga
|
|||
import yokai.domain.library.custom.interactor.RelinkCustomManga
|
||||
import yokai.domain.library.custom.model.CustomMangaInfo
|
||||
import yokai.domain.library.custom.model.CustomMangaInfo.Companion.getMangaInfo
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class CustomMangaManager(val context: Context) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
@ -176,8 +176,7 @@ class CustomMangaManager(val context: Context) {
|
|||
val status: Int? = null,
|
||||
) {
|
||||
|
||||
fun toManga() = MangaImpl().apply {
|
||||
id = this@MangaJson.id
|
||||
fun toManga() = MangaImpl(id = this.id).apply {
|
||||
title = this@MangaJson.title ?: ""
|
||||
author = this@MangaJson.author
|
||||
artist = this@MangaJson.artist
|
||||
|
@ -272,9 +271,6 @@ class CustomMangaManager(val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun mangaFromComicInfoObject(id: Long, comicInfo: ComicInfo) = MangaImpl().apply {
|
||||
this.id = id
|
||||
this.copyFromComicInfo(comicInfo)
|
||||
this.title = comicInfo.series?.value ?: ""
|
||||
}
|
||||
private fun mangaFromComicInfoObject(id: Long, comicInfo: ComicInfo) =
|
||||
MangaImpl(id = id).apply { this.copyFromComicInfo(comicInfo) }
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue