Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
882ed1871a | ||
|
|
7c5c7d5632 | ||
|
|
240c0da577 | ||
|
|
7472ef01a8 | ||
|
|
9907095a2d | ||
|
|
658a527d93 | ||
|
|
3d25f3dd62 | ||
|
|
6290e983e9 | ||
|
|
10ae92ec41 | ||
|
|
2c25286155 | ||
|
|
656ede98b6 | ||
|
|
5fb0118f60 | ||
|
|
27e47b2b4b | ||
|
|
f02e3358aa | ||
|
|
7caae3171c | ||
|
|
0cded599b1 | ||
|
|
4e48240158 | ||
|
|
b488e24798 | ||
|
|
7e7d87c147 | ||
|
|
8b6389cf3a | ||
|
|
8a2725b48b | ||
|
|
a7e8fe12f4 | ||
|
|
67a5473e27 | ||
|
|
e16fba8eab | ||
|
|
cdfbafc789 | ||
|
|
c318f5d57b | ||
|
|
556ab85b0d | ||
|
|
20018aed27 | ||
|
|
e2facc87f0 | ||
|
|
07e951039c | ||
|
|
386d9ebc52 | ||
|
|
d8e058a416 | ||
|
|
f5d9c6076c | ||
|
|
e870855602 | ||
|
|
a3a6b4618b | ||
|
|
3ecae3e16b | ||
|
|
111e8d38dc | ||
|
|
d5fa36b11a | ||
|
|
a35c94cf03 | ||
|
|
b2eff46c38 | ||
|
|
07c0187423 | ||
|
|
0074078539 | ||
|
|
d1641ac0e8 | ||
|
|
13d3489cde | ||
|
|
30efe6bd0a | ||
|
|
5d67c71791 | ||
|
|
c8d9887070 | ||
|
|
8268592e8e | ||
|
|
3b63c5c3f9 | ||
|
|
d3918df4e3 | ||
|
|
7e586da521 | ||
|
|
2f5407736c | ||
|
|
b29a98e680 | ||
|
|
239a996846 | ||
|
|
421ecf527d | ||
|
|
c60e55832e | ||
|
|
b1f9568f8a | ||
|
|
dd1adb433b | ||
|
|
5a4bcfbac5 | ||
|
|
179642fa8f | ||
|
|
33111c4f31 | ||
|
|
03721e8090 | ||
|
|
0c025e0497 | ||
|
|
8cec40f672 | ||
|
|
4f7044bffa | ||
|
|
2c6c857e55 | ||
|
|
ede8c4ebad | ||
|
|
88dc85c401 | ||
|
|
7a94f78d78 | ||
|
|
6cacf6b2c5 | ||
|
|
69d198117e | ||
|
|
eb035c1023 | ||
|
|
6dabc7a331 | ||
|
|
58fe29e526 | ||
|
|
20688d6395 | ||
|
|
044dd7fea9 | ||
|
|
2c1f5081f2 | ||
|
|
9243e90e90 | ||
|
|
0dc7813c40 | ||
|
|
847bedb65c | ||
|
|
8e6e0cf673 | ||
|
|
0da3045c73 | ||
|
|
d8accce9e7 | ||
|
|
dbfd27301c | ||
|
|
5ef5d8c343 | ||
|
|
6ff8282f9f | ||
|
|
e3930e0588 | ||
|
|
129db0ad21 | ||
|
|
13a9ed902c | ||
|
|
4e9435418e | ||
|
|
9d990644dd | ||
|
|
c733cab39e | ||
|
|
5a004eeed4 | ||
|
|
012ce933d3 | ||
|
|
9a2c6bc1ff | ||
|
|
f0877e192d | ||
|
|
a04175a106 | ||
|
|
0cdd4cd6ed | ||
|
|
c64d2a4fa4 | ||
|
|
0b4b5a0b56 | ||
|
|
42ff4e877a | ||
|
|
96f206e1a0 | ||
|
|
d2bce7a1e2 | ||
|
|
f1f64eaba1 | ||
|
|
817eaf1bb2 | ||
|
|
16de3fdb97 | ||
|
|
2452f09714 | ||
|
|
b02c1311ff | ||
|
|
21c35f7744 | ||
|
|
bd2f22e9d7 | ||
|
|
d27776fd26 | ||
|
|
416ce15d1d | ||
|
|
9f9abc6d3e | ||
|
|
995f042fc8 | ||
|
|
e905a50ed0 | ||
|
|
5e036992b0 | ||
|
|
cb81a87200 | ||
|
|
fad2ba23b7 | ||
|
|
e093c0820d | ||
|
|
a65c729906 | ||
|
|
1b7fc89d8b | ||
|
|
246b8512e4 | ||
|
|
4209f0e86d | ||
|
|
653f57ea5d | ||
|
|
1766111e1d | ||
|
|
8caa08843e | ||
|
|
a08ae6f977 | ||
|
|
db246b38f2 | ||
|
|
1119d47a80 | ||
|
|
d4ddfad838 | ||
|
|
e2d0799d17 | ||
|
|
f68f31c80f | ||
|
|
b7c8e72ce2 | ||
|
|
9f5b286a45 | ||
|
|
1de204ff02 | ||
|
|
cc4d364f75 | ||
|
|
cd629065b8 | ||
|
|
46dfd9c44e | ||
|
|
edc797035f | ||
|
|
c1eb7d70b7 | ||
|
|
921636ea69 | ||
|
|
40c2262f47 | ||
|
|
b91e8fd0a1 | ||
|
|
ad7ca59532 | ||
|
|
802c16c0df | ||
|
|
efbb0e1b1c | ||
|
|
607f6e9b6c | ||
|
|
72b775e179 | ||
|
|
491f312036 | ||
|
|
31221062dd | ||
|
|
73e6fa24a8 | ||
|
|
b335fd30d1 | ||
|
|
37395003cb | ||
|
|
39b360b132 | ||
|
|
34b16c2da9 | ||
|
|
525108bf26 | ||
|
|
7563bdf900 | ||
|
|
64785be947 | ||
|
|
a714e9b985 | ||
|
|
bc69a8768c | ||
|
|
7d67761ed8 | ||
|
|
3d9b508335 | ||
|
|
78f0c8823d | ||
|
|
145a3c44b7 | ||
|
|
2063f51289 | ||
|
|
da2bfd8fc9 | ||
|
|
eedcd188c3 | ||
|
|
af8b5b63d5 | ||
|
|
4485d0833e | ||
|
|
a163a6af88 | ||
|
|
38bb5af04b | ||
|
|
11804d1cb8 | ||
|
|
22529fc1d2 | ||
|
|
62b0c5fd62 | ||
|
|
1ed28a4fd4 | ||
|
|
5044aea1d7 | ||
|
|
47fda0ccca | ||
|
|
742490775a | ||
|
|
95bbed2b21 | ||
|
|
2b446e227d | ||
|
|
ea6417dbb1 | ||
|
|
d902417959 | ||
|
|
358197db03 | ||
|
|
102fd1f1a1 | ||
|
|
fe9168c6cf | ||
|
|
f7663fc17f | ||
|
|
e370ec36ab | ||
|
|
8f181c74d4 | ||
|
|
8134ec84c6 | ||
|
|
e2fc5bcbb1 | ||
|
|
67af09c4bd | ||
|
|
b81abf4da5 | ||
|
|
2c288bb34d | ||
|
|
df762e8910 | ||
|
|
253aa19701 | ||
|
|
ea608d875b | ||
|
|
d4340a8b95 | ||
|
|
33b3aafb14 | ||
|
|
6cedc2fe35 | ||
|
|
1bb03015d9 | ||
|
|
c5bef72c63 | ||
|
|
8b7c00b337 | ||
|
|
6199d4fab4 | ||
|
|
2063d8647a | ||
|
|
c0a9f85f77 | ||
|
|
b1651c5b01 | ||
|
|
1a8af2caee | ||
|
|
a593070b9f | ||
|
|
1d700b0663 | ||
|
|
2233885142 | ||
|
|
faf87d2942 | ||
|
|
706673aed0 | ||
|
|
eaea688e33 | ||
|
|
256be3521d | ||
|
|
cec1e8593d | ||
|
|
e18f9be810 | ||
|
|
69dca2db43 | ||
|
|
c03321b55e | ||
|
|
f8b4287229 | ||
|
|
ce2fa0c116 | ||
|
|
ebe9a230fd | ||
|
|
a05ceea4bc | ||
|
|
67c2ded1f0 | ||
|
|
66d09c886e | ||
|
|
bbff7513f1 | ||
|
|
4195279c5d | ||
|
|
464d967a0a | ||
|
|
4dd7bf8916 | ||
|
|
4b134eaef9 | ||
|
|
ece62c8b18 | ||
|
|
b430c185b1 | ||
|
|
5c13ccf9e7 | ||
|
|
cb0e2ba305 | ||
|
|
011fbb38cb | ||
|
|
0a69450bc0 | ||
|
|
b4a97f4347 | ||
|
|
96f7aa95ee | ||
|
|
e92f2177e8 | ||
|
|
e4a3cc5290 | ||
|
|
a3a58a25ef | ||
|
|
ab7bda402e | ||
|
|
d78c168e16 | ||
|
|
2b04b4f008 | ||
|
|
a6cc0d742c | ||
|
|
e7e9a2755c | ||
|
|
56ea4ce2f5 | ||
|
|
05528f6dfd | ||
|
|
49cd36c33e | ||
|
|
898d96ce3a | ||
|
|
b257836b76 | ||
|
|
8fab4e56ef | ||
|
|
8e34c230d7 | ||
|
|
b32f2e2973 | ||
|
|
1047d1a598 | ||
|
|
1e5115b743 | ||
|
|
e1e12de91b | ||
|
|
58950afb99 | ||
|
|
89b64b165f | ||
|
|
90ab927bdc | ||
|
|
50ebf4eacb | ||
|
|
064ba080ab | ||
|
|
4ee0345217 | ||
|
|
d45cbbba98 | ||
|
|
612a7a7063 | ||
|
|
62e3ca4068 | ||
|
|
34dcdc89c3 | ||
|
|
bcc202ff3b | ||
|
|
d4a56b3823 | ||
|
|
626239038b | ||
|
|
f7da431e61 | ||
|
|
6ff59c423b | ||
|
|
076d4977e3 | ||
|
|
2a737a9d72 | ||
|
|
8c77a19eb6 | ||
|
|
3918696477 | ||
|
|
8a6a2b8577 | ||
|
|
584023380c | ||
|
|
1f4dd2bdb7 | ||
|
|
f7d5a0732b | ||
|
|
2a6cc882a5 | ||
|
|
f385339da3 | ||
|
|
c37566a999 | ||
|
|
f90a4b1374 | ||
|
|
b649680d3e | ||
|
|
b84c2abe14 | ||
|
|
58aff762c0 | ||
|
|
e5eef69c91 | ||
|
|
54f91b7a88 | ||
|
|
17676173ef | ||
|
|
31f4aa944a | ||
|
|
b30bd495b7 | ||
|
|
06f893436e | ||
|
|
20a2b3ed90 | ||
|
|
b1e3bc46c8 | ||
|
|
8bc5932889 | ||
|
|
3cd597d71e | ||
|
|
48d5ad462e | ||
|
|
40cab87640 | ||
|
|
f405947ad8 | ||
|
|
fd550c15a5 | ||
|
|
d18537cf04 | ||
|
|
f01f0229a8 | ||
|
|
b369eb2d40 | ||
|
|
f08c65c5bf | ||
|
|
1043f4ecd7 | ||
|
|
4f17a32373 | ||
|
|
2203aae6e6 | ||
|
|
7b60c7b6c5 | ||
|
|
be98e06efc | ||
|
|
45239bca34 | ||
|
|
84509ec1a8 | ||
|
|
2493ce7ed7 | ||
|
|
2b1e53b371 | ||
|
|
d643a1769f | ||
|
|
a63b7a188c | ||
|
|
f9c0162b8a | ||
|
|
cda3cb3512 | ||
|
|
766f6938a7 | ||
|
|
8e1ccb517b | ||
|
|
540aab6f83 | ||
|
|
d1969d7897 | ||
|
|
b69e8fd649 | ||
|
|
2e95cf908d | ||
|
|
b589bae2b0 | ||
|
|
e7e4891d82 | ||
|
|
79df676a13 | ||
|
|
03a46decba | ||
|
|
d80482f443 | ||
|
|
735cce7614 | ||
|
|
8ea35a0b36 | ||
|
|
ad90bd3d05 | ||
|
|
fa3856e845 | ||
|
|
752d4ddc8c | ||
|
|
fd2d7a05d1 | ||
|
|
ccf4675867 | ||
|
|
bc043c73ca | ||
|
|
291ab247bc | ||
|
|
d901f470a1 | ||
|
|
0ebb01e39d | ||
|
|
20ac99bf8d | ||
|
|
90e86e1575 | ||
|
|
28652031e7 | ||
|
|
cd8be40cc8 | ||
|
|
52fc937f96 | ||
|
|
68e4722387 | ||
|
|
5f5b38b2f9 | ||
|
|
65f8d3ce81 | ||
|
|
ac571fd63d | ||
|
|
ea2d6e9f31 | ||
|
|
80b999d648 | ||
|
|
54720039e4 | ||
|
|
8c6717c201 | ||
|
|
6610754e25 | ||
|
|
d38622d8a2 | ||
|
|
cd2750a489 | ||
|
|
30140629ae | ||
|
|
0ce29337ef | ||
|
|
6715f29ebb | ||
|
|
b4a8dc9e26 | ||
|
|
c349a0bf00 | ||
|
|
0dc6c142be | ||
|
|
e46c1d9120 | ||
|
|
27ad554ea1 | ||
|
|
5660772b46 | ||
|
|
dab80642ab | ||
|
|
da46191208 | ||
|
|
6484c4de79 | ||
|
|
6716b6042a | ||
|
|
0686859fa7 | ||
|
|
4180862c3a | ||
|
|
7aa20d3211 | ||
|
|
b694b55710 | ||
|
|
024b613063 | ||
|
|
8768d11a20 | ||
|
|
5898303ba4 | ||
|
|
e79d09941c | ||
|
|
c52a60fe77 | ||
|
|
e64f9aa427 | ||
|
|
ee343f78ef | ||
|
|
1f20d5821f | ||
|
|
b0c512e946 | ||
|
|
0d13b7abd1 | ||
|
|
43fd8285c9 | ||
|
|
2f7e8d618a | ||
|
|
a0b910deeb | ||
|
|
e9dc45613c | ||
|
|
b5e90ec3d2 | ||
|
|
3d9f42c1e1 | ||
|
|
e62f430f37 | ||
|
|
9c53564c46 | ||
|
|
f7add6f6cd | ||
|
|
7786e6a827 | ||
|
|
b213e2aabf | ||
|
|
ebca0fafca | ||
|
|
ac8a2ca985 | ||
|
|
6380d7cc02 | ||
|
|
9056ba6b8d | ||
|
|
23801363c4 | ||
|
|
1cdcd7327f | ||
|
|
50ec50003e | ||
|
|
5ff4db1a3f | ||
|
|
394b44c343 | ||
|
|
eb2aa6f773 | ||
|
|
81130e04e5 | ||
|
|
c0ec0758f4 | ||
|
|
e21f7c545e | ||
|
|
c16dd2fcf6 | ||
|
|
90974e6d73 | ||
|
|
8e4f75d85b | ||
|
|
792ee2adb8 | ||
|
|
2c57489c07 | ||
|
|
e3321770fd | ||
|
|
848673f028 | ||
|
|
e6d3003ecf | ||
|
|
90551b7ce7 | ||
|
|
aabac5da13 | ||
|
|
4fad77f9c1 | ||
|
|
0fb6d746eb | ||
|
|
7fc712d19b | ||
|
|
d6018cfe10 | ||
|
|
5b8f231a98 | ||
|
|
cb503f84e1 | ||
|
|
a34f9b80ac | ||
|
|
3e6696b516 | ||
|
|
9cd8bd439a | ||
|
|
ca5b99e23d | ||
|
|
c44fd74f5b | ||
|
|
f82c4834c3 | ||
|
|
dda95bd69c | ||
|
|
a623cb57a7 | ||
|
|
e798d930fb | ||
|
|
f91b0c9e2f | ||
|
|
b6824cf242 | ||
|
|
41477fdac1 | ||
|
|
d4e8afc2f1 | ||
|
|
d42cbfe3e4 | ||
|
|
c7c3ef52e6 | ||
|
|
92344325b6 | ||
|
|
a8172fb3f9 | ||
|
|
a73ec783d5 | ||
|
|
5c9fc4c9e4 | ||
|
|
494ff38ef3 | ||
|
|
b7e6cbf1ab | ||
|
|
b33623b74f | ||
|
|
4f939ce6fc | ||
|
|
4f45f360df | ||
|
|
4ad54f34fe | ||
|
|
574dda5286 | ||
|
|
d788623509 | ||
|
|
f047f6a23b | ||
|
|
48b30f6b03 | ||
|
|
67d32164d9 | ||
|
|
6672a21049 | ||
|
|
b833019a14 | ||
|
|
00eeeb2a5a | ||
|
|
06a0a68143 | ||
|
|
1a7ae468bd | ||
|
|
e8f74d7358 | ||
|
|
984f01da23 | ||
|
|
d6e75410a5 | ||
|
|
7627e1b58c | ||
|
|
5fb31b18ae | ||
|
|
20903e8217 | ||
|
|
8fe348d4eb | ||
|
|
24cda8a179 | ||
|
|
fc954a968b | ||
|
|
aae53a54cd | ||
|
|
cfedcffc98 | ||
|
|
0e74c74a6c | ||
|
|
fb86c45e78 | ||
|
|
124300b215 | ||
|
|
6746c0627b | ||
|
|
0f2cd3b132 | ||
|
|
e94d6fdc1e | ||
|
|
e301b3071c | ||
|
|
3d3dad5e4a | ||
|
|
f8a9033738 | ||
|
|
edb25f5bcd | ||
|
|
bcb239efd1 | ||
|
|
d208211c6f | ||
|
|
0b20b72241 | ||
|
|
5c29459af2 | ||
|
|
e54fd38784 | ||
|
|
94774f13df | ||
|
|
43ec35fe67 | ||
|
|
b2645dc444 | ||
|
|
c79624a82c | ||
|
|
8201f3c9a6 | ||
|
|
e0ac6a1c38 | ||
|
|
400da5566d | ||
|
|
9b67037774 | ||
|
|
48596542c7 | ||
|
|
9715d65d88 | ||
|
|
2e14b31a69 | ||
|
|
1c8ce6c3db | ||
|
|
dc74a6213d | ||
|
|
e9a66bb7a7 | ||
|
|
3c6d4b5f19 | ||
|
|
f04eebe0d8 | ||
|
|
63b5227ccc | ||
|
|
934d9b1df4 | ||
|
|
8994cb3a6e | ||
|
|
4a32de482b | ||
|
|
95f47793d2 | ||
|
|
f27048618d | ||
|
|
c90ad419e8 | ||
|
|
2cdc3ecb9d | ||
|
|
9f89fcd968 | ||
|
|
7c2d51f811 | ||
|
|
41fe7a29cd | ||
|
|
54f3eec303 | ||
|
|
cd91013f69 | ||
|
|
0af16ef05f | ||
|
|
5ba830d56e | ||
|
|
81919e963a | ||
|
|
8a1d4bb958 | ||
|
|
eabac0e709 | ||
|
|
d90783d7c8 | ||
|
|
faf8211e5c | ||
|
|
65d90947c5 |
@@ -1,13 +1,8 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
labels: bug
|
||||
|
||||
---
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
### Expected behaviour
|
||||
- Tell us what should happen
|
||||
|
||||
### Actual behaviour
|
||||
- Tell us what happens
|
||||
|
||||
### Environment data
|
||||
|
||||
Audiobookshelf Version:
|
||||
|
||||
- [ ] Android App?
|
||||
- [ ] iOS App?
|
||||
|
||||
#### Android Issue
|
||||
Android version:
|
||||
|
||||
Device model:
|
||||
|
||||
Stock or customized system:
|
||||
|
||||
#### iOS Issue
|
||||
iOS Version:
|
||||
|
||||
iPhone model:
|
||||
@@ -0,0 +1,88 @@
|
||||
name: 🐞 ABS App Bug Report
|
||||
description: File a bug/issue and help us improve the Audiobookshelf mobile apps.
|
||||
title: '[Bug]: '
|
||||
labels: ['bug', 'triage']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '## App Bug Description'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: 'Thank you for filing a bug report! 🐛'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: 'Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug.'
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What was the Problem?
|
||||
description: Please help us understand the problem.
|
||||
placeholder: Describe the problem you saw or experienced in detail! Include a screenshot or video, if applicable.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce the Issue
|
||||
description: Please help us understand how we can reliably reproduce the issue.
|
||||
placeholder: '1. Go to the library page of a Podcast library and...'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-was-expected
|
||||
attributes:
|
||||
label: What was Expected?
|
||||
description: Please help us understand what should have happened.
|
||||
placeholder: Please help us visualize by including a doodle or screenshot.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '## Mobile Environment'
|
||||
- type: input
|
||||
id: phone-model
|
||||
attributes:
|
||||
label: Phone Model
|
||||
description: What kind of phone are you using?
|
||||
placeholder: e.g. Pixel 6, iPhone 14, Samusung Galaxy s23, etc
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: phone-os
|
||||
attributes:
|
||||
label: Phone OS
|
||||
description: What operating system? If Android, is it stock or a custom system?
|
||||
placeholder: e.g. Android 14, iOS 17.4, etc
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: app-version
|
||||
attributes:
|
||||
label: Audiobookshelf App Version
|
||||
description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.*
|
||||
multiple: true
|
||||
options:
|
||||
- Android App - 0.9.77
|
||||
- iOS App - 0.9.77
|
||||
- Android App - 0.9.76
|
||||
- iOS App - 0.9.76
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: install-source
|
||||
attributes:
|
||||
label: Installation Source
|
||||
multiple: true
|
||||
options:
|
||||
- Google Play Store
|
||||
- Testflight
|
||||
- SideStore
|
||||
- Other (List in "Additional Notes")
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-notes
|
||||
attributes:
|
||||
label: Additional Notes
|
||||
description: Anything else you want to add?
|
||||
placeholder: 'e.g. I have tried X, Y, and Z.'
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
labels: enhancement
|
||||
|
||||
---
|
||||
|
||||
### Is your feature request related to a problem? Please describe.
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
### Describe the solution you'd like
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
### Describe alternatives you've considered
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
### Additional context
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,57 @@
|
||||
name: 🚀 App Feature Request
|
||||
description: Request a feature/enhancement
|
||||
title: '[Enhancement]: '
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '## App Feature Request Description'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: 'Please first search in both issues & discussions for your enhancement and make sure your app is up to date.'
|
||||
- type: textarea
|
||||
id: describe
|
||||
attributes:
|
||||
label: Describe the Feature/Enhancement
|
||||
description: Please help us understand what you want.
|
||||
placeholder: What is your vision?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: the-why
|
||||
attributes:
|
||||
label: Why would this be helpful?
|
||||
description: Please help us understand why this would enhance your experience.
|
||||
placeholder: Explain the "why" or "use case".
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: image
|
||||
attributes:
|
||||
label: Future Implementation (Screenshot)
|
||||
description: Please help us visualize by including a doodle or screenshot.
|
||||
placeholder: How could this look or where could it go?
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '## App Current Implementation'
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Audiobookshelf App Version
|
||||
description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.*
|
||||
multiple: true
|
||||
options:
|
||||
- Android App - 0.9.77
|
||||
- iOS App - 0.9.77
|
||||
- Android App - 0.9.76
|
||||
- iOS App - 0.9.76
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: current-image
|
||||
attributes:
|
||||
label: Current Implementation (Screenshot)
|
||||
description: What screen were you looking at when you thought of this enhancement?
|
||||
placeholder: If an image is not applicable, please explain why.
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Close fixed issues on release.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close issues marked as fixed upon a release.
|
||||
uses: gcampbell-msft/fixed-pending-release@7fa1b75a0c04bcd4b375110522878e5f6100cff5
|
||||
with:
|
||||
label: 'awaiting release'
|
||||
removeLabel: true
|
||||
applyToAll: true
|
||||
message: Fixed in [${releaseTag}](${releaseUrl}).
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Verify all i18n files are alphabetized
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
paths:
|
||||
- strings/** # Should only check if any strings changed
|
||||
|
||||
jobs:
|
||||
update_translations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Set up node to run the javascript
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
# The only argument is the `directory`, which is where the i18n files are
|
||||
# stored.
|
||||
- name: Run Update JSON Files action
|
||||
uses: audiobookshelf/audiobookshelf-i18n-updater@v1.3.0
|
||||
with:
|
||||
directory: 'strings/' # Adjust the directory path as needed
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 400,
|
||||
"proseWrap": "never",
|
||||
"trailingComma": "none",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"options": {
|
||||
"singleQuote": false,
|
||||
"wrapAttributes": false,
|
||||
"sortAttributes": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"octref.vetur"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"vetur.format.defaultFormatterOptions": {
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 400,
|
||||
"proseWrap": "never",
|
||||
"trailingComma": "none"
|
||||
},
|
||||
"prettyhtml": {
|
||||
"printWidth": 400,
|
||||
"singleQuote": false,
|
||||
"wrapAttributes": false,
|
||||
"sortAttributes": false
|
||||
}
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.detectIndentation": true,
|
||||
"editor.tabSize": 2,
|
||||
"javascript.format.semicolons": "remove",
|
||||
"[javascript][json][jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "octref.vetur"
|
||||
}
|
||||
}
|
||||
@@ -18,54 +18,55 @@ kotlin {
|
||||
"--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
|
||||
"--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
|
||||
]
|
||||
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.audiobookshelf.app'
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig true
|
||||
}
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = ['-Xjvm-default=all']
|
||||
freeCompilerArgs = ['-Xjvm-default=all']
|
||||
}
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 109
|
||||
versionName "0.9.78-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
manifestPlaceholders = [
|
||||
"appAuthRedirectScheme": "com.audiobookshelf.app"
|
||||
]
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 104
|
||||
versionName "0.9.73-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
manifestPlaceholders = [
|
||||
"appAuthRedirectScheme": "com.audiobookshelf.app"
|
||||
]
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
versionNameSuffix "-debug"
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
versionNameSuffix "-debug"
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
flatDir{
|
||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||
}
|
||||
flatDir {
|
||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||
}
|
||||
mavenCentral()
|
||||
// TODO: Temporarily using SNAPSHOT version of Simple Storage that resolves https://github.com/anggrayudi/SimpleStorage/issues/133
|
||||
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
|
||||
}
|
||||
|
||||
configurations.configureEach {
|
||||
@@ -80,19 +81,18 @@ configurations.configureEach {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||
implementation project(':capacitor-android')
|
||||
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||
|
||||
implementation project(':capacitor-cordova-android-plugins')
|
||||
|
||||
implementation "androidx.core:core-ktx:$androidx_core_ktx_version"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||
|
||||
@@ -120,7 +120,7 @@ dependencies {
|
||||
implementation 'io.github.pilgr:paperdb:2.7.2'
|
||||
|
||||
// Simple Storage
|
||||
implementation "com.anggrayudi:storage:1.5.5-SNAPSHOT"
|
||||
implementation "com.anggrayudi:storage:1.5.6"
|
||||
|
||||
// OK HTTP
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
||||
|
||||
@@ -10,6 +10,8 @@ android {
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':byteowls-capacitor-filesharer')
|
||||
implementation project(':capacitor-community-keep-awake')
|
||||
implementation project(':capacitor-community-volume-buttons')
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-browser')
|
||||
implementation project(':capacitor-clipboard')
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
"appId": "com.audiobookshelf.app",
|
||||
"appName": "audiobookshelf-app",
|
||||
"webDir": "dist",
|
||||
"bundledWebRuntime": false,
|
||||
"plugins": {
|
||||
"CapacitorHttp": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"androidScheme": "http"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
"pkg": "@byteowls/capacitor-filesharer",
|
||||
"classpath": "com.byteowls.capacitor.filesharer.FileSharerPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor-community/keep-awake",
|
||||
"classpath": "com.getcapacitor.community.keepawake.KeepAwakePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor-community/volume-buttons",
|
||||
"classpath": "com.ryltsov.alex.plugins.volume.buttons.VolumeButtonsPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/app",
|
||||
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class CollapsedSeries(
|
||||
id:String,
|
||||
var libraryId:String?,
|
||||
var name:String,
|
||||
//var nameIgnorePrefix:String,
|
||||
var sequence:String?,
|
||||
var libraryItemIds:MutableList<String>
|
||||
) : LibraryItemWrapper(id) {
|
||||
@get:JsonIgnore
|
||||
val title get() = name
|
||||
@get:JsonIgnore
|
||||
val numBooks get() = libraryItemIds.size
|
||||
|
||||
@JsonIgnore
|
||||
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
|
||||
val mediaId = "__LIBRARY__${libraryId}__SERIE__${id}"
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
//.setIconUri(getCoverUri())
|
||||
.setSubtitle("${numBooks} books")
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.icu.text.DateFormat
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import androidx.media.utils.MediaConstants
|
||||
import com.audiobookshelf.app.media.MediaManager
|
||||
import com.fasterxml.jackson.annotation.*
|
||||
import com.audiobookshelf.app.media.getUriToAbsIconDrawable
|
||||
import java.util.Date
|
||||
|
||||
// This auto-detects whether it is a Book or Podcast
|
||||
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
|
||||
@@ -25,7 +28,8 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
|
||||
open fun removeAudioTrack(localFileId:String) { }
|
||||
@JsonIgnore
|
||||
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata("", false),null) }
|
||||
|
||||
@JsonIgnore
|
||||
open fun checkHasTracks():Boolean { return false }
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@@ -93,10 +97,15 @@ class Podcast(
|
||||
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
override fun checkHasTracks():Boolean {
|
||||
return (episodes?.size ?: numEpisodes ?: 0) > 0
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
|
||||
val localEpisodeId = "local_ep_" + episode.id
|
||||
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,episode.chapters,audioTrack.duration,0, episode.id, localEpisodeId)
|
||||
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,episode.chapters,audioTrack.duration,episode.size, episode.id, localEpisodeId)
|
||||
episodes?.add(newEpisode)
|
||||
|
||||
var index = 1
|
||||
@@ -182,6 +191,11 @@ class Book(
|
||||
override fun getLocalCopy(): Book {
|
||||
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(), ebookFile, null,null, 0)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
override fun checkHasTracks():Boolean {
|
||||
return (tracks?.size ?: numTracks ?: 0) > 0
|
||||
}
|
||||
}
|
||||
|
||||
// This auto-detects whether it is a BookMetadata or PodcastMetadata
|
||||
@@ -214,7 +228,9 @@ class BookMetadata(
|
||||
var authorName:String?,
|
||||
var authorNameLF:String?,
|
||||
var narratorName:String?,
|
||||
var seriesName:String?
|
||||
var seriesName:String?,
|
||||
@JsonFormat(with=[JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY])
|
||||
var series:List<SeriesType>?
|
||||
) : MediaTypeMetadata(title, explicit) {
|
||||
@JsonIgnore
|
||||
override fun getAuthorDisplayName():String { return authorName ?: "Unknown" }
|
||||
@@ -299,11 +315,18 @@ data class PodcastEpisode(
|
||||
|
||||
val libraryItemDescription = libraryItem.getMediaDescription(null, ctx)
|
||||
val mediaId = localEpisodeId ?: id
|
||||
var subtitle = libraryItemDescription.title
|
||||
if (publishedAt !== null) {
|
||||
val sdf = DateFormat.getDateInstance()
|
||||
val publishedAtDT = Date(publishedAt!!)
|
||||
subtitle = "${sdf.format(publishedAtDT)} / $subtitle"
|
||||
}
|
||||
|
||||
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
.setIconUri(coverUri)
|
||||
.setSubtitle(libraryItemDescription.title)
|
||||
.setSubtitle(subtitle)
|
||||
.setExtras(extras)
|
||||
|
||||
libraryItemDescription.iconBitmap?.let {
|
||||
@@ -342,18 +365,39 @@ data class Library(
|
||||
var name:String,
|
||||
var folders:MutableList<Folder>,
|
||||
var icon:String,
|
||||
var mediaType:String
|
||||
var mediaType:String,
|
||||
var stats: LibraryStats?
|
||||
) {
|
||||
@JsonIgnore
|
||||
fun getMediaMetadata(): MediaMetadataCompat {
|
||||
fun getMediaMetadata(context: Context, targetType: String? = null): MediaMetadataCompat {
|
||||
var mediaId = id
|
||||
if (targetType !== null) {
|
||||
mediaId = "__RECENTLY__$id"
|
||||
}
|
||||
return MediaMetadataCompat.Builder().apply {
|
||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, name)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToAbsIconDrawable(context, icon).toString())
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LibraryStats(
|
||||
var totalItems: Int,
|
||||
var totalSize: Long,
|
||||
var totalDuration: Double,
|
||||
var numAudioFiles: Int
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class SeriesType(
|
||||
var id: String,
|
||||
var name: String,
|
||||
var sequence: String?
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class Folder(
|
||||
var id:String,
|
||||
@@ -387,3 +431,84 @@ data class LibraryItemWithEpisode(
|
||||
var libraryItemWrapper:LibraryItemWrapper,
|
||||
var episode:PodcastEpisode
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LibraryItemSearchResultSeriesItemType(
|
||||
var series: LibrarySeriesItem,
|
||||
var books: List<LibraryItem>?
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LibraryItemSearchResultLibraryItemType(
|
||||
val libraryItem: LibraryItem
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LibraryItemSearchResultType(
|
||||
var book:List<LibraryItemSearchResultLibraryItemType>?,
|
||||
var podcast:List<LibraryItemSearchResultLibraryItemType>?,
|
||||
var series:List<LibraryItemSearchResultSeriesItemType>?,
|
||||
var authors:List<LibraryAuthorItem>?
|
||||
)
|
||||
|
||||
// For personalized shelves
|
||||
@JsonTypeInfo(
|
||||
use=JsonTypeInfo.Id.NAME,
|
||||
property = "type",
|
||||
include = JsonTypeInfo.As.PROPERTY,
|
||||
visible = true
|
||||
)
|
||||
@JsonSubTypes(
|
||||
JsonSubTypes.Type(LibraryShelfBookEntity::class, name = "book"),
|
||||
JsonSubTypes.Type(LibraryShelfSeriesEntity::class, name = "series"),
|
||||
JsonSubTypes.Type(LibraryShelfAuthorEntity::class, name = "authors"),
|
||||
JsonSubTypes.Type(LibraryShelfEpisodeEntity::class, name = "episode"),
|
||||
JsonSubTypes.Type(LibraryShelfPodcastEntity::class, name = "podcast")
|
||||
)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
sealed class LibraryShelfType(
|
||||
open val id: String,
|
||||
open val label: String,
|
||||
open val total: Int,
|
||||
open val type: String,
|
||||
)
|
||||
|
||||
data class LibraryShelfBookEntity(
|
||||
override val id: String,
|
||||
override val label: String,
|
||||
override val total: Int,
|
||||
override val type: String,
|
||||
val entities: List<LibraryItem>?
|
||||
) : LibraryShelfType(id, label, total, type)
|
||||
|
||||
data class LibraryShelfSeriesEntity(
|
||||
override val id: String,
|
||||
override val label: String,
|
||||
override val total: Int,
|
||||
override val type: String,
|
||||
val entities: List<LibrarySeriesItem>?
|
||||
) : LibraryShelfType(id, label, total, type)
|
||||
|
||||
data class LibraryShelfAuthorEntity(
|
||||
override val id: String,
|
||||
override val label: String,
|
||||
override val total: Int,
|
||||
override val type: String,
|
||||
val entities: List<LibraryAuthorItem>?
|
||||
) : LibraryShelfType(id, label, total, type)
|
||||
|
||||
data class LibraryShelfEpisodeEntity(
|
||||
override val id: String,
|
||||
override val label: String,
|
||||
override val total: Int,
|
||||
override val type: String,
|
||||
val entities: List<LibraryItem>?
|
||||
) : LibraryShelfType(id, label, total, type)
|
||||
|
||||
data class LibraryShelfPodcastEntity(
|
||||
override val id: String,
|
||||
override val label: String,
|
||||
override val total: Int,
|
||||
override val type: String,
|
||||
val entities: List<LibraryItem>?
|
||||
) : LibraryShelfType(id, label, total, type)
|
||||
|
||||
@@ -20,6 +20,18 @@ enum class ShakeSensitivitySetting {
|
||||
VERY_LOW, LOW, MEDIUM, HIGH, VERY_HIGH
|
||||
}
|
||||
|
||||
enum class DownloadUsingCellularSetting {
|
||||
ASK, ALWAYS, NEVER
|
||||
}
|
||||
|
||||
enum class StreamingUsingCellularSetting {
|
||||
ASK, ALWAYS, NEVER
|
||||
}
|
||||
|
||||
enum class AndroidAutoBrowseSeriesSequenceOrderSetting {
|
||||
ASC, DESC
|
||||
}
|
||||
|
||||
data class ServerConnectionConfig(
|
||||
var id:String,
|
||||
var index:Int,
|
||||
@@ -123,7 +135,11 @@ data class DeviceSettings(
|
||||
var sleepTimerLength: Long, // Time in milliseconds
|
||||
var disableSleepTimerFadeOut: Boolean,
|
||||
var disableSleepTimerResetFeedback: Boolean,
|
||||
var languageCode: String
|
||||
var languageCode: String,
|
||||
var downloadUsingCellular: DownloadUsingCellularSetting,
|
||||
var streamingUsingCellular: StreamingUsingCellularSetting,
|
||||
var androidAutoBrowseLimitForGrouping: Int,
|
||||
var androidAutoBrowseSeriesSequenceOrder: AndroidAutoBrowseSeriesSequenceOrderSetting
|
||||
) {
|
||||
companion object {
|
||||
// Static method to get default device settings
|
||||
@@ -147,7 +163,11 @@ data class DeviceSettings(
|
||||
autoSleepTimerAutoRewindTime = 300000L, // 5 minutes
|
||||
disableSleepTimerFadeOut = false,
|
||||
disableSleepTimerResetFeedback = false,
|
||||
languageCode = "en-us"
|
||||
languageCode = "en-us",
|
||||
downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS,
|
||||
streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS,
|
||||
androidAutoBrowseLimitForGrouping = 100,
|
||||
androidAutoBrowseSeriesSequenceOrder = AndroidAutoBrowseSeriesSequenceOrderSetting.ASC
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import androidx.media.utils.MediaConstants
|
||||
import com.audiobookshelf.app.BuildConfig
|
||||
import com.audiobookshelf.app.R
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class LibraryAuthorItem(
|
||||
id:String,
|
||||
var libraryId:String,
|
||||
var name:String,
|
||||
var description:String?,
|
||||
var imagePath:String?,
|
||||
var addedAt:Long,
|
||||
var updatedAt:Long,
|
||||
var numBooks:Int?,
|
||||
var libraryItems:MutableList<LibraryItem>?,
|
||||
var series:MutableList<LibrarySeriesItem>?
|
||||
) : LibraryItemWrapper(id) {
|
||||
@get:JsonIgnore
|
||||
val title get() = name
|
||||
|
||||
@get:JsonIgnore
|
||||
val bookCount get() = if (numBooks != null) numBooks else libraryItems!!.size
|
||||
|
||||
@JsonIgnore
|
||||
fun getPortraitUri(): Uri {
|
||||
if (imagePath == null) {
|
||||
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.md_account_outline)
|
||||
}
|
||||
|
||||
return Uri.parse("${DeviceManager.serverAddress}/api/authors/$id/image?token=${DeviceManager.token}")
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, groupTitle: String?): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
if (groupTitle !== null) {
|
||||
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
|
||||
}
|
||||
|
||||
val mediaId = "__LIBRARY__${libraryId}__AUTHOR__${id}"
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
.setIconUri(getPortraitUri())
|
||||
.setSubtitle("${bookCount} books")
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
|
||||
return getMediaDescription(progress, ctx, null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class LibraryCollection(
|
||||
id:String,
|
||||
var libraryId:String,
|
||||
var name:String,
|
||||
//var userId:String?,
|
||||
var description:String?,
|
||||
var books:MutableList<LibraryItem>?,
|
||||
) : LibraryItemWrapper(id) {
|
||||
@get:JsonIgnore
|
||||
val title get() = name
|
||||
|
||||
@get:JsonIgnore
|
||||
val bookCount get() = if (books != null) books!!.size else 0
|
||||
|
||||
@get:JsonIgnore
|
||||
val audiobookCount get() = books?.filter { book -> (book.media as Book).getAudioTracks().isNotEmpty() }?.size ?: 0
|
||||
|
||||
@JsonIgnore
|
||||
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
|
||||
val mediaId = "__LIBRARY__${libraryId}__COLLECTION__${id}"
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
//.setIconUri(getCoverUri())
|
||||
.setSubtitle("${bookCount} books")
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -32,10 +32,18 @@ class LibraryItem(
|
||||
var media:MediaType,
|
||||
var libraryFiles:MutableList<LibraryFile>?,
|
||||
var userMediaProgress:MediaProgress?, // Only included when requesting library item with progress (for downloads)
|
||||
var localLibraryItemId:String? // For Android Auto
|
||||
var collapsedSeries: CollapsedSeries?,
|
||||
var localLibraryItemId:String?, // For Android Auto
|
||||
val recentEpisode: PodcastEpisode? // Podcast episode shelf uses this
|
||||
) : LibraryItemWrapper(id) {
|
||||
@get:JsonIgnore
|
||||
val title get() = media.metadata.title
|
||||
val title: String
|
||||
get() {
|
||||
if (collapsedSeries != null) {
|
||||
return collapsedSeries!!.title
|
||||
}
|
||||
return media.metadata.title
|
||||
}
|
||||
@get:JsonIgnore
|
||||
val authorName get() = media.metadata.getAuthorDisplayName()
|
||||
|
||||
@@ -50,57 +58,116 @@ class LibraryItem(
|
||||
|
||||
@JsonIgnore
|
||||
fun checkHasTracks():Boolean {
|
||||
return if (mediaType == "podcast") {
|
||||
((media as Podcast).numEpisodes ?: 0) > 0
|
||||
} else {
|
||||
((media as Book).numTracks ?: 0) > 0
|
||||
return media.checkHasTracks()
|
||||
}
|
||||
|
||||
@get:JsonIgnore
|
||||
val seriesSequence: String
|
||||
get() {
|
||||
if (mediaType != "podcast") {
|
||||
return ((media as Book).metadata as BookMetadata).series?.get(0)?.sequence.orEmpty()
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@get:JsonIgnore
|
||||
val seriesSequenceParts: List<String>
|
||||
get() {
|
||||
if (seriesSequence.isEmpty()) {
|
||||
return listOf("")
|
||||
}
|
||||
return seriesSequence.split(".", limit = 2)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?, showSeriesNumber: Boolean?, groupTitle: String?): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
|
||||
if (collapsedSeries == null) {
|
||||
if (localLibraryItemId != null) {
|
||||
extras.putLong(
|
||||
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
|
||||
MediaDescriptionCompat.STATUS_DOWNLOADED
|
||||
)
|
||||
}
|
||||
|
||||
if (progress != null) {
|
||||
if (progress.isFinished) {
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
|
||||
)
|
||||
} else {
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
|
||||
)
|
||||
extras.putDouble(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
|
||||
)
|
||||
}
|
||||
} else if (mediaType != "podcast") {
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
|
||||
)
|
||||
}
|
||||
|
||||
if (media.metadata.explicit) {
|
||||
extras.putLong(
|
||||
MediaConstants.METADATA_KEY_IS_EXPLICIT,
|
||||
MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
|
||||
)
|
||||
}
|
||||
}
|
||||
if (groupTitle !== null) {
|
||||
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
|
||||
}
|
||||
|
||||
val mediaId = if (localLibraryItemId != null) {
|
||||
localLibraryItemId
|
||||
} else if (collapsedSeries != null) {
|
||||
if (authorId != null) {
|
||||
"__LIBRARY__${libraryId}__AUTHOR_SERIES__${authorId}__${collapsedSeries!!.id}"
|
||||
} else {
|
||||
"__LIBRARY__${libraryId}__SERIES__${collapsedSeries!!.id}"
|
||||
}
|
||||
} else {
|
||||
id
|
||||
}
|
||||
var subtitle = authorName
|
||||
if (collapsedSeries != null) {
|
||||
subtitle = "${collapsedSeries!!.numBooks} books"
|
||||
}
|
||||
var itemTitle = title
|
||||
if (showSeriesNumber == true && seriesSequence != "") {
|
||||
itemTitle = "$seriesSequence. $itemTitle"
|
||||
}
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(itemTitle)
|
||||
.setIconUri(getCoverUri())
|
||||
.setSubtitle(subtitle)
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?, showSeriesNumber: Boolean?): MediaDescriptionCompat {
|
||||
return getMediaDescription(progress, ctx, authorId, showSeriesNumber, null)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?): MediaDescriptionCompat {
|
||||
return getMediaDescription(progress, ctx, authorId, null, null)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
|
||||
if (localLibraryItemId != null) {
|
||||
extras.putLong(
|
||||
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
|
||||
MediaDescriptionCompat.STATUS_DOWNLOADED
|
||||
)
|
||||
}
|
||||
|
||||
if (progress != null) {
|
||||
if (progress.isFinished) {
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
|
||||
)
|
||||
} else {
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
|
||||
)
|
||||
extras.putDouble(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
|
||||
)
|
||||
}
|
||||
} else if (mediaType != "podcast") {
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
|
||||
)
|
||||
}
|
||||
|
||||
if (media.metadata.explicit) {
|
||||
extras.putLong(MediaConstants.METADATA_KEY_IS_EXPLICIT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
|
||||
}
|
||||
|
||||
val mediaId = localLibraryItemId ?: id
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
.setIconUri(getCoverUri())
|
||||
.setSubtitle(authorName)
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
/*
|
||||
This is needed so Android auto library hierarchy for author series can be implemented
|
||||
*/
|
||||
return getMediaDescription(progress, ctx, null, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import androidx.media.utils.MediaConstants
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class LibrarySeriesItem(
|
||||
id:String,
|
||||
var libraryId:String,
|
||||
var name:String,
|
||||
var description:String?,
|
||||
var addedAt:Long,
|
||||
var updatedAt:Long,
|
||||
var books:MutableList<LibraryItem>?,
|
||||
var localLibraryItemId:String? // For Android Auto
|
||||
) : LibraryItemWrapper(id) {
|
||||
@get:JsonIgnore
|
||||
val title get() = name
|
||||
|
||||
@get:JsonIgnore
|
||||
val audiobookCount: Int
|
||||
get() {
|
||||
if (books == null) return 0
|
||||
val booksWithAudio = books?.filter { b -> b.media.checkHasTracks() }
|
||||
return booksWithAudio?.size ?: 0
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, groupTitle: String?): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
|
||||
if (localLibraryItemId != null) {
|
||||
extras.putLong(
|
||||
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
|
||||
MediaDescriptionCompat.STATUS_DOWNLOADED
|
||||
)
|
||||
}
|
||||
if (groupTitle !== null) {
|
||||
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
|
||||
}
|
||||
|
||||
val mediaId = "__LIBRARY__${libraryId}__SERIES__${id}"
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
//.setIconUri(getCoverUri())
|
||||
.setSubtitle("$audiobookCount books")
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
|
||||
return getMediaDescription(progress, ctx, null)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.audiobookshelf.app.player.PLAYMETHOD_LOCAL
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@@ -78,6 +79,27 @@ class LocalLibraryItem(
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun hasTracks(episode:PodcastEpisode?): Boolean {
|
||||
var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
|
||||
if (episode != null) { // Get podcast episode audio track
|
||||
episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks }
|
||||
}
|
||||
if (audioTracks.size == 0) return false
|
||||
audioTracks.forEach {
|
||||
// Check that metadata is not null
|
||||
if (it.metadata === null) {
|
||||
return false
|
||||
}
|
||||
// Check that file exists
|
||||
val file = File(it.metadata!!.path)
|
||||
if (!file.exists()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getPlaybackSession(episode:PodcastEpisode?, deviceInfo:DeviceInfo):PlaybackSession {
|
||||
val localEpisodeId = episode?.id
|
||||
|
||||
@@ -41,7 +41,7 @@ data class LocalMediaItem(
|
||||
@JsonIgnore
|
||||
fun getMediaMetadata():MediaTypeMetadata {
|
||||
return if (mediaType == "book") {
|
||||
BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null,null)
|
||||
BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null, null, null)
|
||||
} else {
|
||||
PodcastMetadata(name,null,null, mutableListOf(), false)
|
||||
}
|
||||
|
||||
@@ -53,6 +53,20 @@ object DeviceManager {
|
||||
if (deviceData.deviceSettings?.languageCode == null) {
|
||||
deviceData.deviceSettings?.languageCode = "en-us"
|
||||
}
|
||||
|
||||
if (deviceData.deviceSettings?.downloadUsingCellular == null) {
|
||||
deviceData.deviceSettings?.downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS
|
||||
}
|
||||
|
||||
if (deviceData.deviceSettings?.streamingUsingCellular == null) {
|
||||
deviceData.deviceSettings?.streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS
|
||||
}
|
||||
if (deviceData.deviceSettings?.androidAutoBrowseLimitForGrouping == null) {
|
||||
deviceData.deviceSettings?.androidAutoBrowseLimitForGrouping = 100
|
||||
}
|
||||
if (deviceData.deviceSettings?.androidAutoBrowseSeriesSequenceOrder == null) {
|
||||
deviceData.deviceSettings?.androidAutoBrowseSeriesSequenceOrder = AndroidAutoBrowseSeriesSequenceOrderSetting.ASC
|
||||
}
|
||||
}
|
||||
|
||||
fun getBase64Id(id:String):String {
|
||||
|
||||
@@ -70,6 +70,7 @@ class FolderScanner(var ctx: Context) {
|
||||
)
|
||||
localLibraryItem.localFiles.add(localFile)
|
||||
|
||||
val trackFileMetadata = FileMetadata(file.name, file.extension, file.absolutePath, file.getBasePath(ctx), file.length())
|
||||
// Create new audio track
|
||||
val track = AudioTrack(
|
||||
audioTrackFromServer.index,
|
||||
@@ -78,7 +79,7 @@ class FolderScanner(var ctx: Context) {
|
||||
localFile.filename ?: "",
|
||||
localFile.contentUrl,
|
||||
localFile.mimeType ?: "",
|
||||
null,
|
||||
trackFileMetadata,
|
||||
true,
|
||||
localFileId,
|
||||
null,
|
||||
@@ -274,7 +275,8 @@ class FolderScanner(var ctx: Context) {
|
||||
localLibraryItem.localFiles.add(localFile)
|
||||
|
||||
// Create new audio track
|
||||
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioTrackFromServer.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, null, audioTrackFromServer.index)
|
||||
val trackFileMetadata = FileMetadata(docFile.name ?: "", docFile.extension ?: "", docFile.getAbsolutePath(ctx), docFile.getBasePath(ctx), docFile.length())
|
||||
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioTrackFromServer.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", trackFileMetadata, true, localFileId, null, audioTrackFromServer.index)
|
||||
audioTracks.add(track)
|
||||
|
||||
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
|
||||
|
||||
@@ -20,13 +20,23 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
val tag = "MediaManager"
|
||||
|
||||
private var serverLibraryItems = mutableListOf<LibraryItem>() // Store all items here
|
||||
private var selectedLibraryItems = mutableListOf<LibraryItem>()
|
||||
private var selectedLibraryId = ""
|
||||
|
||||
private var cachedLibraryAuthors : MutableMap<String, MutableMap<String, LibraryAuthorItem>> = hashMapOf()
|
||||
private var cachedLibraryAuthorItems : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
|
||||
private var cachedLibraryAuthorSeriesItems : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
|
||||
private var cachedLibrarySeries : MutableMap<String, List<LibrarySeriesItem>> = hashMapOf()
|
||||
private var cachedLibrarySeriesItem : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
|
||||
private var cachedLibraryCollections : MutableMap<String, MutableMap<String, LibraryCollection>> = hashMapOf()
|
||||
private var cachedLibraryRecentShelves : MutableMap<String, MutableList<LibraryShelfType>> = hashMapOf()
|
||||
private var cachedLibraryDiscovery : MutableMap<String, MutableList<LibraryItem>> = hashMapOf()
|
||||
private var cachedLibraryPodcasts : MutableMap<String, MutableMap<String, LibraryItem>> = hashMapOf()
|
||||
private var isLibraryPodcastsCached : MutableMap<String, Boolean> = hashMapOf()
|
||||
var allLibraryPersonalizationsDone : Boolean = false
|
||||
private var libraryPersonalizationsDone : Int = 0
|
||||
|
||||
private var selectedPodcast:Podcast? = null
|
||||
private var selectedLibraryItemId:String? = null
|
||||
private var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>()
|
||||
private var serverLibraryCategories = listOf<LibraryCategory>()
|
||||
private var serverConfigIdUsed:String? = null
|
||||
private var serverConfigLastPing:Long = 0L
|
||||
var serverUserMediaProgress:MutableList<MediaProgress> = mutableListOf()
|
||||
@@ -39,6 +49,35 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
return serverLibraries.find { it.id == id } != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is discovery shelf for [libraryId]
|
||||
* If personalized shelves are not yet populated for library then populate
|
||||
*
|
||||
*/
|
||||
fun getHasDiscovery(libraryId: String) : Boolean {
|
||||
if (cachedLibraryDiscovery.containsKey(libraryId)) {
|
||||
if (cachedLibraryDiscovery[libraryId]!!.isNotEmpty()) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
populatePersonalizedDataForLibrary(libraryId){}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun getLibrary(id:String) : Library? {
|
||||
return serverLibraries.find { it.id == id }
|
||||
}
|
||||
|
||||
/**
|
||||
* Add [libraryItem] to [serverLibraryItems] if it is not already added
|
||||
*/
|
||||
private fun addServerLibrary(libraryItem: LibraryItem) {
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSavedPlaybackRate():Float {
|
||||
if (userSettingsPlaybackRate != null) {
|
||||
return userSettingsPlaybackRate ?: 1f
|
||||
@@ -91,19 +130,32 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun checkResetServerItems() {
|
||||
fun checkResetServerItems():Boolean {
|
||||
// When opening android auto need to check if still connected to server
|
||||
// and reset any server data already set
|
||||
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
|
||||
|
||||
if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
|
||||
Log.d(tag, "Reset caches")
|
||||
podcastEpisodeLibraryItemMap = mutableMapOf()
|
||||
serverLibraryCategories = listOf()
|
||||
serverLibraries = listOf()
|
||||
serverLibraryItems = mutableListOf()
|
||||
selectedLibraryItems = mutableListOf()
|
||||
selectedLibraryId = ""
|
||||
cachedLibraryAuthors = hashMapOf()
|
||||
cachedLibraryAuthorItems = hashMapOf()
|
||||
cachedLibraryAuthorSeriesItems = hashMapOf()
|
||||
cachedLibrarySeries = hashMapOf()
|
||||
cachedLibrarySeriesItem = hashMapOf()
|
||||
cachedLibraryCollections = hashMapOf()
|
||||
cachedLibraryRecentShelves = hashMapOf()
|
||||
cachedLibraryDiscovery = hashMapOf()
|
||||
cachedLibraryPodcasts = hashMapOf()
|
||||
isLibraryPodcastsCached = hashMapOf()
|
||||
serverItemsInProgress = listOf()
|
||||
allLibraryPersonalizationsDone = false
|
||||
libraryPersonalizationsDone = 0
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun loadItemsInProgressForAllLibraries(cb: (List<ItemInProgress>) -> Unit) {
|
||||
@@ -120,28 +172,458 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||
if (selectedLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) {
|
||||
cb(selectedLibraryItems)
|
||||
/**
|
||||
* Load personalized shelves from server for all libraries.
|
||||
* [cb] resolves when all libraries are processed
|
||||
*/
|
||||
fun populatePersonalizedDataForAllLibraries(cb: () -> Unit ) {
|
||||
serverLibraries.forEach {
|
||||
libraryPersonalizationsDone++
|
||||
Log.d(tag, "Loading personalization for library ${it.name} - ${it.id} - ${it.mediaType}")
|
||||
populatePersonalizedDataForLibrary(it.id) {
|
||||
Log.d(tag, "Loaded personalization for library ${it.name} - ${it.id} - ${it.mediaType}")
|
||||
libraryPersonalizationsDone--
|
||||
}
|
||||
}
|
||||
|
||||
while (libraryPersonalizationsDone > 0) { }
|
||||
|
||||
Log.d(tag, "Finished loading all library personalization data")
|
||||
allLibraryPersonalizationsDone = true
|
||||
cb()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get personalized shelves from server for selected [libraryId].
|
||||
* Populates [cachedLibraryRecentShelves] and [cachedLibraryDiscovery].
|
||||
*/
|
||||
private fun populatePersonalizedDataForLibrary(libraryId: String, cb: () -> Unit) {
|
||||
apiHandler.getLibraryPersonalized(libraryId) { shelves ->
|
||||
Log.d(tag, "populatePersonalizedDataForLibrary $libraryId")
|
||||
if (shelves === null) return@getLibraryPersonalized
|
||||
shelves.map { shelf ->
|
||||
Log.d(tag, "$shelf")
|
||||
if (shelf.type == "book") {
|
||||
if (shelf.id == "continue-listening") return@map
|
||||
else if (shelf.id == "listen-again") return@map
|
||||
else if (shelf.id == "recently-added") {
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
cachedLibraryRecentShelves[libraryId] = mutableListOf()
|
||||
}
|
||||
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
|
||||
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
|
||||
}
|
||||
}
|
||||
else if (shelf.id == "discover") {
|
||||
if (!cachedLibraryDiscovery.containsKey(libraryId)) {
|
||||
cachedLibraryDiscovery[libraryId] = mutableListOf()
|
||||
}
|
||||
(shelf as LibraryShelfBookEntity).entities?.map {
|
||||
cachedLibraryDiscovery[libraryId]!!.add(it)
|
||||
}
|
||||
}
|
||||
else if (shelf.id == "continue-reading") return@map
|
||||
else if (shelf.id == "continue-series") return@map
|
||||
shelf as LibraryShelfBookEntity
|
||||
} else if (shelf.type == "series") {
|
||||
if (shelf.id == "recent-series") {
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
cachedLibraryRecentShelves[libraryId] = mutableListOf()
|
||||
}
|
||||
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
|
||||
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
|
||||
}
|
||||
}
|
||||
} else if (shelf.type == "episode") {
|
||||
if (shelf.id == "continue-listening") return@map
|
||||
else if (shelf.id == "listen-again") return@map
|
||||
else if (shelf.id == "newest-episodes") {
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
cachedLibraryRecentShelves[libraryId] = mutableListOf()
|
||||
}
|
||||
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
|
||||
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
|
||||
}
|
||||
|
||||
val podcastLibraryItemIds = mutableListOf<String>()
|
||||
(shelf as LibraryShelfEpisodeEntity).entities?.forEach { libraryItem ->
|
||||
if (!podcastLibraryItemIds.contains(libraryItem.id)) {
|
||||
podcastLibraryItemIds.add(libraryItem.id)
|
||||
loadPodcastItem(libraryItem.libraryId, libraryItem.id) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (shelf.type == "podcast") {
|
||||
if (shelf.id == "recently-added"){
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
cachedLibraryRecentShelves[libraryId] = mutableListOf()
|
||||
}
|
||||
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
|
||||
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
|
||||
}
|
||||
}
|
||||
else if (shelf.id == "discover"){
|
||||
return@map
|
||||
}
|
||||
} else if (shelf.type =="authors") {
|
||||
if (shelf.id == "newest-authors") {
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
cachedLibraryRecentShelves[libraryId] = mutableListOf()
|
||||
}
|
||||
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
|
||||
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Log.d(tag, "populatePersonalizedDataForLibrary $libraryId DONE")
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns podcasts for selected library.
|
||||
* If data is not found from local cache it is loaded from server
|
||||
*/
|
||||
fun loadLibraryPodcasts(libraryId:String, cb: (List<LibraryItem>?) -> Unit) {
|
||||
// Without this there is possibility that only recent podcasts get loaded
|
||||
// Loading recent podcasts will also create cachedLibraryPodcasts entry for library
|
||||
if (!isLibraryPodcastsCached.containsKey(libraryId)) {
|
||||
isLibraryPodcastsCached[libraryId] = false
|
||||
}
|
||||
// Ensure that there is map for library
|
||||
if (!cachedLibraryPodcasts.containsKey(libraryId)) {
|
||||
cachedLibraryPodcasts[libraryId] = mutableMapOf()
|
||||
}
|
||||
if (isLibraryPodcastsCached.getOrElse(libraryId) {false}) {
|
||||
Log.d(tag, "loadLibraryPodcasts: Found from cache: $libraryId")
|
||||
cb(cachedLibraryPodcasts[libraryId]?.values?.sortedBy { libraryItem -> (libraryItem.media as Podcast).metadata.title })
|
||||
} else {
|
||||
apiHandler.getLibraryItems(libraryId) { libraryItems ->
|
||||
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||
if (libraryItemsWithAudio.isNotEmpty()) {
|
||||
selectedLibraryId = libraryId
|
||||
}
|
||||
|
||||
selectedLibraryItems = mutableListOf()
|
||||
libraryItemsWithAudio.forEach { libraryItem ->
|
||||
selectedLibraryItems.add(libraryItem)
|
||||
cachedLibraryPodcasts[libraryId]?.set(libraryItem.id, libraryItem)
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
isLibraryPodcastsCached[libraryId] = true
|
||||
Log.d(tag, "loadLibraryPodcasts: loaded from server: $libraryId")
|
||||
cb(libraryItemsWithAudio.sortedBy { libraryItem -> (libraryItem.media as Podcast).metadata.title })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns series with audio books from selected library.
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadLibrarySeriesWithAudio(libraryId:String, cb: (List<LibrarySeriesItem>) -> Unit) {
|
||||
// Check "cache" first
|
||||
if (cachedLibrarySeries.containsKey(libraryId)) {
|
||||
Log.d(tag, "Series with audio found from cache | Library $libraryId ")
|
||||
cb(cachedLibrarySeries[libraryId] as List<LibrarySeriesItem>)
|
||||
} else {
|
||||
apiHandler.getLibrarySeries(libraryId) { seriesItems ->
|
||||
Log.d(tag, "Series with audio loaded from server | Library $libraryId")
|
||||
val seriesItemsWithAudio = seriesItems.filter { si -> si.audiobookCount > 0 }
|
||||
|
||||
cachedLibrarySeries[libraryId] = seriesItemsWithAudio
|
||||
|
||||
cb(seriesItemsWithAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns series with audiobooks from selected library using filter for paging.
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadLibrarySeriesWithAudio(libraryId:String, seriesFilter:String, cb: (List<LibrarySeriesItem>) -> Unit) {
|
||||
// Check "cache" first
|
||||
if (!cachedLibrarySeries.containsKey(libraryId)) {
|
||||
loadLibrarySeriesWithAudio(libraryId) {}
|
||||
} else {
|
||||
Log.d(tag, "Series with audio found from cache | Library $libraryId ")
|
||||
}
|
||||
val seriesWithBooks = cachedLibrarySeries[libraryId]!!.filter { ls -> ls.title.uppercase().startsWith(seriesFilter) }.toList()
|
||||
cb(seriesWithBooks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts books in series. Assumes that sequence is main.minor
|
||||
*/
|
||||
private fun sortSeriesBooks(seriesBooks: List<LibraryItem>) : List<LibraryItem> {
|
||||
val sortingLogic = compareBy<LibraryItem> { it.seriesSequenceParts[0].length }
|
||||
.thenBy { it.seriesSequenceParts[0].ifEmpty { "" } }
|
||||
.thenBy { it.seriesSequenceParts.getOrElse(1) { "" }.length }
|
||||
.thenBy { it.seriesSequenceParts.getOrElse(1) { "" } }
|
||||
return seriesBooks.sortedWith(sortingLogic)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns books for series from library.
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadLibrarySeriesItemsWithAudio(libraryId:String, seriesId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||
// Check "cache" first
|
||||
if (!cachedLibrarySeriesItem.containsKey(libraryId)) {
|
||||
cachedLibrarySeriesItem[libraryId] = hashMapOf()
|
||||
}
|
||||
if (cachedLibrarySeriesItem[libraryId]!!.containsKey(seriesId)) {
|
||||
Log.d(tag, "Items for series $seriesId found from cache | Library $libraryId")
|
||||
cachedLibrarySeriesItem[libraryId]!![seriesId]?.let { cb(it) }
|
||||
} else {
|
||||
apiHandler.getLibrarySeriesItems(libraryId, seriesId) { libraryItems ->
|
||||
Log.d(tag, "Items for series $seriesId loaded from server | Library $libraryId")
|
||||
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||
|
||||
val sortedLibraryItemsWithAudio = sortSeriesBooks(libraryItemsWithAudio)
|
||||
cachedLibrarySeriesItem[libraryId]!![seriesId] = sortedLibraryItemsWithAudio
|
||||
|
||||
sortedLibraryItemsWithAudio.forEach { libraryItem ->
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
cb(sortedLibraryItemsWithAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns authors with books from library.
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadAuthorsWithBooks(libraryId:String, cb: (List<LibraryAuthorItem>) -> Unit) {
|
||||
// Check "cache" first
|
||||
if (cachedLibraryAuthors.containsKey(libraryId)) {
|
||||
Log.d(tag, "Authors with books found from cache | Library $libraryId ")
|
||||
cb(cachedLibraryAuthors[libraryId]!!.values.toList())
|
||||
} else {
|
||||
// Fetch data from server and add it to local "cache"
|
||||
apiHandler.getLibraryAuthors(libraryId) { authorItems ->
|
||||
Log.d(tag, "Authors with books loaded from server | Library $libraryId ")
|
||||
// TO-DO: This check won't ensure that there is audiobooks. Current API won't offer ability to do so
|
||||
var authorItemsWithBooks = authorItems.filter { li -> li.bookCount != null && li.bookCount!! > 0 }
|
||||
authorItemsWithBooks = authorItemsWithBooks.sortedBy { it.name }
|
||||
// Ensure that there is map for library
|
||||
cachedLibraryAuthors[libraryId] = mutableMapOf()
|
||||
// Cache authors
|
||||
authorItemsWithBooks.forEach {
|
||||
if (!cachedLibraryAuthors[libraryId]!!.containsKey(it.id)) {
|
||||
cachedLibraryAuthors[libraryId]!![it.id] = it
|
||||
}
|
||||
}
|
||||
cb(authorItemsWithBooks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns authors with books from selected library using filter for paging.
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadAuthorsWithBooks(libraryId:String, authorFilter: String, cb: (List<LibraryAuthorItem>) -> Unit) {
|
||||
// Check "cache" first
|
||||
if (cachedLibraryAuthors.containsKey(libraryId)) {
|
||||
Log.d(tag, "Authors with books found from cache | Library $libraryId ")
|
||||
} else {
|
||||
loadAuthorsWithBooks(libraryId) {}
|
||||
}
|
||||
val authorsWithBooks = cachedLibraryAuthors[libraryId]!!.values.filter { lai -> lai.name.uppercase().startsWith(authorFilter) }.toList()
|
||||
cb(authorsWithBooks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns audiobooks for author from library
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadAuthorBooksWithAudio(libraryId:String, authorId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||
// Ensure that there is map for library
|
||||
if (!cachedLibraryAuthorItems.containsKey(libraryId)) {
|
||||
cachedLibraryAuthorItems[libraryId] = mutableMapOf()
|
||||
}
|
||||
// Check "cache" first
|
||||
if (cachedLibraryAuthorItems[libraryId]!!.containsKey(authorId)) {
|
||||
Log.d(tag, "Items for author $authorId found from cache | Library $libraryId")
|
||||
cachedLibraryAuthorItems[libraryId]!![authorId]?.let { cb(it) }
|
||||
} else {
|
||||
apiHandler.getLibraryItemsFromAuthor(libraryId, authorId) { libraryItems ->
|
||||
Log.d(tag, "Items for author $authorId loaded from server | Library $libraryId")
|
||||
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||
|
||||
cachedLibraryAuthorItems[libraryId]!![authorId] = libraryItemsWithAudio
|
||||
|
||||
libraryItemsWithAudio.forEach { libraryItem ->
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
cb(libraryItemsWithAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns audiobooks for author from specified series within library
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadAuthorSeriesBooksWithAudio(libraryId:String, authorId:String, seriesId: String, cb: (List<LibraryItem>) -> Unit) {
|
||||
val authorSeriesKey = "$authorId|$seriesId"
|
||||
// Ensure that there is map for library
|
||||
if (!cachedLibraryAuthorSeriesItems.containsKey(libraryId)) {
|
||||
cachedLibraryAuthorSeriesItems[libraryId] = mutableMapOf()
|
||||
}
|
||||
// Check "cache" first
|
||||
if (cachedLibraryAuthorSeriesItems[libraryId]!!.containsKey(authorSeriesKey)) {
|
||||
Log.d(tag, "Items for series $seriesId with author $authorId found from cache | Library $libraryId")
|
||||
cachedLibraryAuthorSeriesItems[libraryId]!![authorSeriesKey]?.let { cb(it) }
|
||||
} else {
|
||||
apiHandler.getLibrarySeriesItems(libraryId, seriesId) { libraryItems ->
|
||||
Log.d(tag, "Items for series $seriesId with author $authorId loaded from server | Library $libraryId")
|
||||
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||
if (!cachedLibraryAuthors[libraryId]!!.containsKey(authorId)) {
|
||||
Log.d(tag, "Author data is missing")
|
||||
}
|
||||
val authorName = cachedLibraryAuthors[libraryId]!![authorId]?.name ?: ""
|
||||
Log.d(tag, "Using author name: $authorName")
|
||||
val libraryItemsFromAuthorWithAudio = libraryItemsWithAudio.filter { li -> li.authorName.indexOf(authorName, ignoreCase = true) >= 0 }
|
||||
|
||||
val sortedLibraryItemsWithAudio = sortSeriesBooks(libraryItemsFromAuthorWithAudio)
|
||||
cachedLibraryAuthorSeriesItems[libraryId]!![authorId] = sortedLibraryItemsWithAudio
|
||||
|
||||
sortedLibraryItemsWithAudio.forEach { libraryItem ->
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
cb(sortedLibraryItemsWithAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns collections with audiobooks from library
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadLibraryCollectionsWithAudio(libraryId:String, cb: (List<LibraryCollection>) -> Unit) {
|
||||
if (cachedLibraryCollections.containsKey(libraryId)) {
|
||||
Log.d(tag, "Collections with books found from cache | Library $libraryId ")
|
||||
cb(cachedLibraryCollections[libraryId]!!.values.toList())
|
||||
} else {
|
||||
apiHandler.getLibraryCollections(libraryId) { libraryCollections ->
|
||||
Log.d(tag, "Collections with books loaded from server | Library $libraryId ")
|
||||
val libraryCollectionsWithAudio = libraryCollections.filter { lc -> lc.audiobookCount > 0 }
|
||||
|
||||
// Cache collections
|
||||
cachedLibraryCollections[libraryId] = hashMapOf()
|
||||
libraryCollectionsWithAudio.forEach {
|
||||
if (!cachedLibraryCollections[libraryId]!!.containsKey(it.id)) {
|
||||
cachedLibraryCollections[libraryId]!![it.id] = it
|
||||
}
|
||||
}
|
||||
cb(libraryCollectionsWithAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns audiobooks for collection from library
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadLibraryCollectionBooksWithAudio(libraryId: String, collectionId: String, cb: (List<LibraryItem>) -> Unit) {
|
||||
if (!cachedLibraryCollections.containsKey(libraryId)) {
|
||||
loadLibraryCollectionsWithAudio(libraryId) {}
|
||||
}
|
||||
Log.d(tag, "Trying to find collection $collectionId items from from cache | Library $libraryId ")
|
||||
if ( cachedLibraryCollections[libraryId]!!.containsKey(collectionId)) {
|
||||
val libraryCollectionBookswithAudio = cachedLibraryCollections[libraryId]!![collectionId]?.books
|
||||
libraryCollectionBookswithAudio?.forEach { libraryItem ->
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
cb(libraryCollectionBookswithAudio as List<LibraryItem>)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns audiobooks from discovery shelf for [libraryId]
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadLibraryDiscoveryBooksWithAudio(libraryId: String, cb: (List<LibraryItem>) -> Unit) {
|
||||
if (!cachedLibraryDiscovery.containsKey(libraryId)) {
|
||||
cb(listOf())
|
||||
}
|
||||
val libraryItemsWithAudio = cachedLibraryDiscovery[libraryId]?.filter { li -> li.checkHasTracks() }
|
||||
libraryItemsWithAudio?.forEach { libraryItem -> addServerLibrary(libraryItem) }
|
||||
cb(libraryItemsWithAudio as List<LibraryItem>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns recent shelves for [libraryId]
|
||||
* If data is not shelves are found returns empty list
|
||||
*/
|
||||
fun getLibraryRecentShelfs(libraryId: String, cb: (List<LibraryShelfType>) -> Unit) {
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
Log.d(tag, "getLibraryRecentShelfs: No shelves $libraryId")
|
||||
cb(listOf())
|
||||
return
|
||||
}
|
||||
cb(cachedLibraryRecentShelves[libraryId] as List<LibraryShelfType>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns recent shelf by [type] for [libraryId]
|
||||
* If shelf is not found returns null
|
||||
*/
|
||||
fun getLibraryRecentShelfByType(libraryId: String, type:String, cb: (LibraryShelfType?) -> Unit) {
|
||||
Log.d(tag, "getLibraryRecentShelfByType: $libraryId | $type")
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
cb(null)
|
||||
return
|
||||
}
|
||||
for (shelf in cachedLibraryRecentShelves[libraryId]!!) {
|
||||
if (shelf.type == type.lowercase()) {
|
||||
cb(shelf)
|
||||
return
|
||||
}
|
||||
}
|
||||
cb(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads podcasts for newest episodes shelf
|
||||
*/
|
||||
private fun loadPodcastItem(libraryId: String, libraryItemId: String, cb: (LibraryItem?) -> Unit) {
|
||||
// Ensure that there is map for library
|
||||
if (!cachedLibraryPodcasts.containsKey(libraryId)) {
|
||||
cachedLibraryPodcasts[libraryId] = mutableMapOf()
|
||||
}
|
||||
if (cachedLibraryPodcasts[libraryId]!!.containsKey(libraryItemId)) {
|
||||
Log.d(tag, "loadPodcastItem: Podcast found from cache | Library $libraryItemId ")
|
||||
cb(cachedLibraryPodcasts[libraryId]?.get(libraryItemId))
|
||||
} else {
|
||||
Log.d(tag, "loadPodcastItem: Calling getLibraryItem $libraryItemId")
|
||||
apiHandler.getLibraryItem(libraryItemId) { libraryItem ->
|
||||
if (libraryItem !== null) {
|
||||
Log.d(tag, "loadPodcastItem: Got library item ${libraryItem.id} ${libraryItem.media.metadata.title}")
|
||||
val podcast = libraryItem.media as Podcast
|
||||
podcast.episodes?.forEach { podcastEpisode ->
|
||||
podcastEpisodeLibraryItemMap[podcastEpisode.id] = LibraryItemWithEpisode(libraryItem, podcastEpisode)
|
||||
}
|
||||
cachedLibraryPodcasts[libraryId]?.set(libraryItemId, libraryItem)
|
||||
cb(libraryItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) {
|
||||
if (libraryItemId.startsWith("local")) {
|
||||
cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId))
|
||||
@@ -187,8 +669,8 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
}
|
||||
selectedLibraryItemId = libraryItemWrapper.id
|
||||
selectedPodcast = podcast
|
||||
|
||||
val children = podcast.episodes?.map { podcastEpisode ->
|
||||
val episodes = podcast.episodes?.sortedByDescending { it.publishedAt }
|
||||
val children = episodes?.map { podcastEpisode ->
|
||||
|
||||
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id }
|
||||
|
||||
@@ -209,13 +691,16 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads libraries for selected server with stats
|
||||
*/
|
||||
private fun loadLibraries(cb: (List<Library>) -> Unit) {
|
||||
if (serverLibraries.isNotEmpty()) {
|
||||
cb(serverLibraries)
|
||||
} else {
|
||||
apiHandler.getLibraries {
|
||||
serverLibraries = it
|
||||
cb(it)
|
||||
apiHandler.getLibraries { loadedLibraries ->
|
||||
serverLibraries = loadedLibraries
|
||||
cb(serverLibraries)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,6 +814,25 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun initializeInProgressItems(cb: () -> Unit) {
|
||||
Log.d(tag, "Initializing inprogress items")
|
||||
|
||||
loadItemsInProgressForAllLibraries { itemsInProgress ->
|
||||
itemsInProgress.forEach {
|
||||
val libraryItem = it.libraryItemWrapper as LibraryItem
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
|
||||
if (it.episode != null) {
|
||||
podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
|
||||
}
|
||||
}
|
||||
Log.d(tag, "Initializing inprogress items done")
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAndroidAutoItems(cb: () -> Unit) {
|
||||
Log.d(tag, "Load android auto items")
|
||||
|
||||
@@ -343,23 +847,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
Log.w(tag, "No libraries returned from server request")
|
||||
cb()
|
||||
} else {
|
||||
val library = libraries[0]
|
||||
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
|
||||
|
||||
loadItemsInProgressForAllLibraries { itemsInProgress ->
|
||||
itemsInProgress.forEach {
|
||||
val libraryItem = it.libraryItemWrapper as LibraryItem
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
|
||||
if (it.episode != null) {
|
||||
podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
|
||||
}
|
||||
}
|
||||
|
||||
cb() // Fully loaded
|
||||
}
|
||||
cb() // Fully loaded
|
||||
}
|
||||
}
|
||||
} else { // Not connected to server
|
||||
@@ -369,6 +857,65 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles search requests.
|
||||
* Searches from books, series and authors
|
||||
*/
|
||||
suspend fun doSearch(libraryId: String, queryString: String) : Map<String, List<MediaBrowserCompat.MediaItem>> {
|
||||
return suspendCoroutine {
|
||||
apiHandler.getSearchResults(libraryId, queryString) { searchResult ->
|
||||
Log.d(tag, "searchLocalCache: $searchResult")
|
||||
// Nothing found from server
|
||||
if (searchResult === null) {
|
||||
it.resume(mapOf())
|
||||
return@getSearchResults
|
||||
}
|
||||
|
||||
val foundItems: MutableMap<String, List<MediaBrowserCompat.MediaItem>> = mutableMapOf()
|
||||
|
||||
val serverLibrary = serverLibraries.find { sl -> sl.id == libraryId }
|
||||
|
||||
// Books
|
||||
if (searchResult.book !== null && searchResult.book!!.isNotEmpty()) {
|
||||
Log.d(tag, "searchLocalCache: found ${searchResult.book!!.size} books")
|
||||
val children = searchResult.book!!.filter { it.libraryItem.checkHasTracks() }.map { bookResult ->
|
||||
val libraryItem = bookResult.libraryItem
|
||||
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
val description = libraryItem.getMediaDescription(progress, ctx, null, null, "Books (${serverLibrary?.name})")
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
foundItems["book"] = children
|
||||
}
|
||||
if (searchResult.series !== null && searchResult.series!!.isNotEmpty()) {
|
||||
Log.d(tag, "onSearch: found ${searchResult.series!!.size} series")
|
||||
val children = searchResult.series!!.map { seriesResult ->
|
||||
val seriesItem = seriesResult.series
|
||||
seriesItem.books = seriesResult.books as MutableList<LibraryItem>
|
||||
val description = seriesItem.getMediaDescription(null, ctx, "Series (${serverLibrary?.name})")
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
foundItems["series"] = children
|
||||
}
|
||||
if (searchResult.authors !== null && searchResult.authors!!.isNotEmpty()) {
|
||||
Log.d(tag, "onSearch: found ${searchResult.authors!!.size} authors")
|
||||
val children = searchResult.authors!!.map { authorItem ->
|
||||
val description = authorItem.getMediaDescription(null, ctx, "Authors (${serverLibrary?.name})")
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
foundItems["authors"] = children
|
||||
}
|
||||
|
||||
it.resume(foundItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getFirstItem() : LibraryItemWrapper? {
|
||||
if (serverLibraryItems.isNotEmpty()) {
|
||||
return serverLibraryItems[0]
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.audiobookshelf.app.media
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.annotation.AnyRes
|
||||
import com.audiobookshelf.app.R
|
||||
|
||||
/**
|
||||
* get uri to drawable or any other resource type if u wish
|
||||
* @param drawableId - drawable res id
|
||||
* @return - uri
|
||||
*/
|
||||
fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri {
|
||||
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
|
||||
+ "://" + context.resources.getResourcePackageName(drawableId)
|
||||
+ '/' + context.resources.getResourceTypeName(drawableId)
|
||||
+ '/' + context.resources.getResourceEntryName(drawableId))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* get uri to drawable or any other resource type if u wish
|
||||
* @param drawableId - drawable res id
|
||||
* @return - uri
|
||||
*/
|
||||
fun getUriToAbsIconDrawable(context: Context, absIconName: String): Uri {
|
||||
val drawableId = when(absIconName) {
|
||||
"audiobookshelf" -> R.drawable.abs_audiobookshelf
|
||||
"authors" -> R.drawable.abs_authors
|
||||
"book-1" -> R.drawable.abs_book_1
|
||||
"books-1" -> R.drawable.abs_books_1
|
||||
"books-2" -> R.drawable.abs_books_2
|
||||
"columns" -> R.drawable.abs_columns
|
||||
"database" -> R.drawable.abs_database
|
||||
"file-picture" -> R.drawable.abs_file_picture
|
||||
"headphones" -> R.drawable.abs_headphones
|
||||
"heart" -> R.drawable.abs_heart
|
||||
"microphone_1" -> R.drawable.abs_microphone_1
|
||||
"microphone_2" -> R.drawable.abs_microphone_2
|
||||
"microphone_3" -> R.drawable.abs_microphone_3
|
||||
"music" -> R.drawable.abs_music
|
||||
"podcast" -> R.drawable.abs_podcast
|
||||
"radio" -> R.drawable.abs_radio
|
||||
"rocket" -> R.drawable.abs_rocket
|
||||
"rss" -> R.drawable.abs_rss
|
||||
"star" -> R.drawable.abs_star
|
||||
else -> R.drawable.icon_library_folder
|
||||
}
|
||||
return Uri.parse(
|
||||
ContentResolver.SCHEME_ANDROID_RESOURCE
|
||||
+ "://" + context.resources.getResourcePackageName(drawableId)
|
||||
+ '/' + context.resources.getResourceTypeName(drawableId)
|
||||
+ '/' + context.resources.getResourceEntryName(drawableId))
|
||||
}
|
||||
@@ -41,8 +41,11 @@ data class DownloadItemPart(
|
||||
val destinationUri = Uri.fromFile(destinationFile)
|
||||
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
|
||||
|
||||
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
|
||||
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg&raw=1" // For cover images force to jpeg
|
||||
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}"
|
||||
|
||||
downloadUrl += if (serverPath.endsWith("/cover")) "?raw=1" // Download raw cover image
|
||||
else "?token=${DeviceManager.token}"
|
||||
|
||||
val downloadUri = Uri.parse(downloadUrl)
|
||||
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
|
||||
return DownloadItemPart(
|
||||
@@ -77,7 +80,7 @@ data class DownloadItemPart(
|
||||
val isInternalStorage get() = localFolderId.startsWith("internal-")
|
||||
|
||||
@get:JsonIgnore
|
||||
val serverUrl get() = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
|
||||
val serverUrl get() = uri.toString()
|
||||
|
||||
@JsonIgnore
|
||||
fun getDownloadRequest(): DownloadManager.Request {
|
||||
|
||||
@@ -15,8 +15,6 @@ import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
|
||||
|
||||
class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, private val playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
|
||||
private val tag = "MediaDescriptionAdapter"
|
||||
|
||||
@@ -77,7 +75,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
|
||||
.load(uri)
|
||||
.placeholder(R.drawable.icon)
|
||||
.error(R.drawable.icon)
|
||||
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
|
||||
.submit()
|
||||
.get()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -85,7 +83,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
|
||||
Glide.with(playerNotificationService)
|
||||
.asBitmap()
|
||||
.load(Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon))
|
||||
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
|
||||
.submit()
|
||||
.get()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,45 @@
|
||||
package com.audiobookshelf.app.player
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import androidx.annotation.AnyRes
|
||||
import android.util.Log
|
||||
import com.audiobookshelf.app.R
|
||||
import com.audiobookshelf.app.data.*
|
||||
import com.audiobookshelf.app.media.getUriToDrawable
|
||||
|
||||
class BrowseTree(
|
||||
val context: Context,
|
||||
itemsInProgress: List<ItemInProgress>,
|
||||
libraries: List<Library>
|
||||
libraries: List<Library>,
|
||||
recentsLoaded: Boolean
|
||||
) {
|
||||
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
||||
|
||||
/**
|
||||
* get uri to drawable or any other resource type if u wish
|
||||
* @param drawableId - drawable res id
|
||||
* @return - uri
|
||||
*/
|
||||
private fun getUriToDrawable(@AnyRes drawableId: Int): Uri {
|
||||
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
|
||||
+ "://" + context.resources.getResourcePackageName(drawableId)
|
||||
+ '/' + context.resources.getResourceTypeName(drawableId)
|
||||
+ '/' + context.resources.getResourceEntryName(drawableId))
|
||||
}
|
||||
|
||||
init {
|
||||
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()
|
||||
|
||||
val continueListeningMetadata = MediaMetadataCompat.Builder().apply {
|
||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Listening")
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_localaudio).toString())
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Continue")
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
|
||||
}.build()
|
||||
|
||||
val recentMetadata = MediaMetadataCompat.Builder().apply {
|
||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, RECENTLY_ROOT)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Recent")
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.md_clock_outline).toString())
|
||||
}.build()
|
||||
|
||||
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
|
||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_downloaddone).toString())
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
|
||||
}.build()
|
||||
|
||||
val librariesMetadata = MediaMetadataCompat.Builder().apply {
|
||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries")
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.icon_library_folder).toString())
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString())
|
||||
}.build()
|
||||
|
||||
if (itemsInProgress.isNotEmpty()) {
|
||||
@@ -53,13 +47,28 @@ class BrowseTree(
|
||||
}
|
||||
|
||||
if (libraries.isNotEmpty()) {
|
||||
if (recentsLoaded) {
|
||||
rootList += recentMetadata
|
||||
}
|
||||
rootList += librariesMetadata
|
||||
|
||||
libraries.forEach { library ->
|
||||
val libraryMediaMetadata = library.getMediaMetadata()
|
||||
// Skip libraries without audio content
|
||||
if (library.stats?.numAudioFiles == 0) return@forEach
|
||||
Log.d("BrowseTree", "Library $library | ${library.icon}")
|
||||
// Generate library list items for Libraries menu
|
||||
val libraryMediaMetadata = library.getMediaMetadata(context)
|
||||
val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf()
|
||||
children += libraryMediaMetadata
|
||||
mediaIdToChildren[LIBRARIES_ROOT] = children
|
||||
|
||||
if (recentsLoaded) {
|
||||
// Generate library list items for Recent menu
|
||||
val recentlyMediaMetadata = library.getMediaMetadata(context,"recently")
|
||||
val childrenRecently = mediaIdToChildren[RECENTLY_ROOT] ?: mutableListOf()
|
||||
childrenRecently += recentlyMediaMetadata
|
||||
mediaIdToChildren[RECENTLY_ROOT] = childrenRecently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,3 +84,4 @@ const val AUTO_BROWSE_ROOT = "/"
|
||||
const val CONTINUE_ROOT = "__CONTINUE__"
|
||||
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
||||
const val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||
const val RECENTLY_ROOT = "__RECENTLY__"
|
||||
|
||||
@@ -34,6 +34,9 @@ import com.audiobookshelf.app.managers.SleepTimerManager
|
||||
import com.audiobookshelf.app.media.MediaManager
|
||||
import com.audiobookshelf.app.media.MediaProgressSyncer
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import com.audiobookshelf.app.BuildConfig
|
||||
import com.audiobookshelf.app.media.getUriToAbsIconDrawable
|
||||
import com.audiobookshelf.app.media.getUriToDrawable
|
||||
import com.google.android.exoplayer2.*
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
@@ -46,6 +49,7 @@ import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||
import com.google.android.exoplayer2.upstream.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
@@ -118,6 +122,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
private var mShakeDetector: ShakeDetector? = null
|
||||
private var shakeSensorUnregisterTask:TimerTask? = null
|
||||
|
||||
// These are used to trigger reloading if
|
||||
private var forceReloadingAndroidAuto:Boolean = false
|
||||
private var firstLoadDone:Boolean = false
|
||||
|
||||
fun isBrowseTreeInitialized() : Boolean {
|
||||
return this::browseTree.isInitialized
|
||||
}
|
||||
|
||||
// Cache latest search so it wont trigger again when returning from series for example
|
||||
private var cachedSearch : String = ""
|
||||
private var cachedSearchResults : MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
|
||||
/*
|
||||
Service related stuff
|
||||
*/
|
||||
@@ -319,11 +335,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
|
||||
.setExtras(extra)
|
||||
.setTitle(currentPlaybackSession!!.displayTitle)
|
||||
.setIconUri(coverUri)
|
||||
|
||||
bitmap?.let {
|
||||
mediaDescriptionBuilder.setIconBitmap(it)
|
||||
}
|
||||
} ?: mediaDescriptionBuilder.setIconUri(coverUri)
|
||||
|
||||
return mediaDescriptionBuilder.build()
|
||||
}
|
||||
@@ -887,16 +902,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
|
||||
fun closePlayback(calledOnError:Boolean? = false) {
|
||||
Log.d(tag, "closePlayback")
|
||||
val config = DeviceManager.serverConnectionConfig
|
||||
|
||||
val isLocal = mediaProgressSyncer.currentIsLocal
|
||||
val currentSessionId = mediaProgressSyncer.currentSessionId
|
||||
if (mediaProgressSyncer.listeningTimerRunning) {
|
||||
Log.i(tag, "About to close playback so stopping media progress syncer first")
|
||||
|
||||
mediaProgressSyncer.stop(calledOnError == false) { // If closing on error then do not sync progress (causes exception)
|
||||
Log.d(tag, "Media Progress syncer stopped")
|
||||
|
||||
// If not local session then close on server
|
||||
if (!isLocal && currentSessionId != "") {
|
||||
apiHandler.closePlaybackSession(currentSessionId) {
|
||||
apiHandler.closePlaybackSession(currentSessionId, config) {
|
||||
Log.d(tag, "Closed playback session $currentSessionId")
|
||||
}
|
||||
}
|
||||
@@ -904,7 +921,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
} else {
|
||||
// If not local session then close on server
|
||||
if (!isLocal && currentSessionId != "") {
|
||||
apiHandler.closePlaybackSession(currentSessionId) {
|
||||
apiHandler.closePlaybackSession(currentSessionId, config) {
|
||||
Log.d(tag, "Closed playback session $currentSessionId")
|
||||
}
|
||||
}
|
||||
@@ -947,7 +964,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
appVersion: 0.9.46-beta
|
||||
*/
|
||||
val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
|
||||
return DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, Build.VERSION.RELEASE)
|
||||
return DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
||||
}
|
||||
|
||||
private val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
|
||||
@@ -975,6 +992,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
|
||||
private val AUTO_MEDIA_ROOT = "/"
|
||||
private val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||
private val RECENTLY_ROOT = "__RECENTLY__"
|
||||
private val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
||||
private val CONTINUE_ROOT = "__CONTINUE__"
|
||||
private lateinit var browseTree:BrowseTree
|
||||
@@ -997,8 +1015,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
// No further calls will be made to other media browsing methods.
|
||||
null
|
||||
} else {
|
||||
Log.d(tag, "Android Auto starting")
|
||||
isStarted = true
|
||||
mediaManager.checkResetServerItems() // Reset any server items if no longer connected to server
|
||||
|
||||
// Reset cache if no longer connected to server or server changed
|
||||
if (mediaManager.checkResetServerItems()) {
|
||||
forceReloadingAndroidAuto = true
|
||||
}
|
||||
|
||||
isAndroidAuto = true
|
||||
|
||||
@@ -1024,33 +1047,45 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
|
||||
result.detach()
|
||||
|
||||
// Prevent crashing if app is restarted while browsing
|
||||
if ((parentMediaId != DOWNLOADS_ROOT && parentMediaId != AUTO_MEDIA_ROOT) && !firstLoadDone){
|
||||
result.sendResult(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (parentMediaId == DOWNLOADS_ROOT) { // Load downloads
|
||||
|
||||
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
||||
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
|
||||
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
val localBrowseItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
|
||||
localBooks.forEach { localLibraryItem ->
|
||||
if (localLibraryItem.media.getAudioTracks().isNotEmpty()) {
|
||||
val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id)
|
||||
val description = localLibraryItem.getMediaDescription(progress, ctx)
|
||||
|
||||
localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
localBrowseItems += MediaBrowserCompat.MediaItem(
|
||||
description,
|
||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
localPodcasts.forEach { localLibraryItem ->
|
||||
val mediaDescription = localLibraryItem.getMediaDescription(null, ctx)
|
||||
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
localBrowseItems += MediaBrowserCompat.MediaItem(
|
||||
mediaDescription,
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
|
||||
result.sendResult(localBrowseItems)
|
||||
|
||||
} else if (parentMediaId == CONTINUE_ROOT) {
|
||||
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
val localBrowseItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
mediaManager.serverItemsInProgress.forEach { itemInProgress ->
|
||||
val progress: MediaProgressWrapper?
|
||||
val mediaDescription:MediaDescriptionCompat
|
||||
val mediaDescription: MediaDescriptionCompat
|
||||
if (itemInProgress.episode != null) {
|
||||
if (itemInProgress.isLocal) {
|
||||
progress = DeviceManager.dbManager.getLocalMediaProgress("${itemInProgress.libraryItemWrapper.id}-${itemInProgress.episode.id}")
|
||||
@@ -1080,32 +1115,459 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(localBrowseItems)
|
||||
} else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == AUTO_MEDIA_ROOT) {
|
||||
mediaManager.loadAndroidAutoItems {
|
||||
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries)
|
||||
} else if (parentMediaId == AUTO_MEDIA_ROOT) {
|
||||
Log.d(tag, "Trying to initialize browseTree.")
|
||||
if (!this::browseTree.isInitialized || forceReloadingAndroidAuto) {
|
||||
forceReloadingAndroidAuto = false
|
||||
mediaManager.loadAndroidAutoItems {
|
||||
Log.d(tag, "android auto loaded. Starting browseTree initialize")
|
||||
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries, mediaManager.allLibraryPersonalizationsDone)
|
||||
val children = browseTree[parentMediaId]?.map { item ->
|
||||
Log.d(tag, "Found top menu item: ${item.description.title}")
|
||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
Log.d(tag, "browseTree initialize and android auto loaded")
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
firstLoadDone = true
|
||||
if (mediaManager.serverLibraries.isNotEmpty()) {
|
||||
Log.d(tag, "Starting personalization fetch")
|
||||
mediaManager.populatePersonalizedDataForAllLibraries {
|
||||
notifyChildrenChanged("/")
|
||||
}
|
||||
|
||||
Log.d(tag, "Initialize inprogress items")
|
||||
mediaManager.initializeInProgressItems {
|
||||
notifyChildrenChanged("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(tag, "Starting browseTree refresh")
|
||||
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries, mediaManager.allLibraryPersonalizationsDone)
|
||||
val children = browseTree[parentMediaId]?.map { item ->
|
||||
Log.d(tag, "Loading Browser Media Item ${item.description.title}")
|
||||
Log.d(tag, "Found top menu item: ${item.description.title}")
|
||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
Log.d(tag, "browseTree initialize and android auto loaded")
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == RECENTLY_ROOT) {
|
||||
Log.d(tag, "First load done: $firstLoadDone")
|
||||
if (!firstLoadDone) {
|
||||
result.sendResult(null)
|
||||
return
|
||||
}
|
||||
// Wait until top-menu is initialized
|
||||
while (!this::browseTree.isInitialized) {}
|
||||
val children = browseTree[parentMediaId]?.map { item ->
|
||||
Log.d(tag, "[MENU: $parentMediaId] Showing list item ${item.description.title}")
|
||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else if (mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
||||
Log.d(tag, "Loading items for library $parentMediaId")
|
||||
val selectedLibrary = mediaManager.getLibrary(parentMediaId)
|
||||
if (selectedLibrary?.mediaType == "podcast") { // Podcasts are browseable
|
||||
mediaManager.loadLibraryPodcasts(parentMediaId) { libraryItems ->
|
||||
val children = libraryItems?.map { libraryItem ->
|
||||
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(
|
||||
mediaDescription,
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else {
|
||||
val children = mutableListOf(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Authors")
|
||||
.setMediaId("__LIBRARY__${parentMediaId}__AUTHORS")
|
||||
.setIconUri(getUriToAbsIconDrawable(ctx, "authors"))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
),
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Series")
|
||||
.setMediaId("__LIBRARY__${parentMediaId}__SERIES_LIST")
|
||||
.setIconUri(getUriToAbsIconDrawable(ctx, "columns"))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
),
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Collections")
|
||||
.setMediaId("__LIBRARY__${parentMediaId}__COLLECTIONS")
|
||||
.setIconUri(getUriToDrawable(ctx, R.drawable.md_book_multiple_outline))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
if (mediaManager.getHasDiscovery(parentMediaId)) {
|
||||
children.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Discovery")
|
||||
.setMediaId("__LIBRARY__${parentMediaId}__DISCOVERY")
|
||||
.setIconUri(getUriToDrawable(ctx, R.drawable.md_telescope))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
||||
Log.d(tag, "Loading items for library $parentMediaId")
|
||||
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
|
||||
val children = libraryItems.map { libraryItem ->
|
||||
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
|
||||
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
} else if (parentMediaId.startsWith(RECENTLY_ROOT)) {
|
||||
Log.d(tag, "Browsing recently $parentMediaId")
|
||||
val mediaIdParts = parentMediaId.split("__")
|
||||
if (!mediaManager.getIsLibrary(mediaIdParts[2])) {
|
||||
Log.d(tag, "${mediaIdParts[2]} is not library")
|
||||
result.sendResult(null)
|
||||
return
|
||||
}
|
||||
Log.d(tag, "Mediaparts: ${mediaIdParts.size} | $mediaIdParts")
|
||||
if (mediaIdParts.size == 3) {
|
||||
mediaManager.getLibraryRecentShelfs(mediaIdParts[2]) { availableShelfs ->
|
||||
Log.d(tag, "Found ${availableShelfs.size} shelfs")
|
||||
val children : MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
for (shelf in availableShelfs) {
|
||||
if (shelf.type == "book") {
|
||||
children.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Books")
|
||||
.setMediaId("${parentMediaId}__BOOK")
|
||||
.setIconUri(getUriToDrawable(ctx, R.drawable.md_book_open_blank_variant_outline))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
} else if (shelf.type == "series") {
|
||||
children.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Series")
|
||||
.setMediaId("${parentMediaId}__SERIES")
|
||||
.setIconUri(getUriToAbsIconDrawable(ctx, "columns"))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
} else if (shelf.type == "episode") {
|
||||
children.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Episodes")
|
||||
.setMediaId("${parentMediaId}__EPISODE")
|
||||
.setIconUri(getUriToAbsIconDrawable(ctx, "microphone_2"))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
} else if (shelf.type == "podcast") {
|
||||
children.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Podcast")
|
||||
.setMediaId("${parentMediaId}__PODCAST")
|
||||
.setIconUri(getUriToAbsIconDrawable(ctx, "podcast"))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
} else if (shelf.type == "authors") {
|
||||
children.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Authors")
|
||||
.setMediaId("${parentMediaId}__AUTHORS")
|
||||
.setIconUri(getUriToAbsIconDrawable(ctx, "authors"))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (mediaIdParts.size == 4) {
|
||||
mediaManager.getLibraryRecentShelfByType(mediaIdParts[2], mediaIdParts[3]) { shelf ->
|
||||
if (shelf === null) {
|
||||
result.sendResult(mutableListOf())
|
||||
}else {
|
||||
if (shelf.type == "book") {
|
||||
val children = (shelf as LibraryShelfBookEntity).entities?.map { libraryItem ->
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
val description = libraryItem.getMediaDescription(progress, ctx, null, false)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else if (shelf.type == "episode") {
|
||||
val episodesWithRecentEpisode = (shelf as LibraryShelfEpisodeEntity).entities?.filter { libraryItem -> libraryItem.recentEpisode !== null }
|
||||
val children = episodesWithRecentEpisode?.map { libraryItem ->
|
||||
val podcast = libraryItem.media as Podcast
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.libraryId && it.episodeId == libraryItem.recentEpisode?.id }
|
||||
|
||||
// to show download icon
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.recentEpisode!!.id)
|
||||
localLibraryItem?.let { lli ->
|
||||
val localEpisode = (lli.media as Podcast).episodes?.find { it.serverEpisodeId == libraryItem.recentEpisode.id }
|
||||
libraryItem.recentEpisode.localEpisodeId = localEpisode?.id
|
||||
}
|
||||
|
||||
val description = libraryItem.recentEpisode.getMediaDescription(libraryItem, progress, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else if (shelf.type == "podcast") {
|
||||
val children = (shelf as LibraryShelfPodcastEntity).entities?.map { libraryItem ->
|
||||
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(
|
||||
mediaDescription,
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else if (shelf.type == "series") {
|
||||
val children = (shelf as LibraryShelfSeriesEntity).entities?.map { librarySeriesItem ->
|
||||
val description = librarySeriesItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else if (shelf.type == "authors") {
|
||||
val children = (shelf as LibraryShelfAuthorEntity).entities?.map { authorItem ->
|
||||
val description = authorItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else {
|
||||
result.sendResult(mutableListOf())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (parentMediaId.startsWith("__LIBRARY__")) {
|
||||
Log.d(tag, "Browsing library $parentMediaId")
|
||||
val mediaIdParts = parentMediaId.split("__")
|
||||
/*
|
||||
MediaIdParts for Library
|
||||
1: LIBRARY
|
||||
2: mediaId for library
|
||||
3: Browsing style (AUTHORS, AUTHOR, AUTHOR_SERIES, SERIES_LIST, SERIES, COLLECTION, COLLECTIONS, DISCOVERY)
|
||||
4:
|
||||
- Paging: SERIES_LIST, AUTHORS
|
||||
- SeriesId: SERIES
|
||||
- AuthorId: AUTHOR, AUTHOR_SERIES
|
||||
- CollectionId: COLLECTIONS
|
||||
5: SeriesId: AUTHOR_SERIES
|
||||
*/
|
||||
if (!mediaManager.getIsLibrary(mediaIdParts[2])) {
|
||||
Log.d(tag, "${mediaIdParts[2]} is not library")
|
||||
result.sendResult(null)
|
||||
return
|
||||
}
|
||||
Log.d(tag, "$mediaIdParts")
|
||||
if (mediaIdParts[3] == "SERIES_LIST" && mediaIdParts.size == 5) {
|
||||
Log.d(tag, "Loading series from library ${mediaIdParts[2]} with paging ${mediaIdParts[4]}")
|
||||
mediaManager.loadLibrarySeriesWithAudio(mediaIdParts[2], mediaIdParts[4]) { seriesItems ->
|
||||
Log.d(tag, "Received ${seriesItems.size} series")
|
||||
|
||||
val seriesLetters = seriesItems.groupingBy { iwb -> iwb.title.substring(0, mediaIdParts[4].length + 1).uppercase() }.eachCount()
|
||||
if (seriesItems.size > DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseLimitForGrouping && seriesItems.size > 1 && seriesLetters.size > 1) {
|
||||
val children = seriesLetters.map { (seriesLetter, seriesCount) ->
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle(seriesLetter)
|
||||
.setMediaId("${parentMediaId}${seriesLetter.last()}")
|
||||
.setSubtitle("$seriesCount series")
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else {
|
||||
val children = seriesItems.map { seriesItem ->
|
||||
val description = seriesItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
}
|
||||
} else if (mediaIdParts[3] == "SERIES_LIST") {
|
||||
Log.d(tag, "Loading series from library ${mediaIdParts[2]}")
|
||||
mediaManager.loadLibrarySeriesWithAudio(mediaIdParts[2]) { seriesItems ->
|
||||
Log.d(tag, "Received ${seriesItems.size} series")
|
||||
if (seriesItems.size > DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseLimitForGrouping && seriesItems.size > 1) {
|
||||
val seriesLetters = seriesItems.groupingBy { iwb -> iwb.title.first().uppercaseChar() }.eachCount()
|
||||
val children = seriesLetters.map { (seriesLetter, seriesCount) ->
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle(seriesLetter.toString())
|
||||
.setSubtitle("$seriesCount series")
|
||||
.setMediaId("${parentMediaId}__${seriesLetter}")
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else {
|
||||
val children = seriesItems.map { seriesItem ->
|
||||
val description = seriesItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
}
|
||||
} else if (mediaIdParts[3] == "SERIES") {
|
||||
Log.d(tag, "Loading items for serie ${mediaIdParts[4]} from library ${mediaIdParts[2]}")
|
||||
mediaManager.loadLibrarySeriesItemsWithAudio(
|
||||
mediaIdParts[2],
|
||||
mediaIdParts[4]
|
||||
) { libraryItems ->
|
||||
Log.d(tag, "Received ${libraryItems.size} library items")
|
||||
var items = libraryItems
|
||||
if (DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseSeriesSequenceOrder === AndroidAutoBrowseSeriesSequenceOrderSetting.DESC) {
|
||||
items = libraryItems.reversed()
|
||||
}
|
||||
val children = items.map { libraryItem ->
|
||||
val progress =
|
||||
mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
val description = libraryItem.getMediaDescription(progress, ctx, null, true)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (mediaIdParts[3] == "AUTHORS" && mediaIdParts.size == 5) {
|
||||
Log.d(tag, "Loading authors from library ${mediaIdParts[2]} with paging ${mediaIdParts[4]}")
|
||||
mediaManager.loadAuthorsWithBooks(mediaIdParts[2], mediaIdParts[4]) { authorItems ->
|
||||
Log.d(tag, "Received ${authorItems.size} authors")
|
||||
|
||||
val authorLetters = authorItems.groupingBy { iwb -> iwb.name.substring(0, mediaIdParts[4].length + 1).uppercase() }.eachCount()
|
||||
if (authorItems.size > DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseLimitForGrouping && authorItems.size > 1 && authorLetters.size > 1) {
|
||||
val children = authorLetters.map { (authorLetter, authorCount) ->
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle(authorLetter)
|
||||
.setMediaId("${parentMediaId}${authorLetter.last()}")
|
||||
.setSubtitle("$authorCount authors")
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else {
|
||||
val children = authorItems.map { authorItem ->
|
||||
val description = authorItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
}
|
||||
} else if (mediaIdParts[3] == "AUTHORS") {
|
||||
Log.d(tag, "Loading authors from library ${mediaIdParts[2]}")
|
||||
mediaManager.loadAuthorsWithBooks(mediaIdParts[2]) { authorItems ->
|
||||
Log.d(tag, "Received ${authorItems.size} authors")
|
||||
if (authorItems.size > DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseLimitForGrouping && authorItems.size > 1) {
|
||||
val authorLetters = authorItems.groupingBy { iwb -> iwb.name.first().uppercaseChar() }.eachCount()
|
||||
val children = authorLetters.map { (authorLetter, authorCount) ->
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle(authorLetter.toString())
|
||||
.setSubtitle("$authorCount authors")
|
||||
.setMediaId("${parentMediaId}__${authorLetter}")
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else {
|
||||
val children = authorItems.map { authorItem ->
|
||||
val description = authorItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
}
|
||||
} else if (mediaIdParts[3] == "AUTHOR") {
|
||||
mediaManager.loadAuthorBooksWithAudio(mediaIdParts[2], mediaIdParts[4]) { libraryItems ->
|
||||
val children = libraryItems.map { libraryItem ->
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
if (libraryItem.collapsedSeries != null) {
|
||||
val description = libraryItem.getMediaDescription(progress, ctx, mediaIdParts[4])
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
} else {
|
||||
val description = libraryItem.getMediaDescription(progress, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (mediaIdParts[3] == "AUTHOR_SERIES") {
|
||||
mediaManager.loadAuthorSeriesBooksWithAudio(mediaIdParts[2], mediaIdParts[4], mediaIdParts[5]) { libraryItems ->
|
||||
var items = libraryItems
|
||||
if (DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseSeriesSequenceOrder === AndroidAutoBrowseSeriesSequenceOrderSetting.DESC) {
|
||||
items = libraryItems.reversed()
|
||||
}
|
||||
val children = items.map { libraryItem ->
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
val description = libraryItem.getMediaDescription(progress, ctx, null, true)
|
||||
if (libraryItem.collapsedSeries != null) {
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
} else {
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (mediaIdParts[3] == "COLLECTIONS") {
|
||||
Log.d(tag, "Loading collections from library ${mediaIdParts[2]}")
|
||||
mediaManager.loadLibraryCollectionsWithAudio(mediaIdParts[2]) { collectionItems ->
|
||||
Log.d(tag, "Received ${collectionItems.size} collections")
|
||||
val children = collectionItems.map { collectionItem ->
|
||||
val description = collectionItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (mediaIdParts[3] == "COLLECTION") {
|
||||
Log.d(tag, "Loading collection ${mediaIdParts[4]} books from library ${mediaIdParts[2]}")
|
||||
mediaManager.loadLibraryCollectionBooksWithAudio(mediaIdParts[2], mediaIdParts[4]) { libraryItems ->
|
||||
Log.d(tag, "Received ${libraryItems.size} collections")
|
||||
val children = libraryItems.map { libraryItem ->
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
val description = libraryItem.getMediaDescription(progress, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else if (mediaIdParts[3] == "DISCOVERY") {
|
||||
Log.d(tag, "Loading discovery from library ${mediaIdParts[2]}")
|
||||
mediaManager.loadLibraryDiscoveryBooksWithAudio(mediaIdParts[2]) { libraryItems ->
|
||||
Log.d(tag, "Received ${libraryItems.size} libraryItems for discovery")
|
||||
val children = libraryItems.map { libraryItem ->
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
val description = libraryItem.getMediaDescription(progress, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else {
|
||||
result.sendResult(null)
|
||||
}
|
||||
} else {
|
||||
Log.d(tag, "Loading podcast episodes for podcast $parentMediaId")
|
||||
@@ -1117,13 +1579,35 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
|
||||
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||
result.detach()
|
||||
mediaManager.loadAndroidAutoItems {
|
||||
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries)
|
||||
val children = browseTree[LIBRARIES_ROOT]?.map { item ->
|
||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
if (cachedSearch != query) {
|
||||
Log.d(tag, "Search bundle: $extras")
|
||||
var foundBooks: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
var foundPodcasts: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
var foundSeries: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
var foundAuthors: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
|
||||
mediaManager.serverLibraries.forEach { serverLibrary ->
|
||||
runBlocking {
|
||||
// Skip searching library if it doesn't have any audio files
|
||||
if (serverLibrary.stats?.numAudioFiles == 0) return@runBlocking
|
||||
val searchResult = mediaManager.doSearch(serverLibrary.id, query)
|
||||
for (resultData in searchResult.entries.iterator()) {
|
||||
when (resultData.key) {
|
||||
"book" -> foundBooks.addAll(resultData.value)
|
||||
"series" -> foundSeries.addAll(resultData.value)
|
||||
"authors" -> foundAuthors.addAll(resultData.value)
|
||||
"podcast" -> foundPodcasts.addAll(resultData.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
foundBooks.addAll(foundSeries)
|
||||
foundBooks.addAll(foundAuthors)
|
||||
cachedSearchResults = foundBooks
|
||||
}
|
||||
result.sendResult(cachedSearchResults)
|
||||
cachedSearch = query
|
||||
Log.d(tag, "onSearch: Done")
|
||||
}
|
||||
|
||||
//
|
||||
@@ -1186,6 +1670,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
hasNetworkConnectivity = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
Log.i(tag, "Network capabilities changed. hasNetworkConnectivity=$hasNetworkConnectivity | isUnmeteredNetwork=$isUnmeteredNetwork")
|
||||
clientEventEmitter?.onNetworkMeteredChanged(isUnmeteredNetwork)
|
||||
if (hasNetworkConnectivity) {
|
||||
// Force android auto loading if libraries are empty.
|
||||
// Lack of network connectivity is most likely reason for libraries being empty
|
||||
if (isBrowseTreeInitialized() && firstLoadDone && mediaManager.serverLibraries.isEmpty()) {
|
||||
forceReloadingAndroidAuto = true
|
||||
notifyChildrenChanged("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +198,9 @@ class AbsAudioPlayer : Plugin() {
|
||||
return call.resolve(JSObject("{\"error\":\"Podcast episode not found\"}"))
|
||||
}
|
||||
}
|
||||
if (!it.hasTracks(episode)) {
|
||||
return call.resolve(JSObject("{\"error\":\"No audio files found on device. Download book again to fix.\"}"))
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Log.d(tag, "prepareLibraryItem: Preparing Local Media item ${jacksonMapper.writeValueAsString(it)}")
|
||||
|
||||
@@ -196,7 +196,11 @@ class AbsFileSystem : Plugin() {
|
||||
if (localLibraryItem?.folderId?.startsWith("internal-") == true) {
|
||||
Log.d(tag, "Deleting internal library item at absolutePath $absolutePath")
|
||||
val file = File(absolutePath)
|
||||
success = file.deleteRecursively()
|
||||
success = if (file.exists()) {
|
||||
file.deleteRecursively()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
var subfolderPathToDelete = ""
|
||||
localLibraryItem?.folderId?.let { folderId ->
|
||||
@@ -218,7 +222,12 @@ class AbsFileSystem : Plugin() {
|
||||
}
|
||||
|
||||
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
||||
success = docfile?.delete() == true
|
||||
if (docfile?.exists() == true) {
|
||||
success = docfile.delete() == true
|
||||
} else {
|
||||
Log.d(tag, "Folder $contentUrl doesn't exist")
|
||||
success = true
|
||||
}
|
||||
|
||||
if (subfolderPathToDelete != "") {
|
||||
Log.d(tag, "Deleting empty subfolder at $subfolderPathToDelete")
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.audiobookshelf.app.data.*
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
@@ -11,6 +12,7 @@ import com.audiobookshelf.app.media.MediaEventManager
|
||||
import com.audiobookshelf.app.media.MediaProgressSyncData
|
||||
import com.audiobookshelf.app.media.SyncResult
|
||||
import com.audiobookshelf.app.models.User
|
||||
import com.audiobookshelf.app.BuildConfig
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
@@ -43,10 +45,17 @@ class ApiHandler(var ctx:Context) {
|
||||
val address = config?.address ?: DeviceManager.serverAddress
|
||||
val token = config?.token ?: DeviceManager.token
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${address}$endpoint").addHeader("Authorization", "Bearer $token")
|
||||
.build()
|
||||
makeRequest(request, httpClient, cb)
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url("${address}$endpoint").addHeader("Authorization", "Bearer $token")
|
||||
.build()
|
||||
makeRequest(request, httpClient, cb)
|
||||
} catch(e: Exception) {
|
||||
e.printStackTrace()
|
||||
val jsobj = JSObject()
|
||||
jsobj.put("error", "Request failed: ${e.message}")
|
||||
cb(jsobj)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postRequest(endpoint:String, payload: JSObject?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
||||
@@ -56,23 +65,38 @@ class ApiHandler(var ctx:Context) {
|
||||
val requestBody = payload?.toString()?.toRequestBody(mediaType) ?: EMPTY_REQUEST
|
||||
val requestUrl = "${address}$endpoint"
|
||||
Log.d(tag, "postRequest to $requestUrl")
|
||||
val request = Request.Builder().post(requestBody)
|
||||
.url(requestUrl).addHeader("Authorization", "Bearer ${token}")
|
||||
.build()
|
||||
makeRequest(request, null, cb)
|
||||
try {
|
||||
val request = Request.Builder().post(requestBody)
|
||||
.url(requestUrl).addHeader("Authorization", "Bearer ${token}")
|
||||
.build()
|
||||
makeRequest(request, null, cb)
|
||||
} catch(e: Exception) {
|
||||
e.printStackTrace()
|
||||
val jsobj = JSObject()
|
||||
jsobj.put("error", "Request failed: ${e.message}")
|
||||
cb(jsobj)
|
||||
}
|
||||
}
|
||||
|
||||
private fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val requestBody = payload.toString().toRequestBody(mediaType)
|
||||
val request = Request.Builder().patch(requestBody)
|
||||
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
|
||||
.build()
|
||||
makeRequest(request, null, cb)
|
||||
try {
|
||||
val request = Request.Builder().patch(requestBody)
|
||||
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
|
||||
.build()
|
||||
makeRequest(request, null, cb)
|
||||
} catch(e: Exception) {
|
||||
e.printStackTrace()
|
||||
val jsobj = JSObject()
|
||||
jsobj.put("error", "Request failed: ${e.message}")
|
||||
cb(jsobj)
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) {
|
||||
val client = httpClient ?: defaultClient
|
||||
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
Log.d(tag, "FAILURE TO CONNECT")
|
||||
@@ -131,7 +155,7 @@ class ApiHandler(var ctx:Context) {
|
||||
|
||||
fun getLibraries(cb: (List<Library>) -> Unit) {
|
||||
val mapper = jacksonMapper
|
||||
getRequest("/api/libraries", null,null) {
|
||||
getRequest("/api/libraries?include=stats", null,null) {
|
||||
val libraries = mutableListOf<Library>()
|
||||
|
||||
var array = JSONArray()
|
||||
@@ -148,6 +172,23 @@ class ApiHandler(var ctx:Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryPersonalized(libraryItemId:String, cb: (List<LibraryShelfType>?) -> Unit) {
|
||||
getRequest("/api/libraries/$libraryItemId/personalized", null, null) {
|
||||
if (it.has("error")) {
|
||||
Log.e(tag, it.getString("error") ?: "getLibraryStats Failed")
|
||||
cb(null)
|
||||
} else {
|
||||
val items = mutableListOf<LibraryShelfType>()
|
||||
val array = it.getJSONArray("value")
|
||||
for (i in 0 until array.length()) {
|
||||
val item = jacksonMapper.readValue<LibraryShelfType>(array.get(i).toString())
|
||||
items.add(item)
|
||||
}
|
||||
cb(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryItem(libraryItemId:String, cb: (LibraryItem?) -> Unit) {
|
||||
getRequest("/api/items/$libraryItemId?expanded=1", null, null) {
|
||||
if (it.has("error")) {
|
||||
@@ -188,6 +229,103 @@ class ApiHandler(var ctx:Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibrarySeries(libraryId:String, cb: (List<LibrarySeriesItem>) -> Unit) {
|
||||
Log.d(tag, "Getting series")
|
||||
getRequest("/api/libraries/$libraryId/series?minified=1&sort=name&limit=10000", null, null) {
|
||||
val items = mutableListOf<LibrarySeriesItem>()
|
||||
if (it.has("results")) {
|
||||
val array = it.getJSONArray("results")
|
||||
for (i in 0 until array.length()) {
|
||||
val item = jacksonMapper.readValue<LibrarySeriesItem>(array.get(i).toString())
|
||||
items.add(item)
|
||||
}
|
||||
}
|
||||
cb(items)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibrarySeriesItems(libraryId:String, seriesId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||
Log.d(tag, "Getting items for series")
|
||||
val seriesIdBase64 = Base64.encodeToString(seriesId.toByteArray(), Base64.DEFAULT)
|
||||
getRequest("/api/libraries/$libraryId/items?minified=1&sort=media.metadata.title&filter=series.${seriesIdBase64}&limit=1000", null, null) {
|
||||
val items = mutableListOf<LibraryItem>()
|
||||
if (it.has("results")) {
|
||||
val array = it.getJSONArray("results")
|
||||
for (i in 0 until array.length()) {
|
||||
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
|
||||
items.add(item)
|
||||
}
|
||||
}
|
||||
cb(items)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryAuthors(libraryId:String, cb: (List<LibraryAuthorItem>) -> Unit) {
|
||||
Log.d(tag, "Getting series")
|
||||
getRequest("/api/libraries/$libraryId/authors", null, null) {
|
||||
val items = mutableListOf<LibraryAuthorItem>()
|
||||
if (it.has("authors")) {
|
||||
val array = it.getJSONArray("authors")
|
||||
for (i in 0 until array.length()) {
|
||||
val item = jacksonMapper.readValue<LibraryAuthorItem>(array.get(i).toString())
|
||||
items.add(item)
|
||||
}
|
||||
}else{
|
||||
Log.e(tag, "No results")
|
||||
}
|
||||
cb(items)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryItemsFromAuthor(libraryId:String, authorId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||
Log.d(tag, "Getting author items")
|
||||
val authorIdBase64 = Base64.encodeToString(authorId.toByteArray(), Base64.DEFAULT)
|
||||
getRequest("/api/libraries/$libraryId/items?limit=1000&minified=1&filter=authors.${authorIdBase64}&sort=media.metadata.title&collapseseries=1", null, null) {
|
||||
val items = mutableListOf<LibraryItem>()
|
||||
if (it.has("results")) {
|
||||
val array = it.getJSONArray("results")
|
||||
for (i in 0 until array.length()) {
|
||||
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
|
||||
if (item.collapsedSeries != null) {
|
||||
item.collapsedSeries?.libraryId = libraryId
|
||||
}
|
||||
items.add(item)
|
||||
}
|
||||
}else{
|
||||
Log.e(tag, "No results")
|
||||
}
|
||||
cb(items)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryCollections(libraryId:String, cb: (List<LibraryCollection>) -> Unit) {
|
||||
Log.d(tag, "Getting collections")
|
||||
getRequest("/api/libraries/$libraryId/collections?minified=1&sort=name&limit=1000", null, null) {
|
||||
val items = mutableListOf<LibraryCollection>()
|
||||
if (it.has("results")) {
|
||||
val array = it.getJSONArray("results")
|
||||
for (i in 0 until array.length()) {
|
||||
val item = jacksonMapper.readValue<LibraryCollection>(array.get(i).toString())
|
||||
items.add(item)
|
||||
}
|
||||
}
|
||||
cb(items)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSearchResults(libraryId:String, queryString:String, cb: (LibraryItemSearchResultType?) -> Unit) {
|
||||
Log.d(tag, "Doing search for library $libraryId")
|
||||
getRequest("/api/libraries/$libraryId/search?q=$queryString", null, null) {
|
||||
if (it.has("error")) {
|
||||
Log.e(tag, it.getString("error") ?: "getSearchResults Failed")
|
||||
cb(null)
|
||||
} else {
|
||||
val librarySearchResults = jacksonMapper.readValue<LibraryItemSearchResultType>(it.toString())
|
||||
cb(librarySearchResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllItemsInProgress(cb: (List<ItemInProgress>) -> Unit) {
|
||||
getRequest("/api/me/items-in-progress", null, null) {
|
||||
val items = mutableListOf<ItemInProgress>()
|
||||
@@ -296,9 +434,9 @@ class ApiHandler(var ctx:Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun closePlaybackSession(playbackSessionId:String, cb: (Boolean) -> Unit) {
|
||||
fun closePlaybackSession(playbackSessionId:String, config:ServerConnectionConfig?, cb: (Boolean) -> Unit) {
|
||||
Log.d(tag, "closePlaybackSession: playbackSessionId=$playbackSessionId")
|
||||
postRequest("/api/session/$playbackSessionId/close", null, null) {
|
||||
postRequest("/api/session/$playbackSessionId/close", null, config) {
|
||||
cb(true)
|
||||
}
|
||||
}
|
||||
@@ -327,7 +465,7 @@ class ApiHandler(var ctx:Context) {
|
||||
fun sendSyncLocalSessions(playbackSessions:List<PlaybackSession>, cb: (Boolean, String?) -> Unit) {
|
||||
@SuppressLint("HardwareIds")
|
||||
val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
|
||||
val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, Build.VERSION.RELEASE)
|
||||
val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
||||
|
||||
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions, deviceInfo)))
|
||||
Log.d(tag, "Sending ${playbackSessions.size} saved local playback sessions to server")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<!-- drawable/clock_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" /></vector>
|
||||
|
After Width: | Height: | Size: 560 B |
|
After Width: | Height: | Size: 798 B |
|
After Width: | Height: | Size: 387 B |
|
After Width: | Height: | Size: 532 B |
|
After Width: | Height: | Size: 707 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="27dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="27"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M26.719,15.761c-0.165,-0.138 -0.422,-0.341 -0.77,-0.581v-2.703c0,-6.891 -5.586,-12.477 -12.478,-12.477v0c-6.891,0 -12.477,5.586 -12.477,12.477v2.703c-0.348,0.241 -0.605,0.443 -0.77,0.581 -0.137,0.114 -0.223,0.285 -0.223,0.476 0,0 0,0 0,0.001v-0,3.238c0,0 0,0.001 0,0.001 0,0.191 0.086,0.361 0.222,0.475l0.001,0.001c0.385,0.321 1.269,0.993 2.645,1.683v0.315c0,0.849 0.548,1.537 1.223,1.537v0c0.675,0 1.223,-0.688 1.223,-1.537v-7.767c0,-0.849 -0.548,-1.537 -1.223,-1.537v0c-0.647,0 -1.177,0.632 -1.22,1.431l-0.003,0.002v-1.601c0,-5.856 4.747,-10.602 10.603,-10.602v0c5.856,0 10.603,4.747 10.603,10.602v1.601l-0.003,-0.002c-0.043,-0.799 -0.573,-1.431 -1.22,-1.431v0c-0.675,0 -1.223,0.688 -1.223,1.537v7.766c0,0.849 0.548,1.537 1.223,1.537v0c0.676,0 1.223,-0.688 1.223,-1.537v-0.315c1.376,-0.69 2.26,-1.362 2.645,-1.683 0.137,-0.114 0.223,-0.285 0.223,-0.476 0,-0 0,-0.001 0,-0.001v0,-3.237c0,-0 0,-0 0,-0 0,-0.191 -0.086,-0.361 -0.222,-0.475l-0.001,-0.001zM9.12,29.262c0.816,0 1.477,-0.661 1.477,-1.477v0,-16.543c0,-0 0,-0 0,-0 0,-0.816 -0.661,-1.477 -1.477,-1.477h-1.526c-0.816,0 -1.478,0.662 -1.478,1.478v0,16.543c0,0.816 0.661,1.477 1.477,1.477 0,0 0,0 0,0v0zM6.673,13.731h3.368v0.352h-3.368zM14.234,29.262c0.816,0 1.477,-0.661 1.477,-1.477v0,-16.543c0,-0 0,-0 0,-0 0,-0.816 -0.661,-1.477 -1.477,-1.477h-1.526c-0.816,0 -1.478,0.662 -1.478,1.478v0,16.543c0,0.816 0.661,1.477 1.477,1.477 0,0 0,0 0,0v0zM11.787,13.731h3.367v0.352h-3.367zM19.348,29.262c0.816,0 1.477,-0.661 1.477,-1.477v0,-16.543c0,-0 0,-0 0,-0 0,-0.816 -0.661,-1.477 -1.477,-1.477h-1.526c-0.816,0 -1.478,0.662 -1.478,1.478v0,16.543c0,0.816 0.661,1.477 1.477,1.477 0,0 0,0 0,0v0zM16.901,13.731h3.367v0.352h-3.367zM3.566,29.773h19.81c0.615,0 1.113,0.498 1.113,1.113v0c0,0.615 -0.498,1.113 -1.113,1.113h-19.81c-0.615,0 -1.113,-0.498 -1.113,-1.113v-0c0,-0.615 0.498,-1.113 1.113,-1.113z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M16,7.333c2.577,0 4.667,2.089 4.667,4.667v0c0,2.577 -2.089,4.667 -4.667,4.667v0c-2.577,0 -4.667,-2.089 -4.667,-4.667v0c0,-2.577 2.089,-4.667 4.667,-4.667v0zM6.667,10.667c0.747,0 1.44,0.2 2.04,0.56 -0.2,1.907 0.36,3.8 1.507,5.28 -0.667,1.28 -2,2.16 -3.547,2.16 -2.209,0 -4,-1.791 -4,-4v0c0,-2.209 1.791,-4 4,-4v0zM25.333,10.667c2.209,0 4,1.791 4,4v0c0,2.209 -1.791,4 -4,4v0c-1.547,0 -2.88,-0.88 -3.547,-2.16 1.147,-1.48 1.707,-3.373 1.507,-5.28 0.6,-0.36 1.293,-0.56 2.04,-0.56zM7.333,24.333c0,-2.76 3.88,-5 8.667,-5s8.667,2.24 8.667,5v2.333h-17.333v-2.333zM0,26.667v-2c0,-1.853 2.52,-3.413 5.933,-3.867 -0.787,0.907 -1.267,2.16 -1.267,3.533v2.333h-4.667zM32,26.667h-4.667v-2.333c0,-1.373 -0.48,-2.627 -1.267,-3.533 3.413,0.453 5.933,2.013 5.933,3.867v2z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M28,4v26h-21c-1.657,0 -3,-1.343 -3,-3s1.343,-3 3,-3h19v-24h-20c-2.2,0 -4,1.8 -4,4v24c0,2.2 1.8,4 4,4h24v-28h-2zM7.002,26v0c-0.001,0 -0.001,0 -0.002,0 -0.552,0 -1,0.448 -1,1s0.448,1 1,1c0.001,0 0.001,-0 0.002,-0v0h18.997v-2h-18.997z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M12,4v20h4v-20h-4zM16,6.667l5.333,17.333 4,-1.333 -5.333,-17.333 -4,1.333zM6.667,6.667v17.333h4v-17.333h-4zM4,25.333v2.667h24v-2.667h-24z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M7,4h-6c-0.55,0 -1,0.45 -1,1v22c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1v-22c0,-0.55 -0.45,-1 -1,-1zM6,10h-4v-2h4v2zM17,4h-6c-0.55,0 -1,0.45 -1,1v22c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1v-22c0,-0.55 -0.45,-1 -1,-1zM16,10h-4v-2h4v2zM23.909,5.546l-5.358,2.7c-0.491,0.247 -0.691,0.852 -0.443,1.343l8.999,17.861c0.247,0.491 0.852,0.691 1.343,0.443l5.358,-2.7c0.491,-0.247 0.691,-0.852 0.443,-1.343l-8.999,-17.861c-0.247,-0.491 -0.852,-0.691 -1.343,-0.443z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M19.2,6h-6.4v19.2h6.4v-19.2zM22.4,6v19.2h6.4v-19.2h-6.4zM9.6,6h-6.4v19.2h6.4v-19.2zM0,2.8h32v25.6h-32v-25.6z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="27dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="27"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M13.68,0c7.518,0 13.612,2.854 13.612,6.37 0,3.518 -6.096,6.37 -13.612,6.37s-13.612,-2.854 -13.612,-6.37c0,-3.516 6.096,-6.37 13.612,-6.37v0zM0.068,21.31v4.891c2.422,8.602 26.349,6.94 27.227,-0.44v-4.885c-1.195,8.102 -25.313,8.685 -27.227,0.435v0,0zM0,8.578v4.776c2.422,8.401 26.482,7.266 27.362,0.06v-4.773c-1.198,7.914 -25.448,7.995 -27.362,-0.063v0zM0,14.75v4.891c2.422,8.602 26.482,7.44 27.362,0.06v-4.885c-1.198,8.102 -25.448,8.185 -27.362,-0.065v0z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M26,28h-20v-4l6,-10 8.219,10 5.781,-4v8zM26,15c0,1.657 -1.343,3 -3,3s-3,-1.343 -3,-3 1.343,-3 3,-3c1.657,0 3,1.343 3,3zM28.681,7.159c-0.694,-0.947 -1.662,-2.053 -2.724,-3.116s-2.169,-2.03 -3.116,-2.724c-1.612,-1.182 -2.393,-1.319 -2.841,-1.319h-15.5c-1.378,0 -2.5,1.121 -2.5,2.5v27c0,1.378 1.122,2.5 2.5,2.5h23c1.378,0 2.5,-1.122 2.5,-2.5v-19.5c0,-0.448 -0.137,-1.23 -1.319,-2.841zM24.543,5.457c0.959,0.959 1.712,1.825 2.268,2.543h-4.811v-4.811c0.718,0.556 1.584,1.309 2.543,2.268zM28,29.5c0,0.271 -0.229,0.5 -0.5,0.5h-23c-0.271,0 -0.5,-0.229 -0.5,-0.5v-27c0,-0.271 0.229,-0.5 0.5,-0.5 0,0 15.499,-0 15.5,0v7c0,0.552 0.448,1 1,1h7v19.5z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M9,18h-2v14h2c0.55,0 1,-0.45 1,-1v-12c0,-0.55 -0.45,-1 -1,-1zM23,18c-0.55,0 -1,0.45 -1,1v12c0,0.55 0.45,1 1,1h2v-14h-2zM32,16c0,-8.837 -7.163,-16 -16,-16s-16,7.163 -16,16c0,1.919 0.338,3.759 0.958,5.464 -0.609,1.038 -0.958,2.246 -0.958,3.536 0,3.526 2.608,6.443 6,6.929v-13.857c-0.997,0.143 -1.927,0.495 -2.742,1.012 -0.168,-0.835 -0.258,-1.699 -0.258,-2.584 0,-7.18 5.82,-13 13,-13s13,5.82 13,13c0,0.885 -0.088,1.749 -0.257,2.584 -0.816,-0.517 -1.745,-0.87 -2.743,-1.013v13.858c3.392,-0.485 6,-3.402 6,-6.929 0,-1.29 -0.349,-2.498 -0.958,-3.536 0.62,-1.705 0.958,-3.545 0.958,-5.465z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M23.6,2c-3.363,0 -6.258,2.736 -7.599,5.594 -1.342,-2.858 -4.237,-5.594 -7.601,-5.594 -4.637,0 -8.4,3.764 -8.4,8.401 0,9.433 9.516,11.906 16.001,21.232 6.13,-9.268 15.999,-12.1 15.999,-21.232 0,-4.637 -3.763,-8.401 -8.4,-8.401z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M6,6v10c0,3.313 2.688,6 6,6s6,-2.688 6,-6h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5v-2h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5v-2h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5c0,-3.313 -2.688,-6 -6,-6s-6,2.688 -6,6zM20,15v1c0,4.419 -3.581,8 -8,8s-8,-3.581 -8,-8v-2.5c0,-0.831 -0.669,-1.5 -1.5,-1.5s-1.5,0.669 -1.5,1.5v2.5c0,5.569 4.138,10.169 9.5,10.9v2.1h-3c-0.831,0 -1.5,0.669 -1.5,1.5s0.669,1.5 1.5,1.5h9c0.831,0 1.5,-0.669 1.5,-1.5s-0.669,-1.5 -1.5,-1.5h-3v-2.1c5.363,-0.731 9.5,-5.331 9.5,-10.9v-2.5c0,-0.831 -0.669,-1.5 -1.5,-1.5s-1.5,0.669 -1.5,1.5v1.5z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M22.292,14.872c0,-0.836 -0.677,-1.51 -1.51,-1.51 -0.836,0 -1.51,0.677 -1.51,1.51 0,2.977 -0.714,5.435 -2.159,7.076 -1.31,1.484 -3.289,2.341 -5.964,2.341s-4.654,-0.854 -5.964,-2.339c-1.448,-1.641 -2.159,-4.099 -2.159,-7.078 0,-0.836 -0.677,-1.51 -1.51,-1.51 -0.836,0 -1.51,0.677 -1.51,1.51 0,3.711 0.961,6.857 2.914,9.073 1.438,1.63 3.375,2.734 5.815,3.164v2.479h-3.703c-0.661,0 -1.203,0.542 -1.203,1.203v1.206h14.646v-1.206c0,-0.661 -0.542,-1.203 -1.203,-1.203h-3.711v-2.479c2.44,-0.432 4.375,-1.534 5.815,-3.167 1.953,-2.214 2.917,-5.359 2.917,-9.07v0,0zM11.146,0c3.083,0 5.604,2.523 5.604,5.604v0.146h-3.013v3.57h3.016v1.818h-3.016v3.57h3.016v1.284c0,3.083 -2.523,5.604 -5.604,5.604 -3.083,0 -5.604,-2.523 -5.604,-5.604v-1.284h3.016v-3.57h-3.018v-1.818h3.016v-3.57h-3.016v-0.146c0,-3.081 2.521,-5.604 5.604,-5.604v0z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M15,22c2.761,0 5,-2.239 5,-5v-12c0,-2.761 -2.239,-5 -5,-5s-5,2.239 -5,5v12c0,2.761 2.239,5 5,5zM22,14v3c0,3.866 -3.134,7 -7,7s-7,-3.134 -7,-7v-3h-2v3c0,4.632 3.5,8.447 8,8.944v4.056h-4v2h10v-2h-4v-4.056c4.5,-0.497 8,-4.312 8,-8.944v-3h-2z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M30,0h2v23c0,2.761 -3.134,5 -7,5s-7,-2.239 -7,-5c0,-2.761 3.134,-5 7,-5 1.959,0 3.729,0.575 5,1.501v-11.501l-16,3.556v15.444c0,2.761 -3.134,5 -7,5s-7,-2.239 -7,-5c0,-2.761 3.134,-5 7,-5 1.959,0 3.729,0.575 5,1.501v-19.501l18,-4z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="33dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="33"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M18.289,18.549v13.45h-3.973v-13.45c-1.305,-0.706 -2.191,-2.087 -2.191,-3.676 0,-2.307 1.871,-4.178 4.178,-4.178s4.178,1.871 4.178,4.178c0,1.589 -0.887,2.97 -2.192,3.676v0zM10.434,2.963c0.774,-0.382 1.091,-1.319 0.709,-2.091s-1.318,-1.091 -2.091,-0.71c-3.158,1.558 -5.613,4.134 -7.165,7.169 -1.146,2.241 -1.8,4.734 -1.879,7.251 -0.079,2.538 0.421,5.101 1.585,7.46 1.433,2.906 3.863,5.491 7.44,7.322 0.767,0.392 1.706,0.088 2.098,-0.679s0.088,-1.706 -0.679,-2.097c-2.929,-1.498 -4.905,-3.589 -6.059,-5.926 -0.93,-1.887 -1.33,-3.942 -1.267,-5.981 0.064,-2.058 0.599,-4.098 1.536,-5.93 1.256,-2.455 3.234,-4.536 5.771,-5.786v0zM23.554,0.161c-0.773,-0.382 -1.71,-0.064 -2.091,0.71s-0.064,1.71 0.71,2.091c2.537,1.251 4.515,3.332 5.771,5.787 0.937,1.832 1.472,3.871 1.536,5.93 0.064,2.038 -0.336,4.094 -1.267,5.981 -1.153,2.337 -3.13,4.428 -6.059,5.926 -0.767,0.392 -1.07,1.331 -0.679,2.097s1.331,1.071 2.098,0.679c3.577,-1.83 6.007,-4.415 7.44,-7.322 1.164,-2.359 1.664,-4.922 1.585,-7.46 -0.079,-2.516 -0.732,-5.01 -1.878,-7.251 -1.552,-3.034 -4.008,-5.611 -7.165,-7.169v0zM21.968,7.033c-0.582,-0.42 -1.395,-0.287 -1.814,0.295s-0.287,1.395 0.295,1.814c0.235,0.169 0.465,0.359 0.691,0.566 1.319,1.216 2.101,2.838 2.266,4.553 0.166,1.731 -0.295,3.566 -1.464,5.188 -0.198,0.276 -0.427,0.555 -0.686,0.836 -0.487,0.529 -0.453,1.353 0.076,1.839s1.353,0.452 1.84,-0.076c0.313,-0.339 0.607,-0.701 0.88,-1.081 1.555,-2.16 2.167,-4.617 1.943,-6.951 -0.225,-2.349 -1.293,-4.566 -3.091,-6.224 -0.283,-0.261 -0.595,-0.514 -0.935,-0.76v0zM12.156,9.143c0.582,-0.42 0.715,-1.232 0.295,-1.814s-1.232,-0.715 -1.814,-0.296c-0.341,0.246 -0.652,0.499 -0.935,0.76 -1.799,1.658 -2.866,3.875 -3.091,6.224 -0.224,2.334 0.387,4.792 1.943,6.952 0.274,0.379 0.567,0.741 0.88,1.08 0.487,0.529 1.311,0.563 1.839,0.076s0.563,-1.311 0.076,-1.839c-0.259,-0.281 -0.487,-0.56 -0.686,-0.836 -1.169,-1.623 -1.63,-3.457 -1.464,-5.188 0.164,-1.715 0.947,-3.337 2.266,-4.553 0.226,-0.207 0.456,-0.397 0.691,-0.566v0z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M30.925,2.938c0.794,-0.231 1.25,-1.069 1.019,-1.863s-1.069,-1.25 -1.869,-1.012l-26.844,7.869c-0.587,0.169 -1.119,0.456 -1.569,0.825 -1.006,0.725 -1.663,1.906 -1.663,3.244v16c0,2.206 1.794,4 4,4h24c2.206,0 4,-1.794 4,-4v-16c0,-2.206 -1.794,-4 -4,-4h-14.344l17.269,-5.063zM23,25c-2.762,0 -5,-2.238 -5,-5s2.238,-5 5,-5 5,2.238 5,5 -2.238,5 -5,5zM5,16c0,-0.55 0.45,-1 1,-1h6c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-6c-0.55,0 -1,-0.45 -1,-1zM4,20c0,-0.55 0.45,-1 1,-1h8c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-8c-0.55,0 -1,-0.45 -1,-1zM5,24c0,-0.55 0.45,-1 1,-1h6c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-6c-0.55,0 -1,-0.45 -1,-1z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M22,2l-10,10h-6l-6,8c0,0 6.357,-1.77 10.065,-0.94l-10.065,12.94 13.184,-10.255c1.839,4.208 -1.184,10.255 -1.184,10.255l8,-6v-6l10,-10 2,-10 -10,2z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M4.259,23.467c-2.35,0 -4.259,1.917 -4.259,4.252 0,2.349 1.909,4.244 4.259,4.244 2.358,0 4.265,-1.895 4.265,-4.244 -0,-2.336 -1.907,-4.252 -4.265,-4.252zM0.005,10.873v6.133c3.993,0 7.749,1.562 10.577,4.391 2.825,2.822 4.384,6.595 4.384,10.603h6.16c-0,-11.651 -9.478,-21.127 -21.121,-21.127zM0.012,0v6.136c14.243,0 25.836,11.604 25.836,25.864h6.152c0,-17.64 -14.352,-32 -31.988,-32z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M32,12.408l-11.056,-1.607 -4.944,-10.018 -4.944,10.018 -11.056,1.607 8,7.798 -1.889,11.011 9.889,-5.199 9.889,5.199 -1.889,-11.011 8,-7.798z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1 @@
|
||||
<!-- drawable/account_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" /></vector>
|
||||
@@ -0,0 +1 @@
|
||||
<!-- drawable/book_multiple_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M19 2A2 2 0 0 1 21 4V16A2 2 0 0 1 19 18H9A2 2 0 0 1 7 16V4A2 2 0 0 1 9 2H19M19 4H16V10L13.5 7.75L11 10V4H9V16H19M3 20A2 2 0 0 0 5 22H17V20H5V6H3Z" /></vector>
|
||||
@@ -0,0 +1 @@
|
||||
<!-- drawable/book_open_blank_variant_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12 21.5C10.65 20.65 8.2 20 6.5 20C4.85 20 3.15 20.3 1.75 21.05C1.65 21.1 1.6 21.1 1.5 21.1C1.25 21.1 1 20.85 1 20.6V6C1.6 5.55 2.25 5.25 3 5C4.11 4.65 5.33 4.5 6.5 4.5C8.45 4.5 10.55 4.9 12 6C13.45 4.9 15.55 4.5 17.5 4.5C18.67 4.5 19.89 4.65 21 5C21.75 5.25 22.4 5.55 23 6V20.6C23 20.85 22.75 21.1 22.5 21.1C22.4 21.1 22.35 21.1 22.25 21.05C20.85 20.3 19.15 20 17.5 20C15.8 20 13.35 20.65 12 21.5M11 7.5C9.64 6.9 7.84 6.5 6.5 6.5C5.3 6.5 4.1 6.65 3 7V18.5C4.1 18.15 5.3 18 6.5 18C7.84 18 9.64 18.4 11 19V7.5M13 19C14.36 18.4 16.16 18 17.5 18C18.7 18 19.9 18.15 21 18.5V7C19.9 6.65 18.7 6.5 17.5 6.5C16.16 6.5 14.36 6.9 13 7.5V19Z" /></vector>
|
||||
@@ -0,0 +1 @@
|
||||
<!-- drawable/clock_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" /></vector>
|
||||
@@ -0,0 +1 @@
|
||||
<!-- drawable/telescope.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M21.9,8.9L20.2,9.9L16.2,3L17.9,2L21.9,8.9M9.8,7.9L12.8,13.1L18.9,9.6L15.9,4.4L9.8,7.9M11.4,12.7L9.4,9.2L5.1,11.7L7.1,15.2L11.4,12.7M2.1,14.6L3.1,16.3L5.7,14.8L4.7,13.1L2.1,14.6M12.1,14L11.8,13.6L7.5,16.1L7.8,16.5C8,16.8 8.3,17.1 8.6,17.3L7,22H9L10.4,17.7H10.5L12,22H14L12.1,16.4C12.6,15.7 12.6,14.8 12.1,14Z" /></vector>
|
||||
@@ -1,15 +1,15 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.20'
|
||||
ext.kotlin_version = '2.0.0'
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
classpath 'com.android.tools.build:gradle:8.1.1'
|
||||
classpath 'com.google.gms:google-services:4.4.2'
|
||||
classpath 'com.android.tools.build:gradle:8.8.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
@@ -26,7 +26,7 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
tasks.register('clean', Delete) {
|
||||
delete rootProject.layout.buildDirectory
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,12 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
|
||||
include ':byteowls-capacitor-filesharer'
|
||||
project(':byteowls-capacitor-filesharer').projectDir = new File('../node_modules/@byteowls/capacitor-filesharer/android')
|
||||
|
||||
include ':capacitor-community-keep-awake'
|
||||
project(':capacitor-community-keep-awake').projectDir = new File('../node_modules/@capacitor-community/keep-awake/android')
|
||||
|
||||
include ':capacitor-community-volume-buttons'
|
||||
project(':capacitor-community-volume-buttons').projectDir = new File('../node_modules/@capacitor-community/volume-buttons/android')
|
||||
|
||||
include ':capacitor-app'
|
||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||
|
||||
|
||||
@@ -31,4 +31,6 @@ android.useAndroidX=true
|
||||
|
||||
kapt.use.worker.api=false
|
||||
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.buildfeatures.buildconfig=true
|
||||
|
||||
org.gradle.warning.mode=all
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
|
||||
}
|
||||
|
||||
include ':app'
|
||||
include ':capacitor-cordova-android-plugins'
|
||||
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||
|
||||
@@ -1,39 +1,24 @@
|
||||
ext {
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 34
|
||||
targetSdkVersion = 33
|
||||
androidxActivityVersion = '1.7.0'
|
||||
androidxAppCompatVersion = '1.6.1'
|
||||
compileSdkVersion = 35
|
||||
targetSdkVersion = 34
|
||||
androidxActivityVersion = '1.8.0'
|
||||
androidxAppCompatVersion = '1.7.0'
|
||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||
androidxCoreVersion = '1.10.0'
|
||||
androidPlayCore = '1.9.0'
|
||||
androidxFragmentVersion = '1.5.6'
|
||||
androidxCoreVersion = '1.12.0'
|
||||
androidxFragmentVersion = '1.6.2'
|
||||
junitVersion = '4.13.2'
|
||||
androidxJunitVersion = '1.1.5'
|
||||
androidxEspressoCoreVersion = '3.5.1'
|
||||
cordovaAndroidVersion = '10.1.1'
|
||||
androidx_car_version = '1.0.0-alpha7'
|
||||
androidx_core_ktx_version = '1.12.0'
|
||||
androidx_media_version = '1.6.0'
|
||||
androidx_preference_version = '1.1.1'
|
||||
androidx_test_runner_version = '1.3.0'
|
||||
arch_lifecycle_version = '2.2.0'
|
||||
constraint_layout_version = '2.0.1'
|
||||
espresso_version = '3.3.0'
|
||||
androidx_core_ktx_version = '1.15.0'
|
||||
androidx_media_version = '1.7.0'
|
||||
exoplayer_version = '2.18.7'
|
||||
fragment_version = '1.2.5'
|
||||
glide_version = '4.11.0'
|
||||
gms_strict_version_matcher_version = '1.0.3'
|
||||
gradle_version = '3.1.4'
|
||||
gson_version = '2.8.5'
|
||||
junit_version = '4.13'
|
||||
kotlin_version = '1.8.10'
|
||||
kotlin_coroutines_version = '1.6.4'
|
||||
multidex_version = '1.0.3'
|
||||
play_services_auth_version = '18.1.0'
|
||||
recycler_view_version = '1.1.0'
|
||||
robolectric_version = '4.2'
|
||||
junit_version = '4.13.2'
|
||||
kotlin_version = '2.1.0'
|
||||
kotlin_coroutines_version = '1.10.1'
|
||||
test_runner_version = '1.1.0'
|
||||
coreSplashScreenVersion = '1.0.1'
|
||||
androidxWebkitVersion = '1.6.1'
|
||||
androidxWebkitVersion = '1.9.0'
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
"appId": "com.audiobookshelf.app",
|
||||
"appName": "audiobookshelf-app",
|
||||
"webDir": "dist",
|
||||
"bundledWebRuntime": false,
|
||||
"plugins": {
|
||||
"CapacitorHttp": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"androidScheme": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center mr-2 cursor-pointer">
|
||||
<span class="material-icons text-3xl text-fg">arrow_back</span>
|
||||
</a>
|
||||
<div v-if="user && currentLibrary && networkConnected">
|
||||
<div v-if="user && currentLibrary && socketConnected">
|
||||
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
||||
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
|
||||
<p class="text-sm leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
||||
@@ -54,8 +54,8 @@ export default {
|
||||
this.$store.commit('setCastAvailable', val)
|
||||
}
|
||||
},
|
||||
networkConnected() {
|
||||
return this.$store.state.networkConnected
|
||||
socketConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
currentLibrary() {
|
||||
return this.$store.getters['libraries/getCurrentLibrary']
|
||||
@@ -160,4 +160,4 @@ export default {
|
||||
transform: translate(10px, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" ref="cover" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" raw @imageLoaded="coverImageLoaded" />
|
||||
</div>
|
||||
|
||||
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30">
|
||||
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30" @click.stop="showSyncsFailedDialog">
|
||||
<span class="material-icons text-error text-3xl">error</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,7 +50,7 @@
|
||||
<div id="playerContent" class="playerContainer w-full z-20 absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" :style="{ backgroundColor: showFullscreen ? '' : coverRgb }" @click="clickContainer">
|
||||
<div v-if="showFullscreen" class="absolute bottom-4 left-0 right-0 w-full pb-4 pt-2 mx-auto px-6" style="max-width: 414px">
|
||||
<div class="flex items-center justify-between pointer-events-auto">
|
||||
<span v-if="!isPodcast && serverLibraryItemId && networkConnected" class="material-icons text-3xl text-fg-muted cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
||||
<span v-if="!isPodcast && serverLibraryItemId && socketConnected" class="material-icons text-3xl text-fg-muted cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
||||
<!-- hidden for podcasts but still using this as a placeholder -->
|
||||
<span v-else class="material-icons text-3xl text-white text-opacity-0">bookmark</span>
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
<script>
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import { FastAverageColor } from 'fast-average-color'
|
||||
import WrappingMarquee from '@/assets/WrappingMarquee.js'
|
||||
|
||||
@@ -375,8 +376,8 @@ export default {
|
||||
return secondsRemaining + 's'
|
||||
}
|
||||
},
|
||||
networkConnected() {
|
||||
return this.$store.state.networkConnected
|
||||
socketConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
mediaId() {
|
||||
if (this.isPodcast || !this.playbackSession) return null
|
||||
@@ -390,6 +391,13 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showSyncsFailedDialog() {
|
||||
Dialog.alert({
|
||||
title: this.$strings.HeaderProgressSyncFailed,
|
||||
message: this.$strings.MessageProgressSyncFailed,
|
||||
cancelText: this.$strings.ButtonOk
|
||||
})
|
||||
},
|
||||
clickChaptersBtn() {
|
||||
if (!this.chapters.length) return
|
||||
this.showChapterModal = true
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
|
||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
|
||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" :is-auto="isAutoSleepTimer" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />
|
||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="currentTime" :library-item-id="serverLibraryItemId" @select="selectBookmark" />
|
||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="currentTime" :library-item-id="serverLibraryItemId" :playback-rate="playbackSpeed" @select="selectBookmark" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import CellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -39,6 +40,7 @@ export default {
|
||||
serverEpisodeId: null
|
||||
}
|
||||
},
|
||||
mixins: [CellularPermissionHelpers],
|
||||
computed: {
|
||||
bookmarks() {
|
||||
if (!this.serverLibraryItemId) return []
|
||||
@@ -193,12 +195,21 @@ export default {
|
||||
const startTime = payload.startTime
|
||||
const startWhenReady = !payload.paused
|
||||
|
||||
const isLocal = libraryItemId.startsWith('local')
|
||||
if (!isLocal) {
|
||||
const hasPermission = await this.checkCellularPermission('streaming')
|
||||
if (!hasPermission) {
|
||||
this.$store.commit('setPlayerDoneStartingPlayback')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// When playing local library item and can also play this item from the server
|
||||
// then store the server library item id so it can be used if a cast is made
|
||||
const serverLibraryItemId = payload.serverLibraryItemId || null
|
||||
const serverEpisodeId = payload.serverEpisodeId || null
|
||||
|
||||
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
|
||||
if (isLocal && this.$store.state.isCasting) {
|
||||
const { value } = await Dialog.confirm({
|
||||
title: 'Warning',
|
||||
message: `Cannot cast downloaded media items. Confirm to close cast and play on your device.`
|
||||
@@ -373,4 +384,4 @@ export default {
|
||||
this.$eventBus.$off('device-focus-update', this.deviceFocused)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -79,7 +79,10 @@ export default {
|
||||
return this.$store.state.serverSettings || {}
|
||||
},
|
||||
username() {
|
||||
return this.user ? this.user.username : ''
|
||||
return this.user?.username || ''
|
||||
},
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
navItems() {
|
||||
var items = [
|
||||
@@ -132,6 +135,12 @@ export default {
|
||||
})
|
||||
|
||||
if (this.serverConnectionConfig) {
|
||||
items.push({
|
||||
icon: 'language',
|
||||
text: this.$strings.ButtonGoToWebClient,
|
||||
action: 'openWebClient'
|
||||
})
|
||||
|
||||
items.push({
|
||||
icon: 'login',
|
||||
text: this.$strings.ButtonSwitchServerUser,
|
||||
@@ -151,6 +160,10 @@ export default {
|
||||
if (action === 'logout') {
|
||||
await this.logout()
|
||||
this.$router.push('/connect')
|
||||
} else if (action === 'openWebClient') {
|
||||
this.show = false
|
||||
let path = `/library/${this.$store.state.libraries.currentLibraryId}`
|
||||
await this.$store.dispatch('user/openWebClient', path)
|
||||
}
|
||||
},
|
||||
clickBackground() {
|
||||
@@ -158,6 +171,10 @@ export default {
|
||||
},
|
||||
async logout() {
|
||||
if (this.user) {
|
||||
if (this.$store.getters['getIsPlayerOpen']) {
|
||||
this.$eventBus.$emit('close-stream')
|
||||
}
|
||||
|
||||
await this.$nativeHttp.post('/logout').catch((error) => {
|
||||
console.error('Failed to logout', error)
|
||||
})
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
</template>
|
||||
|
||||
<div v-show="!entities.length && initialized" class="w-full py-16 text-center text-xl">
|
||||
<div class="py-4 capitalize">No {{ entityName }}</div>
|
||||
<div v-if="page === 'collections'" class="py-4">{{ $strings.MessageNoCollections }}</div>
|
||||
<div v-else class="py-4 capitalize">No {{ entityName }}</div>
|
||||
<ui-btn v-if="hasFilter" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,9 +42,6 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
_author() {
|
||||
return this.author || {}
|
||||
},
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
|
||||
<!-- Alternative bookshelf title/author/sort -->
|
||||
<div v-if="isAltViewEnabled" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
||||
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
||||
{{ displayTitle }}
|
||||
</p>
|
||||
<div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }" class="flex items-center">
|
||||
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
||||
{{ displayTitle }}
|
||||
</p>
|
||||
<widgets-explicit-indicator v-if="isExplicit" />
|
||||
</div>
|
||||
<p class="truncate text-fg-muted" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || ' ' }}</p>
|
||||
<p v-if="displaySortLine" class="truncate text-fg-muted" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||
</div>
|
||||
@@ -91,8 +94,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Podcast Num Episodes Incomplete -->
|
||||
<div v-else-if="numEpisodesIncomplete && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
||||
<p class="text-white" :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodesIncomplete }}</p>
|
||||
<div v-else-if="numEpisodesIncomplete && !isSelectionMode" class="absolute rounded-full bg-yellow-400 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
||||
<p class="text-black" :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodesIncomplete }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -281,7 +284,7 @@ export default {
|
||||
if (this.collapsedSeries) return null
|
||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
|
||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
|
||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
|
||||
if (this.orderBy === 'addedAt') return this.$getString('LabelAddedDate', [this.$formatDate(this._libraryItem.addedAt)])
|
||||
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
||||
@@ -310,7 +313,7 @@ export default {
|
||||
return !!this.userProgress?.isFinished
|
||||
},
|
||||
showError() {
|
||||
return this.numMissingParts || this.isMissing || this.isInvalid
|
||||
return this.isMissing || this.isInvalid
|
||||
},
|
||||
localLibraryItemId() {
|
||||
if (this.isLocal) return this.libraryItemId
|
||||
@@ -341,9 +344,8 @@ export default {
|
||||
isInvalid() {
|
||||
return this._libraryItem.isInvalid
|
||||
},
|
||||
numMissingParts() {
|
||||
if (this.isPodcast) return 0
|
||||
return this.media.numMissingParts
|
||||
isExplicit() {
|
||||
return !!this.mediaMetadata.explicit
|
||||
},
|
||||
overlayWrapperClasslist() {
|
||||
var classes = []
|
||||
@@ -366,9 +368,6 @@ export default {
|
||||
userCanDownload() {
|
||||
return this.store.getters['user/getUserCanDownload']
|
||||
},
|
||||
userIsRoot() {
|
||||
return this.store.getters['user/getIsRoot']
|
||||
},
|
||||
titleFontSize() {
|
||||
return 0.75 * this.sizeMultiplier
|
||||
},
|
||||
|
||||
@@ -21,7 +21,13 @@
|
||||
<p class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
|
||||
<p v-if="displaySortLine" class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||
<p v-if="duration" class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ $elapsedPretty(duration) }}</p>
|
||||
<p v-if="episodes" class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ episodes }}</p>
|
||||
|
||||
<p v-if="numEpisodesIncomplete" class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">
|
||||
{{ $getString('LabelNumEpisodesIncomplete', [numEpisodes, numEpisodesIncomplete]) }}
|
||||
</p>
|
||||
<p v-else-if="numEpisodes" class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">
|
||||
{{ $getString('LabelNumEpisodes', [numEpisodes]) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||
@@ -106,16 +112,13 @@ export default {
|
||||
isPodcast() {
|
||||
return this.mediaType === 'podcast'
|
||||
},
|
||||
episodes() {
|
||||
if (this.isPodcast) {
|
||||
if (this.media.numEpisodes == 1) {
|
||||
return '1 episode'
|
||||
} else {
|
||||
return this.media.numEpisodes + ' episodes'
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
numEpisodes() {
|
||||
if (this.isLocal && this.isPodcast && this.media.episodes) return this.media.episodes.length
|
||||
return this.media.numEpisodes
|
||||
},
|
||||
numEpisodesIncomplete() {
|
||||
if (this.isLocal) return 0
|
||||
return this._libraryItem.numEpisodesIncomplete || 0
|
||||
},
|
||||
placeholderUrl() {
|
||||
return '/book_placeholder.jpg'
|
||||
@@ -201,7 +204,7 @@ export default {
|
||||
if (this.collapsedSeries) return null
|
||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
|
||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
|
||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
|
||||
if (this.orderBy === 'addedAt') return this.$getString('LabelAddedDate', [this.$formatDate(this._libraryItem.addedAt)])
|
||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||
return null
|
||||
},
|
||||
@@ -215,7 +218,7 @@ export default {
|
||||
return !!this.userProgress?.isFinished
|
||||
},
|
||||
showError() {
|
||||
return this.numMissingParts || this.isMissing || this.isInvalid
|
||||
return this.isMissing || this.isInvalid
|
||||
},
|
||||
isStreaming() {
|
||||
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
|
||||
@@ -235,10 +238,6 @@ export default {
|
||||
isInvalid() {
|
||||
return this._libraryItem.isInvalid
|
||||
},
|
||||
numMissingParts() {
|
||||
if (this.isPodcast) return 0
|
||||
return this.media.numMissingParts
|
||||
},
|
||||
store() {
|
||||
return this.$store || this.$nuxt.$store
|
||||
},
|
||||
|
||||
@@ -3,12 +3,19 @@
|
||||
<div v-show="!loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg px-4 py-6 w-full">
|
||||
<!-- list of server connection configs -->
|
||||
<template v-if="!showForm">
|
||||
<div v-for="config in serverConnectionConfigs" :key="config.id" class="flex items-center py-4 my-1 border-b border-fg/10 relative" @click="connectToServer(config)">
|
||||
<span class="material-icons-outlined text-xl text-fg-muted">dns</span>
|
||||
<p class="pl-3 pr-6 text-base text-fg">{{ config.name }}</p>
|
||||
<div v-for="config in serverConnectionConfigs" :key="config.id" class="border-b border-fg/10 py-4">
|
||||
<div class="flex items-center my-1 relative" @click="connectToServer(config)">
|
||||
<span class="material-icons-outlined text-xl text-fg-muted">dns</span>
|
||||
<p class="pl-3 pr-6 text-base text-fg">{{ config.name }}</p>
|
||||
|
||||
<div class="absolute top-0 right-0 h-full px-4 flex items-center" @click.stop="editServerConfig(config)">
|
||||
<span class="material-icons text-lg text-fg-muted">more_vert</span>
|
||||
<div class="absolute top-0 right-0 h-full px-4 flex items-center" @click.stop="editServerConfig(config)">
|
||||
<span class="material-icons text-lg text-fg-muted">more_vert</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- warning message if server connection config is using an old user id -->
|
||||
<div v-if="!checkIdUuid(config.userId)" class="flex flex-nowrap justify-between items-center space-x-4 pt-4">
|
||||
<p class="text-xs text-warning">{{ $strings.MessageOldServerConnectionWarning }}</p>
|
||||
<ui-btn class="text-xs whitespace-nowrap" :padding-x="2" :padding-y="1" @click="showOldUserIdWarningDialog">{{ $strings.LabelMoreInfo }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-1 py-4 w-full">
|
||||
@@ -45,7 +52,7 @@
|
||||
<ui-text-input v-model="password" type="password" :disabled="processing" :placeholder="$strings.LabelPassword" class="w-full mb-2 text-lg" />
|
||||
|
||||
<div class="flex items-center pt-2">
|
||||
<ui-icon-btn v-if="serverConfig.id" small bg-color="error" icon="delete" @click="removeServerConfigClick" />
|
||||
<ui-icon-btn v-if="serverConfig.id" small bg-color="error" icon="delete" type="button" @click="removeServerConfigClick" />
|
||||
<div class="flex-grow" />
|
||||
<ui-btn :disabled="processing || !networkConnected" type="submit" class="mt-1 h-10">{{ networkConnected ? $strings.ButtonSubmit : $strings.MessageNoNetworkConnection }}</ui-btn>
|
||||
</div>
|
||||
@@ -141,6 +148,16 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showOldUserIdWarningDialog() {
|
||||
Dialog.alert({
|
||||
title: 'Old Server Connection Warning',
|
||||
message: this.$strings.MessageOldServerConnectionWarningHelp,
|
||||
cancelText: this.$strings.ButtonOk
|
||||
})
|
||||
},
|
||||
checkIdUuid(userId) {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(userId)
|
||||
},
|
||||
/**
|
||||
* Initiates the login process using OpenID via OAuth2.0.
|
||||
* 1. Verifying the server's address
|
||||
@@ -486,9 +503,8 @@ export default {
|
||||
validateServerUrl(url, protocolOverride = null) {
|
||||
try {
|
||||
var urlObject = new URL(url)
|
||||
var address = `${protocolOverride ? protocolOverride : urlObject.protocol}//${urlObject.hostname}`
|
||||
if (urlObject.port) address += ':' + urlObject.port
|
||||
return address
|
||||
if (protocolOverride) urlObject.protocol = protocolOverride
|
||||
return urlObject.href
|
||||
} catch (error) {
|
||||
console.error('Invalid URL', error)
|
||||
return null
|
||||
|
||||
@@ -39,9 +39,6 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
_author() {
|
||||
return this.author || {}
|
||||
},
|
||||
@@ -61,9 +58,9 @@ export default {
|
||||
if (!this.imagePath || !this.serverAddress) return null
|
||||
if (process.env.NODE_ENV !== 'production' && this.serverAddress.startsWith('http://192.168')) {
|
||||
// Testing
|
||||
return `http://localhost:3333/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||
return `http://localhost:3333/api/authors/${this.authorId}/image?ts=${this.updatedAt}`
|
||||
}
|
||||
return `${this.serverAddress}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||
return `${this.serverAddress}/api/authors/${this.authorId}/image?ts=${this.updatedAt}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -141,9 +141,6 @@ export default {
|
||||
},
|
||||
authorBottom() {
|
||||
return 0.75 * this.sizeMultiplier
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="w-full h-9 bg-bg relative z-20">
|
||||
<div id="bookshelf-toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-2">
|
||||
<div class="flex items-center w-full text-sm">
|
||||
<p v-show="!selectedSeriesName" class="pt-1">{{ totalEntities }} {{ entityTitle }}</p>
|
||||
<p v-show="selectedSeriesName" class="ml-2 pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
|
||||
<p v-show="!selectedSeriesName" class="pt-1">{{ $formatNumber(totalEntities) }} {{ entityTitle }}</p>
|
||||
<p v-show="selectedSeriesName" class="ml-2 pt-1">{{ selectedSeriesName }} ({{ $formatNumber(totalEntities) }})</p>
|
||||
<div class="flex-grow" />
|
||||
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="changeView">{{ !bookshelfListView ? 'view_list' : 'grid_view' }}</span>
|
||||
<template v-if="page === 'library'">
|
||||
@@ -13,6 +13,7 @@
|
||||
</div>
|
||||
<span class="material-icons px-2" @click="showSortModal = true">sort</span>
|
||||
</template>
|
||||
<span v-if="seriesBookPage" class="material-icons px-2" @click="downloadSeries">download</span>
|
||||
<span v-if="(page == 'library' && isBookLibrary) || seriesBookPage" class="material-icons px-2" @click="showMoreMenuDialog = true">more_vert</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,14 +65,14 @@ export default {
|
||||
return this.$route.query || {}
|
||||
},
|
||||
entityTitle() {
|
||||
if (this.isPodcast) return this.$strings.LabelPodcasts
|
||||
if (this.page === 'library') return this.$strings.LabelBooks
|
||||
else if (this.page === 'series') {
|
||||
if (this.page === 'library') {
|
||||
return this.isPodcast ? this.$strings.LabelPodcasts : this.$strings.LabelBooks
|
||||
} else if (this.page === 'playlists') {
|
||||
return this.$strings.ButtonPlaylists
|
||||
} else if (this.page === 'series') {
|
||||
return this.$strings.LabelSeries
|
||||
} else if (this.page === 'collections') {
|
||||
return this.$strings.ButtonCollections
|
||||
} else if (this.page === 'playlists') {
|
||||
return this.$strings.ButtonPlaylists
|
||||
} else if (this.page === 'authors') {
|
||||
return this.$strings.LabelAuthors
|
||||
}
|
||||
@@ -144,6 +145,10 @@ export default {
|
||||
async changeView() {
|
||||
this.bookshelfListView = !this.bookshelfListView
|
||||
await this.$hapticsImpact()
|
||||
},
|
||||
downloadSeries() {
|
||||
console.log('Download Series click')
|
||||
this.$eventBus.$emit('download-series-click')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -6,34 +6,34 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||
<div ref="container" class="w-full rounded-lg bg-primary border border-border overflow-y-auto overflow-x-hidden" style="max-height: 80vh" @click.stop.prevent>
|
||||
<div class="w-full h-full p-4" v-show="showBookmarkTitleInput">
|
||||
<div class="w-full rounded-lg bg-primary border border-border overflow-y-auto overflow-x-hidden relative mt-16" style="max-height: 80vh" @click.stop.prevent>
|
||||
<div class="w-full h-full p-4" v-if="showBookmarkTitleInput">
|
||||
<div class="flex mb-4 items-center">
|
||||
<div class="w-9 h-9 flex items-center justify-center rounded-full hover:bg-white hover:bg-opacity-10 cursor-pointer" @click.stop="showBookmarkTitleInput = false">
|
||||
<span class="material-icons text-3xl">arrow_back</span>
|
||||
</div>
|
||||
<p class="text-xl pl-2">{{ selectedBookmark ? 'Edit Bookmark' : 'New Bookmark' }}</p>
|
||||
<div class="flex-grow" />
|
||||
<p class="text-xl font-mono">{{ this.$secondsToTimestamp(currentTime) }}</p>
|
||||
<p class="text-xl font-mono">{{ this.$secondsToTimestamp(currentTime / _playbackRate) }}</p>
|
||||
</div>
|
||||
|
||||
<ui-text-input-with-label v-model="newBookmarkTitle" label="Note" />
|
||||
<ui-text-input-with-label v-model="newBookmarkTitle" ref="noteInput" label="Note" />
|
||||
<div class="flex justify-end mt-6">
|
||||
<ui-btn color="success" class="w-full" @click.stop="submitBookmark">{{ selectedBookmark ? 'Update' : 'Create' }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-full" v-show="!showBookmarkTitleInput">
|
||||
<div class="w-full h-full" v-else>
|
||||
<template v-for="bookmark in bookmarks">
|
||||
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @edit="editBookmark" @delete="deleteBookmark" />
|
||||
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" :playback-rate="_playbackRate" @click="clickBookmark" @edit="editBookmark" @delete="deleteBookmark" />
|
||||
</template>
|
||||
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
|
||||
<p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
|
||||
</div>
|
||||
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center justify-between border-b border-fg/10 bg-success cursor-pointer text-white text-opacity-80" @click.stop="createBookmark">
|
||||
<span class="material-icons">add</span>
|
||||
<p class="text-base pl-2">{{ $strings.ButtonCreateBookmark }}</p>
|
||||
<p class="text-sm font-mono">{{ this.$secondsToTimestamp(currentTime) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="canCreateBookmark && !showBookmarkTitleInput" class="flex px-4 py-2 items-center text-center justify-between border-b border-fg/10 bg-success cursor-pointer text-white text-opacity-80 sticky bottom-0 left-0 w-full" @click.stop="createBookmark">
|
||||
<span class="material-icons">add</span>
|
||||
<p class="text-base pl-2">{{ $strings.ButtonCreateBookmark }}</p>
|
||||
<p class="text-sm font-mono">{{ this.$secondsToTimestamp(currentTime / _playbackRate) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,6 +54,10 @@ export default {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
playbackRate: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
libraryItemId: String
|
||||
},
|
||||
data() {
|
||||
@@ -82,6 +86,10 @@ export default {
|
||||
},
|
||||
canCreateBookmark() {
|
||||
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
||||
},
|
||||
_playbackRate() {
|
||||
if (!this.playbackRate || isNaN(this.playbackRate)) return 1
|
||||
return this.playbackRate
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -151,6 +159,16 @@ export default {
|
||||
this.selectedBookmark = null
|
||||
this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm')
|
||||
this.showBookmarkTitleInput = true
|
||||
|
||||
// Auto focus the input and select the text
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.noteInput?.$refs.input?.$refs.input) {
|
||||
this.$refs.noteInput.$refs.input.$refs.input.focus()
|
||||
setTimeout(() => {
|
||||
this.$refs.noteInput?.$refs.input?.$refs.input?.select()
|
||||
}, 10)
|
||||
}
|
||||
})
|
||||
},
|
||||
async submitBookmark() {
|
||||
await this.$hapticsImpact()
|
||||
|
||||
@@ -210,7 +210,7 @@ export default {
|
||||
]
|
||||
},
|
||||
sublistItems() {
|
||||
return (this[this.sublist] || []).map((item) => {
|
||||
const sublistItems = (this[this.sublist] || []).map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
text: item,
|
||||
@@ -223,6 +223,13 @@ export default {
|
||||
}
|
||||
}
|
||||
})
|
||||
if (this.sublist === 'series') {
|
||||
sublistItems.unshift({
|
||||
text: this.$strings.MessageNoSeries,
|
||||
value: this.$encode('no-series')
|
||||
})
|
||||
}
|
||||
return sublistItems
|
||||
},
|
||||
filterData() {
|
||||
return this.$store.state.libraries.filterData || {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="modal modal-bg w-screen fixed bottom-0 left-0 flex items-center justify-center z-50" :class="halfScreen ? 'h-[50vh] min-h-[400px]' : 'h-screen'" @click.stop @touchstart.stop @touchend.stop>
|
||||
<div ref="wrapper" class="modal modal-bg w-screen fixed bottom-0 left-0 flex items-center justify-center z-50" :class="threeQuartersScreen ? 'h-[75vh] min-h-[400px]' : 'h-screen'" @click.stop @touchstart.stop @touchend.stop>
|
||||
<div ref="content" class="relative text-fg h-full w-full bg-bg">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
processing: Boolean,
|
||||
halfScreen: Boolean
|
||||
threeQuartersScreen: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -135,6 +135,14 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isConnectedToServer) {
|
||||
items.push({
|
||||
text: this.$strings.ButtonGoToWebClient,
|
||||
value: 'openWebClient',
|
||||
icon: 'language'
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.episode) {
|
||||
items.push({
|
||||
text: this.$strings.LabelMoreInfo,
|
||||
@@ -285,6 +293,8 @@ export default {
|
||||
this.clickRSSFeed()
|
||||
} else if (action === 'sendEbook') {
|
||||
this.showSendEbookDevicesModal = true
|
||||
} else if (action === 'openWebClient') {
|
||||
this.$store.dispatch('user/openWebClient', `/item/${this.serverLibraryItemId}`)
|
||||
}
|
||||
},
|
||||
async toggleFinished() {
|
||||
@@ -481,8 +491,8 @@ export default {
|
||||
this.$emit('update:processing', true)
|
||||
let url = `/api/playlists/${this.playlist.id}/item/${this.serverLibraryItemId}`
|
||||
if (this.serverEpisodeId) url += `/${this.serverEpisodeId}`
|
||||
this.$axios
|
||||
.$delete(url)
|
||||
this.$nativeHttp
|
||||
.delete(url)
|
||||
.then(() => {
|
||||
this.$toast.success('Item removed from playlist')
|
||||
})
|
||||
|
||||
@@ -63,6 +63,10 @@ export default {
|
||||
{
|
||||
text: this.$strings.LabelFileModified,
|
||||
value: 'mtimeMs'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelRandomly,
|
||||
value: 'random'
|
||||
}
|
||||
],
|
||||
podcastItems: [
|
||||
@@ -82,6 +86,10 @@ export default {
|
||||
text: this.$strings.LabelSize,
|
||||
value: 'size'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelNumberOfEpisodes,
|
||||
value: 'media.numTracks'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelFileBirthtime,
|
||||
value: 'birthtimeMs'
|
||||
@@ -89,6 +97,10 @@ export default {
|
||||
{
|
||||
text: this.$strings.LabelFileModified,
|
||||
value: 'mtimeMs'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelRandomly,
|
||||
value: 'random'
|
||||
}
|
||||
],
|
||||
episodeItems: [
|
||||
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
}
|
||||
},
|
||||
rates() {
|
||||
return [0.5, 1, 1.2, 1.5, 2, 3]
|
||||
return [0.5, 1, 1.2, 1.5, 1.7, 2, 3]
|
||||
},
|
||||
canIncrement() {
|
||||
return this.playbackRate + 0.1 <= this.MAX_SPEED
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<p v-if="episode.episode" class="font-semibold text-gray-200 text-xs">#{{ episode.episode }}</p>
|
||||
<p class="break-words mb-1 text-sm">{{ episode.title }}</p>
|
||||
<p v-if="episode.subtitle" class="break-words mb-1 text-xs text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
|
||||
<p class="text-xxs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
||||
<p class="text-xxs text-gray-300">{{ $getString('LabelPublishedDate', [episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : $strings.LabelUnknown]) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,13 +6,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center"
|
||||
@click="
|
||||
show = false
|
||||
manualTimerModal = false
|
||||
"
|
||||
>
|
||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-border" style="max-height: 75%" @click.stop>
|
||||
<div v-if="manualTimerModal" class="p-4">
|
||||
<div class="flex mb-4" @click="manualTimerModal = false">
|
||||
@@ -71,7 +65,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
manualTimerModal: null,
|
||||
manualTimerModal: false,
|
||||
manualTimeoutMin: 1
|
||||
}
|
||||
},
|
||||
@@ -81,6 +75,9 @@ export default {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
if (!val) {
|
||||
this.manualTimerModal = false
|
||||
}
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :key="bookmark.id" :id="`bookmark-row-${bookmark.id}`" class="flex items-center px-1 py-4 justify-start relative" :class="highlight ? 'bg-bg bg-opacity-60' : ' bg-opacity-20'" @click="click">
|
||||
<div :key="bookmark.time" :id="`bookmark-row-${bookmark.time}`" class="flex items-center px-1 py-4 justify-start relative" :class="highlight ? 'bg-bg bg-opacity-60' : ' bg-opacity-20'" @click="click">
|
||||
<div class="flex-grow overflow-hidden px-2">
|
||||
<div class="flex items-center mb-0.5">
|
||||
<i class="material-icons text-lg pr-1 -mb-1" :class="highlight ? 'text-success' : 'text-fg-muted'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</i>
|
||||
@@ -7,7 +7,7 @@
|
||||
{{ bookmark.title }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm font-mono text-fg-muted flex items-center"><span class="material-icons text-base pl-px pr-1">schedule</span>{{ $secondsToTimestamp(bookmark.time) }}</p>
|
||||
<p class="text-sm font-mono text-fg-muted flex items-center"><span class="material-icons text-base pl-px pr-1">schedule</span>{{ $secondsToTimestamp(bookmark.time / playbackRate) }}</p>
|
||||
</div>
|
||||
<div class="h-full flex items-center justify-end transform w-16 pr-2" @click.stop>
|
||||
<span class="material-icons text-2xl mr-2 text-fg hover:text-yellow-400" @click.stop="editClick">edit</span>
|
||||
@@ -23,7 +23,8 @@ export default {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
highlight: Boolean
|
||||
highlight: Boolean,
|
||||
playbackRate: Number
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
@@ -39,7 +40,6 @@ export default {
|
||||
editClick() {
|
||||
this.$emit('edit', this.bookmark)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -349,7 +349,7 @@ export default {
|
||||
}
|
||||
},
|
||||
parseFilenames(filenames) {
|
||||
const acceptableImages = ['.jpeg', '.jpg', '.png']
|
||||
const acceptableImages = ['.jpeg', '.jpg', '.png', '.webp']
|
||||
var imageFiles = filenames.filter((f) => {
|
||||
return acceptableImages.includes((Path.extname(f) || '').toLowerCase())
|
||||
})
|
||||
|
||||
@@ -34,16 +34,18 @@ export default {
|
||||
currentLocationNum: 0,
|
||||
currentLocationCfi: null,
|
||||
inittingDisplay: true,
|
||||
isRefreshingUI: false,
|
||||
ereaderSettings: {
|
||||
theme: 'dark',
|
||||
fontScale: 100,
|
||||
lineSpacing: 115
|
||||
lineSpacing: 115,
|
||||
textStroke: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isPlayerOpen() {
|
||||
this.updateHeight()
|
||||
this.refreshUI()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -112,7 +114,8 @@ export default {
|
||||
'*': {
|
||||
color: `${fontColor}!important`,
|
||||
'background-color': `${backgroundColor}!important`,
|
||||
'line-height': this.ereaderSettings.lineSpacing + '%!important'
|
||||
'line-height': this.ereaderSettings.lineSpacing + '%!important',
|
||||
'-webkit-text-stroke': this.ereaderSettings.textStroke/100 + 'px ' + fontColor + '!important'
|
||||
},
|
||||
a: {
|
||||
color: `${fontColor}!important`
|
||||
@@ -135,11 +138,6 @@ export default {
|
||||
goToChapter(href) {
|
||||
return this.rendition?.display(href)
|
||||
},
|
||||
updateHeight() {
|
||||
if (this.rendition && this.rendition.resize) {
|
||||
this.rendition.resize(window.innerWidth, window.innerHeight - this.readerHeightOffset)
|
||||
}
|
||||
},
|
||||
prev() {
|
||||
if (this.rendition) {
|
||||
this.rendition.prev()
|
||||
@@ -382,14 +380,54 @@ export default {
|
||||
this.rendition.getContents().forEach((c) => {
|
||||
c.addStylesheetRules(this.themeRules)
|
||||
})
|
||||
},
|
||||
async screenOrientationChange() {
|
||||
if (this.isRefreshingUI) return
|
||||
this.isRefreshingUI = true
|
||||
const windowWidth = window.innerWidth
|
||||
this.refreshUI()
|
||||
|
||||
// Window width does not always change right away. Wait up to 250ms for a change.
|
||||
// iPhone 10 on iOS 16 took between 100 - 200ms to update when going from portrait to landscape
|
||||
// but landscape to portrait was immediate
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
if (window.innerWidth !== windowWidth) {
|
||||
this.refreshUI()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.isRefreshingUI = false
|
||||
},
|
||||
refreshUI() {
|
||||
if (this.rendition?.resize) {
|
||||
this.rendition.resize(window.innerWidth, window.innerHeight - this.readerHeightOffset)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.book?.destroy()
|
||||
},
|
||||
mounted() {
|
||||
this.initEpub()
|
||||
}
|
||||
|
||||
if (screen.orientation) {
|
||||
// Not available on ios
|
||||
screen.orientation.addEventListener('change', this.screenOrientationChange)
|
||||
} else {
|
||||
document.addEventListener('orientationchange', this.screenOrientationChange)
|
||||
}
|
||||
window.addEventListener('resize', this.screenOrientationChange)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.book?.destroy()
|
||||
|
||||
if (screen.orientation) {
|
||||
// Not available on ios
|
||||
screen.orientation.removeEventListener('change', this.screenOrientationChange)
|
||||
} else {
|
||||
document.removeEventListener('orientationchange', this.screenOrientationChange)
|
||||
}
|
||||
window.removeEventListener('resize', this.screenOrientationChange)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -404,4 +442,4 @@ export default {
|
||||
max-height: calc(100% - 132px);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="w-full h-full overflow-auto">
|
||||
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
|
||||
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md bg-white" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="loadedRatio = $event" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf>
|
||||
<pdf v-if="pdfDocInitParams" ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md bg-white" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="loadedRatio = $event" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,8 @@ export default {
|
||||
page: 1,
|
||||
numPages: 0,
|
||||
windowWidth: 0,
|
||||
windowHeight: 0
|
||||
windowHeight: 0,
|
||||
pdfDocInitParams: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -106,14 +107,6 @@ export default {
|
||||
if (!this.userItemProgress?.ebookLocation || isNaN(this.userItemProgress.ebookLocation)) return 0
|
||||
return Number(this.userItemProgress.ebookLocation)
|
||||
},
|
||||
pdfDocInitParams() {
|
||||
return {
|
||||
url: this.url,
|
||||
httpHeaders: {
|
||||
Authorization: `Bearer ${this.userToken}`
|
||||
}
|
||||
}
|
||||
},
|
||||
isPlayerOpen() {
|
||||
return this.$store.getters['getIsPlayerOpen']
|
||||
}
|
||||
@@ -157,6 +150,8 @@ export default {
|
||||
}
|
||||
},
|
||||
numPagesLoaded(e) {
|
||||
if (!e) return
|
||||
|
||||
this.numPages = e
|
||||
},
|
||||
prev() {
|
||||
@@ -175,6 +170,14 @@ export default {
|
||||
screenOrientationChange() {
|
||||
this.windowWidth = window.innerWidth
|
||||
this.windowHeight = window.innerHeight
|
||||
},
|
||||
init() {
|
||||
this.pdfDocInitParams = {
|
||||
url: this.url,
|
||||
httpHeaders: {
|
||||
Authorization: `Bearer ${this.userToken}`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -188,6 +191,8 @@ export default {
|
||||
document.addEventListener('orientationchange', this.screenOrientationChange)
|
||||
}
|
||||
window.addEventListener('resize', this.screenOrientationChange)
|
||||
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (screen.orientation) {
|
||||
@@ -199,4 +204,4 @@ export default {
|
||||
window.removeEventListener('resize', this.screenOrientationChange)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -54,39 +54,63 @@
|
||||
</modals-fullscreen-modal>
|
||||
|
||||
<!-- ereader settings modal -->
|
||||
<modals-fullscreen-modal v-model="showSettingsModal" :theme="ereaderTheme" half-screen>
|
||||
<modals-fullscreen-modal v-model="showSettingsModal" :theme="ereaderTheme" threeQuartersScreen>
|
||||
<div style="box-shadow: 0px -8px 8px #11111155">
|
||||
<div class="flex items-end justify-between h-20 px-4 pb-2 mb-6">
|
||||
<div class="flex items-end justify-between h-14 px-4 pb-2 mb-6">
|
||||
<h1 class="text-lg">{{ $strings.HeaderEreaderSettings }}</h1>
|
||||
<button class="flex" @click="showSettingsModal = false">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(100vh-85px)]">
|
||||
<div class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(75vh-85px)]">
|
||||
<div class="w-full h-full px-4">
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-32">
|
||||
<p class="text-base">{{ $strings.LabelTheme }}:</p>
|
||||
<p class="text-sm">{{ $strings.LabelTheme }}</p>
|
||||
</div>
|
||||
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" />
|
||||
<ui-toggle-btns v-model="ereaderSettings.theme" name="theme" :items="themeItems" @input="settingsUpdated" />
|
||||
</div>
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-32">
|
||||
<p class="text-base">{{ $strings.LabelFontScale }}:</p>
|
||||
<p class="text-sm">{{ $strings.LabelFontScale }}</p>
|
||||
</div>
|
||||
<ui-range-input v-model="ereaderSettings.fontScale" :min="5" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
|
||||
</div>
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-32">
|
||||
<p class="text-base">{{ $strings.LabelLineSpacing }}:</p>
|
||||
<p class="text-sm">{{ $strings.LabelLineSpacing }}</p>
|
||||
</div>
|
||||
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-32">
|
||||
<p class="text-base">{{ $strings.LabelLayout }}:</p>
|
||||
<p class="text-sm">{{ $strings.LabelFontBoldness }}</p>
|
||||
</div>
|
||||
<ui-toggle-btns v-model="ereaderSettings.spread" :items="spreadItems" @input="settingsUpdated" />
|
||||
<ui-range-input v-model="ereaderSettings.textStroke" :min="0" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
|
||||
</div>
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-32">
|
||||
<p class="text-sm">{{ $strings.LabelLayout }}</p>
|
||||
</div>
|
||||
<ui-toggle-btns v-model="ereaderSettings.spread" name="spread" :items="spreadItems" @input="settingsUpdated" />
|
||||
</div>
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-32">
|
||||
<p class="text-sm">{{ $strings.LabelNavigateWithVolume }}</p>
|
||||
</div>
|
||||
<ui-toggle-btns v-model="ereaderSettings.navigateWithVolume" name="navigate-volume" :items="navigateWithVolumeItems" @input="settingsUpdated" />
|
||||
</div>
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-32">
|
||||
<p class="text-sm">{{ $strings.LabelNavigateWithVolumeWhilePlaying }}</p>
|
||||
</div>
|
||||
<ui-toggle-btns v-model="ereaderSettings.navigateWithVolumeWhilePlaying" name="navigate-volume-playing" :items="onOffToggleButtonItems" @input="settingsUpdated" />
|
||||
</div>
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-32">
|
||||
<p class="text-sm">{{ $strings.LabelKeepScreenAwake }}</p>
|
||||
</div>
|
||||
<ui-toggle-btns v-model="ereaderSettings.keepScreenAwake" name="keep-awake" :items="onOffToggleButtonItems" @input="settingsUpdated" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,6 +121,8 @@
|
||||
|
||||
<script>
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { VolumeButtons } from '@capacitor-community/volume-buttons'
|
||||
import { KeepAwake } from '@capacitor-community/keep-awake'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -112,11 +138,16 @@ export default {
|
||||
showSettingsModal: false,
|
||||
comicHasMetadata: false,
|
||||
chapters: [],
|
||||
isInittingWatchVolume: false,
|
||||
ereaderSettings: {
|
||||
theme: 'dark',
|
||||
fontScale: 100,
|
||||
lineSpacing: 115,
|
||||
spread: 'auto'
|
||||
spread: 'auto',
|
||||
textStroke: 0,
|
||||
navigateWithVolume: 'enabled',
|
||||
navigateWithVolumeWhilePlaying: false,
|
||||
keepScreenAwake: false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -127,13 +158,17 @@ export default {
|
||||
this.comicHasMetadata = false
|
||||
this.registerListeners()
|
||||
this.hideToolbar()
|
||||
|
||||
console.log('showReader for ebookFile', JSON.stringify(this.ebookFile))
|
||||
} else {
|
||||
this.unregisterListeners()
|
||||
this.$showHideStatusBar(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
isPlayerOpen(newVal, oldVal) {
|
||||
// Closed player
|
||||
if (!newVal && oldVal) {
|
||||
this.initWatchVolume()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -173,6 +208,34 @@ export default {
|
||||
}
|
||||
]
|
||||
},
|
||||
navigateWithVolumeItems() {
|
||||
return [
|
||||
{
|
||||
text: this.$strings.LabelOn,
|
||||
value: 'enabled'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelNavigateWithVolumeMirrored,
|
||||
value: 'mirrored'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelOff,
|
||||
value: 'none'
|
||||
}
|
||||
]
|
||||
},
|
||||
onOffToggleButtonItems() {
|
||||
return [
|
||||
{
|
||||
text: this.$strings.LabelOn,
|
||||
value: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelOff,
|
||||
value: false
|
||||
}
|
||||
]
|
||||
},
|
||||
themeItems() {
|
||||
return [
|
||||
{
|
||||
@@ -257,6 +320,9 @@ export default {
|
||||
settingsUpdated() {
|
||||
this.$refs.readerComponent?.updateSettings?.(this.ereaderSettings)
|
||||
localStorage.setItem('ereaderSettings', JSON.stringify(this.ereaderSettings))
|
||||
|
||||
this.initWatchVolume()
|
||||
this.initKeepScreenAwake()
|
||||
},
|
||||
goToChapter(href) {
|
||||
this.showTOCModal = false
|
||||
@@ -369,31 +435,88 @@ export default {
|
||||
try {
|
||||
const settings = localStorage.getItem('ereaderSettings')
|
||||
if (settings) {
|
||||
const ereaderSettings = JSON.parse(settings)
|
||||
if (!ereaderSettings.spread) {
|
||||
// Added in 0.9.71
|
||||
ereaderSettings.spread = 'auto'
|
||||
const _ereaderSettings = JSON.parse(settings)
|
||||
for (const key in this.ereaderSettings) {
|
||||
if (_ereaderSettings[key] !== undefined) {
|
||||
this.ereaderSettings[key] = _ereaderSettings[key]
|
||||
}
|
||||
}
|
||||
this.ereaderSettings = ereaderSettings
|
||||
this.settingsUpdated()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load ereader settings', error)
|
||||
}
|
||||
},
|
||||
async initWatchVolume() {
|
||||
if (this.isInittingWatchVolume || !this.isEpub) return
|
||||
this.isInittingWatchVolume = true
|
||||
const isWatching = await VolumeButtons.isWatching()
|
||||
|
||||
if (this.ereaderSettings.navigateWithVolume !== 'none' && (this.ereaderSettings.navigateWithVolumeWhilePlaying || !this.isPlayerOpen)) {
|
||||
if (!isWatching.value) {
|
||||
const options = {
|
||||
disableSystemVolumeHandler: true,
|
||||
suppressVolumeIndicator: true
|
||||
}
|
||||
await VolumeButtons.watchVolume(options, this.volumePressed)
|
||||
}
|
||||
} else if (isWatching.value) {
|
||||
await VolumeButtons.clearWatch().catch((error) => {
|
||||
console.error('Failed to clear volume watch', error)
|
||||
})
|
||||
}
|
||||
|
||||
this.isInittingWatchVolume = false
|
||||
},
|
||||
async initKeepScreenAwake() {
|
||||
try {
|
||||
if (this.ereaderSettings.keepScreenAwake) {
|
||||
await KeepAwake.keepAwake()
|
||||
console.log('Reader keep screen awake enabled')
|
||||
} else {
|
||||
await KeepAwake.allowSleep()
|
||||
console.log('Reader keep screen awake disabled')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to init keep screen awake', error)
|
||||
}
|
||||
},
|
||||
registerListeners() {
|
||||
this.$eventBus.$on('close-ebook', this.closeEvt)
|
||||
document.body.addEventListener('touchstart', this.touchstart)
|
||||
document.body.addEventListener('touchend', this.touchend)
|
||||
this.initWatchVolume()
|
||||
this.initKeepScreenAwake()
|
||||
},
|
||||
unregisterListeners() {
|
||||
this.$eventBus.$on('close-ebook', this.closeEvt)
|
||||
document.body.removeEventListener('touchstart', this.touchstart)
|
||||
document.body.removeEventListener('touchend', this.touchend)
|
||||
VolumeButtons.clearWatch().catch((error) => {
|
||||
console.error('Failed to clear volume watch', error)
|
||||
})
|
||||
KeepAwake.allowSleep().catch((error) => {
|
||||
console.error('Failed to allow sleep', error)
|
||||
})
|
||||
},
|
||||
volumePressed(e) {
|
||||
if (this.ereaderSettings.navigateWithVolume == 'enabled') {
|
||||
if (e.direction == 'up') {
|
||||
this.prev()
|
||||
} else {
|
||||
this.next()
|
||||
}
|
||||
} else if (this.ereaderSettings.navigateWithVolume == 'mirrored') {
|
||||
if (e.direction == 'down') {
|
||||
this.prev()
|
||||
} else {
|
||||
this.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.unregisterListeners()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||