Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1bd679046 | ||
|
|
34cfeb3250 | ||
|
|
6bbcb979c3 | ||
|
|
5ef621d26a | ||
|
|
d5bc33a280 | ||
|
|
d7c1c6bca6 | ||
|
|
4b968f3bed | ||
|
|
ffd46f1d29 | ||
|
|
70b4edcbb7 | ||
|
|
96d8bfdb73 | ||
|
|
bdb1c8ef1b | ||
|
|
ce66536dde | ||
|
|
829e1906d6 | ||
|
|
8f1153b05b | ||
|
|
92db1a4ce5 | ||
|
|
4ca4fe81f0 | ||
|
|
d00cf77d1d | ||
|
|
4d6eb9ff1d | ||
|
|
022de71c62 | ||
|
|
0fde488dde | ||
|
|
76c758d027 | ||
|
|
b847feae22 | ||
|
|
5a4efa33cf | ||
|
|
fb45ed99c2 | ||
|
|
9967a534bd | ||
|
|
9745243cda | ||
|
|
ab10e04c72 | ||
|
|
d9ebe79ec7 | ||
|
|
e2774e48cd | ||
|
|
a6c476a97a | ||
|
|
a4313bc401 | ||
|
|
e8b313605f | ||
|
|
c1a56e59dd | ||
|
|
e909f6e2bd | ||
|
|
8becbb7f53 | ||
|
|
81b4da3584 | ||
|
|
ea6f633d9b | ||
|
|
09dc4fc87b | ||
|
|
85a7d0df84 | ||
|
|
a602a22eb0 | ||
|
|
7a9cc78264 | ||
|
|
8152f4db96 | ||
|
|
f316a6ebaa | ||
|
|
ab1a9daefb | ||
|
|
bbf3261815 | ||
|
|
45715a6c45 | ||
|
|
635b508795 | ||
|
|
24f31f6fa2 | ||
|
|
73708a52d9 | ||
|
|
ef37928981 | ||
|
|
8f2fe76706 | ||
|
|
1c2deee063 | ||
|
|
fcdced1a00 | ||
|
|
ff17ab3e9b | ||
|
|
5458e174e6 | ||
|
|
7505832fb9 | ||
|
|
b3bd8c8db1 | ||
|
|
407ab3d17a | ||
|
|
d0a9d80341 | ||
|
|
2cd44707b7 | ||
|
|
2690525ab5 | ||
|
|
5dd467cb84 | ||
|
|
1641c896d7 | ||
|
|
6c702d4556 | ||
|
|
457ffc2066 | ||
|
|
7f171594f3 | ||
|
|
4cab5459dd | ||
|
|
0c31b66116 | ||
|
|
343e3d3bc7 | ||
|
|
5c8e6841ee | ||
|
|
f2ccc339aa | ||
|
|
eec16bdfbd | ||
|
|
0675a6addb | ||
|
|
72ec208253 | ||
|
|
54f293b0e7 | ||
|
|
36537a1edf | ||
|
|
c38ed8ff51 | ||
|
|
06a03157d3 | ||
|
|
e5ac0394b9 | ||
|
|
bc3ece6ca8 | ||
|
|
4367a2821f | ||
|
|
baa60254f0 | ||
|
|
bc9f9bdac4 | ||
|
|
f1d497a70c | ||
|
|
e1a6ccd9d0 | ||
|
|
b888e127d2 | ||
|
|
5fad9ccc45 | ||
|
|
92a0cb70b4 | ||
|
|
55733e47af | ||
|
|
843843e735 | ||
|
|
6f3685ec11 | ||
|
|
47cc883f25 | ||
|
|
7a0d8e785c | ||
|
|
3276c0e072 | ||
|
|
6772303bf5 | ||
|
|
561a651dc4 | ||
|
|
65c1122bb1 | ||
|
|
e57d0138a6 | ||
|
|
cac7614bc0 | ||
|
|
8414567a63 | ||
|
|
3b18b74f3a | ||
|
|
e7254ab764 | ||
|
|
2d1e1ece4d | ||
|
|
7af22a422e | ||
|
|
34034bee34 | ||
|
|
f398e9b27b | ||
|
|
b758f7843d | ||
|
|
8a9b7d5a6c | ||
|
|
b3b8e7f509 | ||
|
|
a083230d06 | ||
|
|
67f1c935ec | ||
|
|
f14052800e | ||
|
|
c409b72beb | ||
|
|
cbdb6c0934 | ||
|
|
8fc008bce9 | ||
|
|
654eb39877 | ||
|
|
8e67be7a35 | ||
|
|
45d19a669e | ||
|
|
d0cbe2a27e | ||
|
|
d86fda521f | ||
|
|
be666be9a6 | ||
|
|
1fe259a6f2 | ||
|
|
37c3c1cb84 | ||
|
|
2b35f6bb9a | ||
|
|
308489dc6c | ||
|
|
b0bd790141 | ||
|
|
53c96d21dd | ||
|
|
acc21e207f | ||
|
|
a7c55544c3 | ||
|
|
b621a8ebd0 | ||
|
|
b431832c83 | ||
|
|
828a8bb212 | ||
|
|
e4bcb276df | ||
|
|
4b5e4a43a3 | ||
|
|
216fee6757 | ||
|
|
8e66ed043d | ||
|
|
1f60cf7d1c | ||
|
|
02b61cd36a | ||
|
|
70e0b32265 | ||
|
|
65c6f8ff52 | ||
|
|
f37590f088 | ||
|
|
e5ea28ea34 | ||
|
|
a77e972b82 | ||
|
|
56c4974803 | ||
|
|
6ec8fe035c | ||
|
|
9287777b87 | ||
|
|
1abf793e19 | ||
|
|
e44b743016 | ||
|
|
789dae907a | ||
|
|
de0d7b28f8 | ||
|
|
19248c3a30 | ||
|
|
cdb72b7789 | ||
|
|
4a51dc8d64 | ||
|
|
388014d62b | ||
|
|
f5956aa44d | ||
|
|
1e09ce7a65 | ||
|
|
24990cde08 | ||
|
|
92ebb82a6a | ||
|
|
ad6d023f46 | ||
|
|
b0d5f65dfe | ||
|
|
50598cc3e7 | ||
|
|
b3c1985d45 | ||
|
|
ad2bc470a1 | ||
|
|
88ef01f129 | ||
|
|
5da858aa4c | ||
|
|
e50a8e8d68 | ||
|
|
b4069af1c7 | ||
|
|
31fbc8c95c | ||
|
|
cf5410622f | ||
|
|
ef09b241ea | ||
|
|
a08109d745 | ||
|
|
d86d12f2db | ||
|
|
bc9ad69260 | ||
|
|
314aba6d65 | ||
|
|
995eba2c11 | ||
|
|
4e73075728 | ||
|
|
f785031ad8 | ||
|
|
9c10cb02b8 | ||
|
|
2ab4001a43 | ||
|
|
94afe3c15f | ||
|
|
d48d1c44ea | ||
|
|
0fbee213a3 | ||
|
|
d2c2c73eeb | ||
|
|
db07a12be5 | ||
|
|
8448910628 | ||
|
|
a33fc389e3 | ||
|
|
5637d46488 | ||
|
|
6d71154589 | ||
|
|
b6540b10b9 | ||
|
|
a182265077 | ||
|
|
0e11815206 | ||
|
|
86455a2d46 | ||
|
|
abc5475bea | ||
|
|
cabf13fb3a | ||
|
|
79b03a4907 | ||
|
|
fcd27863a1 | ||
|
|
e3c5097035 | ||
|
|
b9c253733e | ||
|
|
1d8c4a42a8 | ||
|
|
48f45beeeb | ||
|
|
5c7f38c2dc | ||
|
|
c4e3b2f6dd | ||
|
|
f26e8be3f4 | ||
|
|
a42012a958 | ||
|
|
659676adc2 | ||
|
|
2cb30deeda | ||
|
|
ff3b4c7a27 | ||
|
|
1171fb17e6 | ||
|
|
0052312459 | ||
|
|
826347023b | ||
|
|
f2ea2993cc | ||
|
|
be95d0c198 | ||
|
|
f6d8d3c536 | ||
|
|
1abbd8baba | ||
|
|
cad53dcce5 | ||
|
|
4016c22289 | ||
|
|
56fd18be75 | ||
|
|
00c1977f87 | ||
|
|
b19a9267f6 | ||
|
|
5783210873 | ||
|
|
f242ffb2af | ||
|
|
60bf7ecde7 | ||
|
|
c52f7812f1 | ||
|
|
44d220b74c | ||
|
|
13b3abdcd4 | ||
|
|
282da93630 | ||
|
|
ccbcea162d | ||
|
|
5f227b2327 | ||
|
|
b233129c32 | ||
|
|
01ffc49e18 | ||
|
|
4a40192647 | ||
|
|
08e8993101 | ||
|
|
3be1d2ad87 | ||
|
|
1408bee041 | ||
|
|
9bf5ee90ef | ||
|
|
9e3f718ab9 | ||
|
|
b37707d933 | ||
|
|
ae1018f8c5 | ||
|
|
faa6fbcc39 | ||
|
|
2b68bd82a4 | ||
|
|
88f44bd63b | ||
|
|
ced3aa44f1 | ||
|
|
6f1a488b92 | ||
|
|
b229f1b1dc | ||
|
|
88a309712d | ||
|
|
3d93f9b04b | ||
|
|
f37682c154 | ||
|
|
4cd00a4b44 | ||
|
|
ce8d9f09fa | ||
|
|
82707f8347 | ||
|
|
eae231722f | ||
|
|
f269c0ae08 | ||
|
|
840361707a | ||
|
|
dbab82c533 | ||
|
|
fa605f5416 | ||
|
|
1983f1e864 | ||
|
|
cfc2680786 | ||
|
|
7eb7ee81ba | ||
|
|
420f792290 | ||
|
|
733fb5d96c | ||
|
|
640b64a407 | ||
|
|
0388010c8b | ||
|
|
ab6816be62 | ||
|
|
1d3278f837 | ||
|
|
ede879c550 | ||
|
|
ddc504eb05 | ||
|
|
8bf42fa46a | ||
|
|
879cb951a0 | ||
|
|
3939706f42 | ||
|
|
f5c3a950bf | ||
|
|
3021f96465 | ||
|
|
117c063f78 | ||
|
|
10232720ae | ||
|
|
d2feeedec1 | ||
|
|
42f2b25f5c | ||
|
|
c5360f264f | ||
|
|
9517531388 | ||
|
|
be282f3ac7 | ||
|
|
c5fafba7b5 | ||
|
|
704e59afdf | ||
|
|
be6e0c9498 | ||
|
|
2a543d3aad | ||
|
|
697e806dd8 | ||
|
|
a28d5085bb | ||
|
|
4cac015176 | ||
|
|
ae1770e4ac | ||
|
|
a5a710cbb1 | ||
|
|
3d7172d664 | ||
|
|
68431d4a25 | ||
|
|
5d5f602e80 | ||
|
|
aa6256b2d4 | ||
|
|
17ff1b4217 | ||
|
|
7709665538 | ||
|
|
00d15f5475 | ||
|
|
23c4ca5d3f | ||
|
|
c048ecf9cf | ||
|
|
d5e51366e9 | ||
|
|
94eb5ce091 | ||
|
|
055290f075 | ||
|
|
008c2171ff | ||
|
|
835d5aee82 | ||
|
|
bdf5cb7a2e | ||
|
|
81bf27c13e | ||
|
|
8c8af56e67 | ||
|
|
418d1b22d6 | ||
|
|
607e93e180 | ||
|
|
08adc87493 | ||
|
|
d42adbf85d | ||
|
|
6dc17f51ce | ||
|
|
87e0ede2be | ||
|
|
330c3a6d22 | ||
|
|
6b53f132d5 | ||
|
|
08c511f854 | ||
|
|
00c6cbd7a6 | ||
|
|
5718628d72 | ||
|
|
0ca4aadfb5 | ||
|
|
8e2d8fb89d | ||
|
|
ce34b1173d | ||
|
|
790e785dd3 | ||
|
|
97ac1033eb | ||
|
|
eee0737a1c | ||
|
|
83af1e1c53 | ||
|
|
4585757ac2 | ||
|
|
5990d27a07 | ||
|
|
f80c3a0090 | ||
|
|
385fdd1fae | ||
|
|
3a75dd75b5 | ||
|
|
fc3238a618 | ||
|
|
f7c6330910 | ||
|
|
e53471f85e | ||
|
|
4423d0f17f | ||
|
|
f10e2b79ac | ||
|
|
777ee3d408 | ||
|
|
e440ac088b | ||
|
|
7e7609ca1f | ||
|
|
fb2005fd83 | ||
|
|
7b87792d8a | ||
|
|
9d684536a1 | ||
|
|
75888c5ba6 | ||
|
|
f76628574a | ||
|
|
b2d6dd6897 | ||
|
|
b577ab01ae | ||
|
|
ad75065848 | ||
|
|
cf6ff4c315 | ||
|
|
58116fb451 | ||
|
|
f15f609832 | ||
|
|
799dd5adbe | ||
|
|
582fb5ba93 | ||
|
|
0f03ac28d8 | ||
|
|
4418404a71 | ||
|
|
8427cc52d8 | ||
|
|
d6bda5c3d7 | ||
|
|
9a79f3ba40 | ||
|
|
d08855585a | ||
|
|
6c523d006b | ||
|
|
9185c0e36b | ||
|
|
3013dcd9f3 | ||
|
|
ce7abc7afb | ||
|
|
f5bc2ebb59 | ||
|
|
5714ac881e | ||
|
|
160ebcf7f5 | ||
|
|
8638868fb7 | ||
|
|
883c27885f | ||
|
|
c2ee9032b9 | ||
|
|
ebc75bddca | ||
|
|
5971163361 | ||
|
|
639f814ef8 | ||
|
|
1733b3124d | ||
|
|
f3e632078a | ||
|
|
b174c20faf | ||
|
|
5d74c6787f | ||
|
|
095cb8d3a1 | ||
|
|
f745ff3e91 | ||
|
|
8b976cee3c | ||
|
|
6e77a5e94b | ||
|
|
c8052be29a | ||
|
|
7d83b9d86a | ||
|
|
b977113e34 | ||
|
|
027e3410d8 | ||
|
|
282cbf301a | ||
|
|
75a83606f2 | ||
|
|
cff16d17b5 | ||
|
|
859ab94104 | ||
|
|
b6390003b6 | ||
|
|
c4514ad4cd | ||
|
|
e7360e07ab | ||
|
|
875e4ea32c | ||
|
|
72d51d5c52 | ||
|
|
3d5ad28eb0 | ||
|
|
7bb8c5b8f4 | ||
|
|
a917917166 | ||
|
|
5c39c481b7 | ||
|
|
66a2e9ef69 | ||
|
|
41c64f4b02 | ||
|
|
be58017cac | ||
|
|
d82228483d | ||
|
|
8f249ec2a2 | ||
|
|
5815afafb7 | ||
|
|
6fd45a2608 | ||
|
|
f30fd72778 | ||
|
|
b455724b1c | ||
|
|
ddafaa60eb | ||
|
|
9bf7a97e55 | ||
|
|
7afff19fce | ||
|
|
9f88bf15ab | ||
|
|
25b3ad4f8e | ||
|
|
91cfb0b2d2 | ||
|
|
7da2cfd456 | ||
|
|
97eaf20dbc | ||
|
|
aff52e7d5f | ||
|
|
1a6174b1c8 | ||
|
|
98866ee407 | ||
|
|
703957e2b7 | ||
|
|
baada58e8c | ||
|
|
885c2a5865 | ||
|
|
5fef8a4d51 | ||
|
|
40c4c425f6 | ||
|
|
c0e0e53ee7 | ||
|
|
2029ceeee0 | ||
|
|
a9999aae2e | ||
|
|
b5a4787424 | ||
|
|
2b39717cab | ||
|
|
8f53a39f63 | ||
|
|
020976e257 | ||
|
|
cdfa20b9a7 | ||
|
|
b87e242de2 | ||
|
|
147f2d23a5 | ||
|
|
0fcf584b01 | ||
|
|
43361b3a2a | ||
|
|
df1db3d0ff | ||
|
|
2a4c58a680 | ||
|
|
0c1b20d3e7 | ||
|
|
9ce33bba75 | ||
|
|
23ec6ea9b1 | ||
|
|
1946b1c333 | ||
|
|
1251ae3bc0 | ||
|
|
6b3b35d4df | ||
|
|
9e943c82b9 | ||
|
|
9c4c0b1e56 | ||
|
|
a5538ece8e | ||
|
|
092599d167 | ||
|
|
af72415952 | ||
|
|
a2c058877a | ||
|
|
d4ecf97436 | ||
|
|
49d9ceb73a | ||
|
|
7c8cbc4b3f | ||
|
|
ff4a2a751f | ||
|
|
6fdf1d0ca0 | ||
|
|
8303159abf | ||
|
|
0a6371b159 | ||
|
|
c835a59cbf | ||
|
|
32bfdde04e | ||
|
|
f9fcb089a1 | ||
|
|
3bcdcbdac7 | ||
|
|
4f9d0c09ef | ||
|
|
ab3f112981 | ||
|
|
740d9e432a | ||
|
|
bca96ae215 | ||
|
|
b18de72bdd | ||
|
|
62d7590501 | ||
|
|
f3b02e8fc2 | ||
|
|
f17becff10 | ||
|
|
042b8f60d5 | ||
|
|
2dfcc4de8f | ||
|
|
33b238c646 | ||
|
|
a7bd050013 | ||
|
|
5ab5d1ece7 | ||
|
|
07930d416d | ||
|
|
929816c342 | ||
|
|
5cee5ad5d3 | ||
|
|
9d2a0acfcf | ||
|
|
b2719a263f | ||
|
|
3eead80cb4 | ||
|
|
9efc9d53a5 | ||
|
|
82b1887f6d | ||
|
|
dee0c54fde | ||
|
|
92cea96616 | ||
|
|
e1aa0e5bc6 | ||
|
|
57a2f67646 | ||
|
|
5f752ed251 | ||
|
|
2297eb0d1c | ||
|
|
7a8583e352 | ||
|
|
04658e99fd | ||
|
|
7e7eff6c8e | ||
|
|
2547d36254 | ||
|
|
45c3ec504f | ||
|
|
6b85bc51de | ||
|
|
19c5bac6d3 | ||
|
|
e9a55cc1db | ||
|
|
dc61aa07ac | ||
|
|
a36ca26720 | ||
|
|
a4e0c90fe9 | ||
|
|
bf46187d0a | ||
|
|
119ceb258e | ||
|
|
a737f7c7da | ||
|
|
a4f05c744e | ||
|
|
274f2d4c84 | ||
|
|
7c2528f4a7 | ||
|
|
7d1b36c7fb | ||
|
|
156c58e544 | ||
|
|
542b6c4e1a | ||
|
|
3cde52a3a5 | ||
|
|
8abb5cbd3a | ||
|
|
47648f6e2d | ||
|
|
d0b2bbee15 | ||
|
|
c7f0eab9c2 | ||
|
|
e504a2c591 | ||
|
|
76f613f82f | ||
|
|
623a9ffcb5 | ||
|
|
3641cd81cd | ||
|
|
e93e6f0061 | ||
|
|
d917f7c881 | ||
|
|
461b466bb8 | ||
|
|
39e912bb7f | ||
|
|
4922f3daf8 | ||
|
|
60620eccec | ||
|
|
343f7e7f56 | ||
|
|
d0d08f8a3c | ||
|
|
ada6a6f3ff | ||
|
|
c29edfd402 | ||
|
|
f16d557138 | ||
|
|
4f871534b0 | ||
|
|
1c6f6ad619 | ||
|
|
f55604f8fd | ||
|
|
09cb29c532 | ||
|
|
e3a931fab4 | ||
|
|
6ee7682547 | ||
|
|
24f1b1d377 | ||
|
|
f8b8ce0b5b | ||
|
|
6597f0058d | ||
|
|
e06f5380a1 | ||
|
|
c74079797a | ||
|
|
4f10e3f40a | ||
|
|
52a87c10b5 | ||
|
|
04f0aa1a7a | ||
|
|
9ca72341a8 | ||
|
|
0588cceee0 | ||
|
|
3e7b1a1de8 | ||
|
|
39d36eece6 | ||
|
|
2b2acd1311 | ||
|
|
b5c039eaba | ||
|
|
8633908301 | ||
|
|
425d6b7deb | ||
|
|
a6da85d2e5 | ||
|
|
fbbbacb121 | ||
|
|
5c0471d06e | ||
|
|
cf3ec3687c | ||
|
|
398c7ef80c | ||
|
|
2da5d6c28d | ||
|
|
8960130548 | ||
|
|
b4ff025836 | ||
|
|
9eae76442f | ||
|
|
e5a6c97058 | ||
|
|
ff4dfcac61 | ||
|
|
d2bc068f95 | ||
|
|
2778c88d3a | ||
|
|
5b66b440ce | ||
|
|
7b4e8a8f68 | ||
|
|
587f40f79f | ||
|
|
3cb1c4b027 | ||
|
|
5678215b25 | ||
|
|
5acab43669 | ||
|
|
974076c5f1 | ||
|
|
a4bac75d2a | ||
|
|
a0a899f999 | ||
|
|
92eecce9b0 | ||
|
|
a9b5521dfe | ||
|
|
be1921240a | ||
|
|
f602fe8d7d | ||
|
|
abe6c4c2b8 | ||
|
|
d01a7e18a2 | ||
|
|
276877119d | ||
|
|
5b77fdf54b | ||
|
|
b71bb9e48d | ||
|
|
05b5ecaa5f | ||
|
|
f7e0ba6544 | ||
|
|
836b5ef725 | ||
|
|
bc7eb9abaa | ||
|
|
dd177c5578 | ||
|
|
03b2bbd22c | ||
|
|
8729ad9cd1 | ||
|
|
fa9d964a32 | ||
|
|
dc12917b66 | ||
|
|
800c91c637 | ||
|
|
c3e73256bf | ||
|
|
84f45f523a | ||
|
|
43295a58eb | ||
|
|
10630944d3 | ||
|
|
1e2fef884f | ||
|
|
9839bf4014 | ||
|
|
dd680ecf70 | ||
|
|
1528231032 | ||
|
|
6f5d88a5c1 | ||
|
|
442293215c | ||
|
|
ab2ba579d1 | ||
|
|
c22ede7aa1 | ||
|
|
bfa4cd58a7 | ||
|
|
4b5c1e6d15 | ||
|
|
f367c79e8e | ||
|
|
c3cead5993 | ||
|
|
713706caff | ||
|
|
b84d6ac2ef | ||
|
|
78f15ac7d9 | ||
|
|
3f223f4306 | ||
|
|
a52c75601c | ||
|
|
8c162c75c5 | ||
|
|
bb8c07f232 | ||
|
|
3d117362ef | ||
|
|
023601fd09 | ||
|
|
5b1c7cbf5c | ||
|
|
4f2c5b1909 | ||
|
|
779ad2f8b9 | ||
|
|
b195c0d203 | ||
|
|
41521cca95 | ||
|
|
108a28384c | ||
|
|
b641cab624 | ||
|
|
b031d36fa2 | ||
|
|
97dc62afe1 | ||
|
|
e37d71fba6 | ||
|
|
1be10010c8 | ||
|
|
cec0e1bbcb | ||
|
|
7c4e4aea26 | ||
|
|
60f6108d35 | ||
|
|
43fb668c7d | ||
|
|
ca77d3cf2a | ||
|
|
906cfb5e43 | ||
|
|
20a38c662e | ||
|
|
1aa0934104 | ||
|
|
415d9e075d | ||
|
|
df74ff587a | ||
|
|
44dbcccd72 | ||
|
|
195dcf3fd7 | ||
|
|
ee9a10f39d | ||
|
|
188b15172f | ||
|
|
d2604c7848 | ||
|
|
a50057d80c | ||
|
|
8b28267b68 | ||
|
|
fa281cb400 | ||
|
|
1e3b321dcf | ||
|
|
7b0f7639e9 | ||
|
|
9d271eb932 | ||
|
|
103332e087 | ||
|
|
405a60b09b | ||
|
|
c903c7357a | ||
|
|
3c34adad82 | ||
|
|
2970cc5061 | ||
|
|
a2e71ee52a | ||
|
|
661d10d256 | ||
|
|
85a5c829e0 | ||
|
|
5782ae9438 | ||
|
|
600c93ed0f | ||
|
|
a38a52ebf3 | ||
|
|
dc83ad2dee | ||
|
|
d93910ba5a | ||
|
|
c54b375c06 | ||
|
|
e35a25b135 | ||
|
|
b4df9d87d8 | ||
|
|
0336af0624 | ||
|
|
1021604889 | ||
|
|
204e8d1b2b | ||
|
|
9f32f3a025 | ||
|
|
41dc71cf80 | ||
|
|
646ab03660 | ||
|
|
f3107ed19a | ||
|
|
978d373893 | ||
|
|
a6fe748b01 | ||
|
|
db26f4dbd9 | ||
|
|
d8435602d3 | ||
|
|
53643c9c5a | ||
|
|
d6d82248a8 | ||
|
|
3bd3f2dff8 | ||
|
|
8cf2dfc71b | ||
|
|
b7dc92d85a | ||
|
|
bd5647f4c7 | ||
|
|
b28eb09e3f | ||
|
|
9d22682626 | ||
|
|
0dabcec67c | ||
|
|
3c21002d32 | ||
|
|
20e45d4d42 | ||
|
|
da13725bea | ||
|
|
43c7bcced0 | ||
|
|
07d46605c7 | ||
|
|
daa3d07f7a | ||
|
|
729f59ba96 | ||
|
|
7da14338f9 | ||
|
|
cc73774085 | ||
|
|
6c83385af7 | ||
|
|
196940778f | ||
|
|
7544eb96fc | ||
|
|
0da85ba015 | ||
|
|
78484de959 | ||
|
|
829a6bbdc6 | ||
|
|
f71bd27a03 | ||
|
|
89aeb66a96 | ||
|
|
a4eb6b3cb3 | ||
|
|
a2eb97e504 | ||
|
|
bab3a53a53 | ||
|
|
837970147c | ||
|
|
0ecf21467b | ||
|
|
62c2cb3fb7 | ||
|
|
4fdb5a60d5 | ||
|
|
95334d2c8b | ||
|
|
6867d84ca9 | ||
|
|
baddf7e763 | ||
|
|
c21a2bf13a | ||
|
|
ac336efa33 | ||
|
|
52e010d79b | ||
|
|
91205a0b2a | ||
|
|
a510565324 | ||
|
|
3693bc39f4 | ||
|
|
aa7a732cce | ||
|
|
8d4535d6bf | ||
|
|
d6b4f2c682 | ||
|
|
1ba1701574 | ||
|
|
b30ab6a537 | ||
|
|
8462e5c763 | ||
|
|
0db3502855 | ||
|
|
c300a85204 | ||
|
|
a14484583d | ||
|
|
b68eda3051 | ||
|
|
36804a43bc | ||
|
|
2f70acd5d0 | ||
|
|
2d649d70be | ||
|
|
7b42915a1b | ||
|
|
590bd2dfd8 | ||
|
|
22abaa15c2 | ||
|
|
e1c9b34bbe | ||
|
|
5260f454ef | ||
|
|
b3ee3f44c4 | ||
|
|
64ac515ed0 | ||
|
|
29e72da327 | ||
|
|
96c25c428d | ||
|
|
699a475315 | ||
|
|
e7860e4f55 | ||
|
|
93985ee02d | ||
|
|
0806d71e96 | ||
|
|
a66186adc3 | ||
|
|
a3b1e02783 | ||
|
|
4a507e4d79 | ||
|
|
8808e0dfb8 | ||
|
|
9a00122525 | ||
|
|
ff775176a6 | ||
|
|
38943a74d5 | ||
|
|
63172ad79b | ||
|
|
06549bad2c | ||
|
|
63f5bea155 | ||
|
|
6e4a9c6a73 | ||
|
|
2c0cadf296 | ||
|
|
54a0cd2b50 | ||
|
|
4a363200a0 | ||
|
|
ee07f8391c | ||
|
|
7489100c26 | ||
|
|
7bb1fd6947 | ||
|
|
c8081da596 | ||
|
|
b970cfb305 | ||
|
|
cf2ce55d02 | ||
|
|
a847a110c7 | ||
|
|
3201bfe507 | ||
|
|
c8d2de60bb | ||
|
|
9df4d5f381 | ||
|
|
c75955a4a3 | ||
|
|
43e9b000d9 | ||
|
|
35bec0bed2 | ||
|
|
d84d99632e | ||
|
|
0eb4f74e36 | ||
|
|
44393643c9 | ||
|
|
1a1410eb1c | ||
|
|
34ba71e229 | ||
|
|
1c05556d98 | ||
|
|
94258b8d97 | ||
|
|
00310914da | ||
|
|
d42657b0a5 | ||
|
|
57f7a72d8d | ||
|
|
36474b9705 | ||
|
|
048e0d01de | ||
|
|
a2cb0f073c | ||
|
|
67a8826d24 | ||
|
|
e3301801b7 | ||
|
|
4ec76766d0 | ||
|
|
b3008e99dc | ||
|
|
1f76a177fe | ||
|
|
19817bf254 | ||
|
|
b5a3d6489b | ||
|
|
272f0d1d99 | ||
|
|
0ccd3b353d | ||
|
|
e0e28b5e35 | ||
|
|
48b3b83cae | ||
|
|
2407a0125e | ||
|
|
865b01c93b | ||
|
|
34ce5e9448 | ||
|
|
8df3db3607 | ||
|
|
b7124e5bda | ||
|
|
57d6855c78 | ||
|
|
64cc897f1a | ||
|
|
caa2811c0a | ||
|
|
c43e8b6b41 | ||
|
|
3ed1cdb261 | ||
|
|
bfd060e66e | ||
|
|
91fc3aa9fa | ||
|
|
153f94715d | ||
|
|
e8719f3796 | ||
|
|
f73d75dd6a | ||
|
|
6a3ede9102 | ||
|
|
b4aef5d14d | ||
|
|
32f8ea31e2 | ||
|
|
841759ddd8 | ||
|
|
bd6f7583c8 | ||
|
|
0c3b1d388f | ||
|
|
778faf2950 | ||
|
|
4581705d56 | ||
|
|
001fdab48b | ||
|
|
6c5778e547 | ||
|
|
2450512059 | ||
|
|
bb80b42426 | ||
|
|
717eefad97 | ||
|
|
644a8a6110 | ||
|
|
3d8935dc2b | ||
|
|
e16b4cb82d | ||
|
|
5ce951d599 | ||
|
|
0b5d7a1624 | ||
|
|
e256d988fc | ||
|
|
25ca5d52b2 | ||
|
|
e6942fe353 | ||
|
|
f9e17678b0 | ||
|
|
f385504c30 | ||
|
|
e34268c4c1 | ||
|
|
4b95927f5f | ||
|
|
2ccefb76a5 | ||
|
|
44d8ee902a | ||
|
|
4bc184f06c | ||
|
|
a3e0503613 | ||
|
|
fc06690ffd | ||
|
|
9606932a5c | ||
|
|
55e7ab5490 | ||
|
|
3d77f397f6 | ||
|
|
2c0b4fc072 | ||
|
|
6e246ed246 | ||
|
|
39e695dbcc | ||
|
|
3d42671277 | ||
|
|
28fd3f6f7e | ||
|
|
24bf7e5165 | ||
|
|
afe93e989f | ||
|
|
b6e01e71fa | ||
|
|
07d30410da | ||
|
|
79299ee95a | ||
|
|
eb01ca2b81 | ||
|
|
15d8f61e97 | ||
|
|
bfdc4b3183 | ||
|
|
405cf34193 | ||
|
|
d930182e54 | ||
|
|
4ddd01b77c | ||
|
|
5a4b2754e3 | ||
|
|
af88005cb5 | ||
|
|
6558ec7909 | ||
|
|
04f2b20503 | ||
|
|
d2ca717f48 | ||
|
|
a1237ab9b6 |
@@ -1,93 +0,0 @@
|
||||
# This .editorconfig section approximates ktfmt's formatting rules. You can include it in an
|
||||
# existing .editorconfig file or use it standalone by copying it to <project root>/.editorconfig
|
||||
# and making sure your editor is set to read settings from .editorconfig files.
|
||||
#
|
||||
# It includes editor-specific config options for IntelliJ IDEA.
|
||||
#
|
||||
# If any option is wrong, PR are welcome
|
||||
|
||||
[{*.kt,*.kts}]
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
indent_size = 2
|
||||
ij_continuation_indent_size = 2
|
||||
ij_java_names_count_to_use_import_on_demand = 9999
|
||||
ij_kotlin_align_in_columns_case_branch = false
|
||||
ij_kotlin_align_multiline_binary_operation = false
|
||||
ij_kotlin_align_multiline_extends_list = false
|
||||
ij_kotlin_align_multiline_method_parentheses = false
|
||||
ij_kotlin_align_multiline_parameters = true
|
||||
ij_kotlin_align_multiline_parameters_in_calls = false
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ij_kotlin_assignment_wrap = normal
|
||||
ij_kotlin_blank_lines_after_class_header = 0
|
||||
ij_kotlin_blank_lines_around_block_when_branches = 0
|
||||
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
|
||||
ij_kotlin_block_comment_at_first_column = true
|
||||
ij_kotlin_call_parameters_new_line_after_left_paren = true
|
||||
ij_kotlin_call_parameters_right_paren_on_new_line = false
|
||||
ij_kotlin_call_parameters_wrap = on_every_item
|
||||
ij_kotlin_catch_on_new_line = false
|
||||
ij_kotlin_class_annotation_wrap = split_into_lines
|
||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||
ij_kotlin_continuation_indent_for_chained_calls = true
|
||||
ij_kotlin_continuation_indent_for_expression_bodies = true
|
||||
ij_kotlin_continuation_indent_in_argument_lists = true
|
||||
ij_kotlin_continuation_indent_in_elvis = false
|
||||
ij_kotlin_continuation_indent_in_if_conditions = false
|
||||
ij_kotlin_continuation_indent_in_parameter_lists = false
|
||||
ij_kotlin_continuation_indent_in_supertype_lists = false
|
||||
ij_kotlin_else_on_new_line = false
|
||||
ij_kotlin_enum_constants_wrap = off
|
||||
ij_kotlin_extends_list_wrap = normal
|
||||
ij_kotlin_field_annotation_wrap = split_into_lines
|
||||
ij_kotlin_finally_on_new_line = false
|
||||
ij_kotlin_if_rparen_on_new_line = false
|
||||
ij_kotlin_import_nested_classes = false
|
||||
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
|
||||
ij_kotlin_keep_blank_lines_before_right_brace = 2
|
||||
ij_kotlin_keep_blank_lines_in_code = 2
|
||||
ij_kotlin_keep_blank_lines_in_declarations = 2
|
||||
ij_kotlin_keep_first_column_comment = true
|
||||
ij_kotlin_keep_indents_on_empty_lines = false
|
||||
ij_kotlin_keep_line_breaks = true
|
||||
ij_kotlin_lbrace_on_next_line = false
|
||||
ij_kotlin_line_comment_add_space = false
|
||||
ij_kotlin_line_comment_at_first_column = true
|
||||
ij_kotlin_method_annotation_wrap = split_into_lines
|
||||
ij_kotlin_method_call_chain_wrap = normal
|
||||
ij_kotlin_method_parameters_new_line_after_left_paren = true
|
||||
ij_kotlin_method_parameters_right_paren_on_new_line = true
|
||||
ij_kotlin_method_parameters_wrap = on_every_item
|
||||
ij_kotlin_name_count_to_use_star_import = 9999
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 9999
|
||||
ij_kotlin_parameter_annotation_wrap = off
|
||||
ij_kotlin_space_after_comma = true
|
||||
ij_kotlin_space_after_extend_colon = true
|
||||
ij_kotlin_space_after_type_colon = true
|
||||
ij_kotlin_space_before_catch_parentheses = true
|
||||
ij_kotlin_space_before_comma = false
|
||||
ij_kotlin_space_before_extend_colon = true
|
||||
ij_kotlin_space_before_for_parentheses = true
|
||||
ij_kotlin_space_before_if_parentheses = true
|
||||
ij_kotlin_space_before_lambda_arrow = true
|
||||
ij_kotlin_space_before_type_colon = false
|
||||
ij_kotlin_space_before_when_parentheses = true
|
||||
ij_kotlin_space_before_while_parentheses = true
|
||||
ij_kotlin_spaces_around_additive_operators = true
|
||||
ij_kotlin_spaces_around_assignment_operators = true
|
||||
ij_kotlin_spaces_around_equality_operators = true
|
||||
ij_kotlin_spaces_around_function_type_arrow = true
|
||||
ij_kotlin_spaces_around_logical_operators = true
|
||||
ij_kotlin_spaces_around_multiplicative_operators = true
|
||||
ij_kotlin_spaces_around_range = false
|
||||
ij_kotlin_spaces_around_relational_operators = true
|
||||
ij_kotlin_spaces_around_unary_operator = false
|
||||
ij_kotlin_spaces_around_when_arrow = true
|
||||
ij_kotlin_variable_annotation_wrap = off
|
||||
ij_kotlin_while_on_new_line = false
|
||||
ij_kotlin_wrap_elvis_expressions = 1
|
||||
ij_kotlin_wrap_expression_body_functions = 1
|
||||
ij_kotlin_wrap_first_method_in_call_chain = false
|
||||
@@ -1,11 +1,2 @@
|
||||
# KMP removal
|
||||
3d496c99c14106a7f433d01a6a9a1301cc89844a
|
||||
e7f4c8a7ebe4e907f65af46e6dddfd3a542415c0
|
||||
ab2713154f846b3db6d44625d1022bf421b275d0
|
||||
cbdfa67d075ec1f8ceec0f3559498cbca01783a0
|
||||
|
||||
# Renamed Renovate config file
|
||||
f304304bc5b64edf189aaa473b05df912067a8bd
|
||||
|
||||
# Refactored database module into core and impl
|
||||
7b0b206905b8dc1f824a2d2a10e3d94c6b9dd11c
|
||||
cec0e1bbcb7eec6c8eb90807c105cbeab9c01c6c # Reformatted Gradle build files with 2 space indent
|
||||
c3cead59934c8c5221694940fc83be57fa50ac20 # Moved data module into app
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
**/*.html linguist-vendored
|
||||
android/src/main/baseline-prof.txt linguist-generated -diff
|
||||
android/src/main/generated/** linguist-generated -diff
|
||||
@@ -0,0 +1 @@
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 432 KiB |
|
Before Width: | Height: | Size: 435 KiB |
@@ -1,80 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":dependencyDashboard",
|
||||
"helpers:pinGitHubActionDigests",
|
||||
"github>msfjarvis/shared-workflows//renovate/automerge"
|
||||
],
|
||||
"branchConcurrentLimit": 15,
|
||||
"github-actions": {
|
||||
"managerFilePatterns": [
|
||||
"/.github/reusable-workflows/.+\\.ya?ml$/",
|
||||
],
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
matchDatasources: [
|
||||
"maven"
|
||||
],
|
||||
registryUrls: [
|
||||
"https://maven.google.com/",
|
||||
"https://repo1.maven.org/maven2",
|
||||
"https://plugins.gradle.org/m2/",
|
||||
"https://jitpack.io",
|
||||
],
|
||||
},
|
||||
{
|
||||
"matchDepNames": [
|
||||
"com.google.devtools.ksp",
|
||||
"dev.drewhamilton.poko",
|
||||
"org.jetbrains.kotlin:kotlin-gradle-plugin",
|
||||
"org.jetbrains.kotlin.plugin.compose",
|
||||
"org.jetbrains.kotlin.plugin.serialization",
|
||||
],
|
||||
"groupName": "Kotlin and Compose",
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"/^androidx.*/",
|
||||
],
|
||||
"groupName": "AndroidX"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"/^io.sentry/",
|
||||
"/^sentry/",
|
||||
],
|
||||
"groupName": "Sentry"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"/^com.squareup.anvil/"
|
||||
],
|
||||
"allowedVersions": "!/.*-1-8$/",
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"github-actions"
|
||||
],
|
||||
"matchDepNames": [
|
||||
"msfjarvis/compose-lobsters"
|
||||
],
|
||||
"enabled": false,
|
||||
},
|
||||
],
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/SpotlessPlugin\.kt$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"KTFMT_VERSION = \"(?<currentValue>.*)\""
|
||||
],
|
||||
"datasourceTemplate": "maven",
|
||||
"depNameTemplate": "com.facebook:ktfmt",
|
||||
"registryUrlTemplate": "https://repo1.maven.org/maven2/",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
name: 'Setup Gradle'
|
||||
description: 'Checks out the repository and sets up Java and Gradle'
|
||||
inputs:
|
||||
token:
|
||||
description: 'token input for actions/checkout'
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
fetch-depth:
|
||||
description: 'fetch-depth input for actions/checkout'
|
||||
required: false
|
||||
default: 1
|
||||
cache-read-only:
|
||||
description: 'cache-read-only input for gradle/actions/setup-gradle'
|
||||
required: false
|
||||
default: ${{ github.event.repository != null && github.ref_name != github.event.repository.default_branch }}
|
||||
dependency-graph:
|
||||
description: 'dependency-graph input for gradle/actions/setup-gradle'
|
||||
required: false
|
||||
default: 'disabled'
|
||||
persist-credentials:
|
||||
description: 'Control the actions/checkout persist-credentials setting'
|
||||
required: false
|
||||
default: false
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: ${{ inputs.fetch-depth }}
|
||||
token: ${{ inputs.token }}
|
||||
lfs: true
|
||||
persist-credentials: ${{ inputs.persist-credentials }}
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 25
|
||||
|
||||
- name: Accept Android SDK licenses
|
||||
shell: bash
|
||||
run: |
|
||||
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses || true
|
||||
|
||||
- name: Set global Gradle properties
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ~/.gradle/
|
||||
echo "android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect" >> ~/.gradle/gradle.properties
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
|
||||
with:
|
||||
add-job-summary: always
|
||||
cache-read-only: ${{ inputs.cache-read-only }}
|
||||
dependency-graph: ${{ inputs.dependency-graph }}
|
||||
validate-wrappers: true
|
||||
@@ -1,80 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- renovate/**
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- name: Setup build environment
|
||||
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@92aae743ab8e1c98741128f1bfda3715750f5cf6 # main
|
||||
with:
|
||||
fetch-depth: 0
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
dependency-graph: generate-and-submit
|
||||
|
||||
- name: Check changelog format
|
||||
uses: mindsers/changelog-reader-action@1faaf50aa09d5793d9a100819973df801febfb31 # v2.4.0
|
||||
with:
|
||||
path: ./CHANGELOG.md
|
||||
validation_level: error
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew --no-configuration-cache --stacktrace :build-logic:check check -PslimTests
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: (Fail-only) Upload test report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Test report
|
||||
path: '**/build/reports/tests/**'
|
||||
|
||||
dependency-review:
|
||||
runs-on: macos-26
|
||||
if: "github.event_name == 'push' && github.event.ref != 'refs/heads/main'"
|
||||
needs:
|
||||
- check
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
||||
with:
|
||||
base-ref: refs/heads/main
|
||||
head-ref: ${{ github.ref }}
|
||||
|
||||
release-app:
|
||||
runs-on: macos-26
|
||||
needs:
|
||||
- check
|
||||
steps:
|
||||
- name: Setup build environment
|
||||
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@92aae743ab8e1c98741128f1bfda3715750f5cf6 # main
|
||||
with:
|
||||
cache-read-only: true
|
||||
|
||||
- name: Build release app
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
./gradlew --no-configuration-cache --stacktrace assembleRelease collectReleaseBundle
|
||||
./scripts/signing-cleanup.sh
|
||||
env:
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
@@ -0,0 +1,42 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
name: Deploy snapshot builds
|
||||
jobs:
|
||||
deploy-release-snapshot:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
steps:
|
||||
- uses: actions/setup-java@d202f5dbf7256730fb690ec59f6381650114feb2
|
||||
with:
|
||||
java-version: '11'
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: scripts/signing-setup.sh "$ENCRYPT_KEY"
|
||||
env:
|
||||
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build release app
|
||||
uses: burrunan/gradle-cache-action@03c71a8ba93d670980695505f48f49daf43704a6
|
||||
with:
|
||||
arguments: :app:assembleRelease
|
||||
|
||||
- name: Clean secrets
|
||||
run: scripts/signing-cleanup.sh
|
||||
|
||||
- name: Deploy snapshot
|
||||
run: scripts/deploy-snapshot.sh
|
||||
env:
|
||||
ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}
|
||||
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
|
||||
SERVER_ADDRESS: ${{ secrets.SERVER_ADDRESS }}
|
||||
SERVER_DESTINATION: ${{ secrets.SERVER_DESTINATION }}
|
||||
SSH_PORT: ${{ secrets.SSH_PORT }}
|
||||
@@ -0,0 +1,55 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- staging
|
||||
- trying
|
||||
|
||||
jobs:
|
||||
run-unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-java@d202f5dbf7256730fb690ec59f6381650114feb2
|
||||
with:
|
||||
java-version: '11'
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- uses: burrunan/gradle-cache-action@03c71a8ba93d670980695505f48f49daf43704a6
|
||||
name: Run unit tests
|
||||
with:
|
||||
arguments: test --stacktrace
|
||||
|
||||
- name: (Fail-only) upload test report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@27bce4eee761b5bc643f46a8dfb41b430c8d05f6
|
||||
with:
|
||||
name: Test report
|
||||
path: app/build/reports
|
||||
|
||||
stylecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-java@d202f5dbf7256730fb690ec59f6381650114feb2
|
||||
with:
|
||||
java-version: '11'
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- uses: burrunan/gradle-cache-action@03c71a8ba93d670980695505f48f49daf43704a6
|
||||
name: Check codestyle with Spotless
|
||||
with:
|
||||
arguments: spotlessCheck
|
||||
@@ -1,176 +0,0 @@
|
||||
name: Release to Google Play
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
inAppUpdatePriority:
|
||||
description: 'In app update priority (0-5)'
|
||||
type: number
|
||||
default: 0
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
publish-google-play-release:
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- name: Setup build environment
|
||||
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@92aae743ab8e1c98741128f1bfda3715750f5cf6 # main
|
||||
with:
|
||||
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
persist-credentials: true
|
||||
|
||||
- name: Set up Git author
|
||||
shell: bash
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email noreply@github.com
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
env:
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
|
||||
- name: Get current version
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew -q --no-configuration-cache clearPreRelease
|
||||
VERSION="$(grep 'versioning-plugin.versionName' android/version.properties | cut -d = -f 2)"
|
||||
VERSION_CODE="$(grep 'versioning-plugin.versionCode' android/version.properties | cut -d = -f 2)"
|
||||
echo VERSION="${VERSION}" >> $GITHUB_ENV
|
||||
echo VERSION_CODE="${VERSION_CODE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Update changelog
|
||||
uses: msfjarvis/keep-a-changelog-new-release@e60399f488b25149c0963a4e22fff928a9277ce7 # v2.2.0
|
||||
with:
|
||||
tag: v${{ env.VERSION }}
|
||||
version: ${{ env.VERSION }}
|
||||
|
||||
- name: Get changelog entry
|
||||
id: changelog_reader
|
||||
uses: mindsers/changelog-reader-action@1faaf50aa09d5793d9a100819973df801febfb31 # v2.4.0
|
||||
with:
|
||||
version: ${{ env.VERSION }}
|
||||
path: ./CHANGELOG.md
|
||||
|
||||
- name: Write changelog to fastlane metadata
|
||||
shell: bash
|
||||
env:
|
||||
CHANGELOG: ${{ steps.changelog_reader.outputs.changes }}
|
||||
run: |
|
||||
mkdir -p fastlane/metadata/android/en-US/changelogs
|
||||
printf '%s\n' "${CHANGELOG}" > fastlane/metadata/android/en-US/changelogs/${VERSION_CODE}.txt
|
||||
|
||||
- name: Commit changes
|
||||
shell: bash
|
||||
run: |
|
||||
git add .
|
||||
git commit -m 'feat(release): bump version'
|
||||
|
||||
- name: Calculate VCS info for Sentry
|
||||
shell: bash
|
||||
run: |
|
||||
# Get HEAD commit SHA
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
echo "SENTRY_VCS_HEAD_SHA=${HEAD_SHA}" >> $GITHUB_ENV
|
||||
|
||||
# Get parent info (format: commit parent1 [parent2...])
|
||||
PARENTS=$(git rev-list --parents -n 1 HEAD)
|
||||
PARENT_COUNT=$(echo "$PARENTS" | wc -w)
|
||||
|
||||
if [ "$PARENT_COUNT" -ge 3 ]; then
|
||||
# Merge commit (commit + 2+ parents): use first parent
|
||||
BASE_SHA=$(echo "$PARENTS" | awk '{print $2}')
|
||||
else
|
||||
# Normal commit: use last release
|
||||
LAST_TAG=$(gh release list --limit 1 --json tagName -q '.[0].tagName' || echo "")
|
||||
if [ -n "$LAST_TAG" ]; then
|
||||
BASE_SHA=$(git rev-parse "${LAST_TAG}^{commit}")
|
||||
else
|
||||
BASE_SHA="${HEAD_SHA}"
|
||||
fi
|
||||
fi
|
||||
echo "SENTRY_VCS_BASE_SHA=${BASE_SHA}" >> $GITHUB_ENV
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build release assets
|
||||
shell: bash
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_VCS_HEAD_SHA: ${{ env.SENTRY_VCS_HEAD_SHA }}
|
||||
SENTRY_VCS_BASE_SHA: ${{ env.SENTRY_VCS_BASE_SHA }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
./gradlew --no-configuration-cache assembleRelease uploadSentryProguardMappingsRelease collectReleaseApks collectReleaseBundle -PenableSentry
|
||||
|
||||
- name: Clean secrets
|
||||
run: scripts/signing-cleanup.sh
|
||||
|
||||
- name: Write release notes file
|
||||
shell: bash
|
||||
env:
|
||||
CHANGELOG: ${{ steps.changelog_reader.outputs.changes }}
|
||||
run: |
|
||||
mkdir -p distribution/whatsnew
|
||||
printf '%s' "${CHANGELOG}" >> distribution/whatsnew/whatsnew-en-GB
|
||||
|
||||
- name: Publish bundle to Google Play
|
||||
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
|
||||
with:
|
||||
mappingFile: android/bundle/mapping.txt
|
||||
packageName: dev.msfjarvis.claw.android
|
||||
releaseFiles: android/bundle/*.aab
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
status: completed
|
||||
track: production
|
||||
whatsNewDirectory: distribution/whatsnew
|
||||
inAppUpdatePriority: ${{ inputs.inAppUpdatePriority }}
|
||||
|
||||
- name: Post-release work
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.POST_RELEASE_GH_TOKEN }}
|
||||
run: |
|
||||
# Tag the current version
|
||||
git tag "v${VERSION}" -F distribution/whatsnew/whatsnew-en-GB
|
||||
|
||||
# Push the tag to GitHub
|
||||
git push origin "v${VERSION}"
|
||||
|
||||
# Create GitHub release notes with footer
|
||||
RELEASE_NOTES=$(mktemp)
|
||||
{
|
||||
cat distribution/whatsnew/whatsnew-en-GB
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo '<a href="https://play.google.com/store/apps/details?id=dev.msfjarvis.claw.android">'
|
||||
echo ' <img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"'
|
||||
echo ' alt="Get it on Google Play"'
|
||||
echo ' height="80" />'
|
||||
echo '</a>'
|
||||
echo ""
|
||||
echo '<a href="https://android.izzysoft.de/repo/apk/dev.msfjarvis.claw.android">'
|
||||
echo ' <img src="./.github/izzy-badge.webp"'
|
||||
echo ' alt="Get it on IzzyOnDroid"'
|
||||
echo ' height="65" />'
|
||||
echo '</a>'
|
||||
} > "$RELEASE_NOTES"
|
||||
|
||||
# Create a GitHub release
|
||||
gh release create "v${VERSION}" --notes-file "$RELEASE_NOTES" --title "v${VERSION}" ./android/apk/*.apk ./android/bundle/*.aab
|
||||
|
||||
# Clean up temp file
|
||||
rm "$RELEASE_NOTES"
|
||||
|
||||
# Start the next development iteration
|
||||
./gradlew -q --no-configuration-cache bumpSnapshot
|
||||
git commit -am 'feat(release): start next development iteration'
|
||||
git push -u origin
|
||||
@@ -1,72 +0,0 @@
|
||||
name: Android release build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tramline-input:
|
||||
description: "Tramline input"
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
signed-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup build environment
|
||||
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@92aae743ab8e1c98741128f1bfda3715750f5cf6 # main
|
||||
with:
|
||||
cache-read-only: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Tramline
|
||||
id: tramline
|
||||
uses: tramlinehq/deploy-action@58cc98c188b9eda853e549ede6a8b2f58fe29516 # v0.1.7
|
||||
with:
|
||||
input: ${{ github.event.inputs.tramline-input }}
|
||||
|
||||
- name: Calculate VCS info for Sentry
|
||||
shell: bash
|
||||
run: |
|
||||
# Get HEAD commit SHA
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
echo "SENTRY_VCS_HEAD_SHA=${HEAD_SHA}" >> $GITHUB_ENV
|
||||
|
||||
# Get parent info (format: commit parent1 [parent2...])
|
||||
PARENTS=$(git rev-list --parents -n 1 HEAD)
|
||||
PARENT_COUNT=$(echo "$PARENTS" | wc -w)
|
||||
|
||||
if [ "$PARENT_COUNT" -ge 3 ]; then
|
||||
# Merge commit (commit + 2+ parents): use first parent
|
||||
BASE_SHA=$(echo "$PARENTS" | awk '{print $2}')
|
||||
else
|
||||
# Normal commit: use last release
|
||||
LAST_TAG=$(gh release list --limit 1 --json tagName -q '.[0].tagName' || echo "")
|
||||
if [ -n "$LAST_TAG" ]; then
|
||||
BASE_SHA=$(git rev-parse "${LAST_TAG}^{commit}")
|
||||
else
|
||||
BASE_SHA="${HEAD_SHA}"
|
||||
fi
|
||||
fi
|
||||
echo "SENTRY_VCS_BASE_SHA=${BASE_SHA}" >> $GITHUB_ENV
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build release app
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
./gradlew --no-configuration-cache --stacktrace assembleRelease uploadSentryProguardMappingsRelease collectReleaseBundle -PenableSentry
|
||||
./scripts/signing-cleanup.sh
|
||||
env:
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_VCS_HEAD_SHA: ${{ env.SENTRY_VCS_HEAD_SHA }}
|
||||
SENTRY_VCS_BASE_SHA: ${{ env.SENTRY_VCS_BASE_SHA }}
|
||||
ORG_GRADLE_PROJECT_VERSION_NAME: ${{ steps.tramline.outputs.version_name }}
|
||||
ORG_GRADLE_PROJECT_VERSION_CODE: ${{ steps.tramline.outputs.version_code }}
|
||||
|
||||
- name: Upload app bundle
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-aab
|
||||
path: android/build/outputs/bundle/release/android-release.aab
|
||||
@@ -1,102 +1,10 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/androidstudio,gradle,intellij+all
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio,gradle,intellij+all
|
||||
|
||||
### Intellij+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
### START CHANGES - We use Gradle auto-import ###
|
||||
.idea/artifacts
|
||||
.idea/compiler.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/modules.xml
|
||||
.idea/*.iml
|
||||
.idea/modules
|
||||
*.iml
|
||||
*.ipr
|
||||
### END CHANGES - We use Gradle auto-import ###
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Intellij+all Patch ###
|
||||
# Ignore everything but code style settings and run configurations
|
||||
# that are supposed to be shared within teams.
|
||||
|
||||
.idea/*
|
||||
|
||||
### START CHANGES - We use Spotless to enforce code style ###
|
||||
.idea/codeStyles
|
||||
### END CHANGES - We use Spotless to enforce code style ###
|
||||
!.idea/runConfigurations
|
||||
# Created by https://www.gitignore.io/api/androidstudio,gradle
|
||||
# Edit at https://www.gitignore.io/?templates=androidstudio,gradle
|
||||
|
||||
### Gradle ###
|
||||
.gradle
|
||||
**/build/
|
||||
!src/**/build/
|
||||
build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
@@ -104,21 +12,14 @@ gradle-app.setting
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
# Avoid ignore Gradle wrappper properties
|
||||
!gradle-wrapper.properties
|
||||
|
||||
# Cache of project
|
||||
.gradletasknamecache
|
||||
|
||||
# Eclipse Gradle plugin generated files
|
||||
# Eclipse Core
|
||||
.project
|
||||
# JDT-specific (Eclipse Java Development Tools)
|
||||
.classpath
|
||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
### Gradle Patch ###
|
||||
# Java heap dump
|
||||
*.hprof
|
||||
**/build/
|
||||
|
||||
### AndroidStudio ###
|
||||
# Covers files to be ignored for android development using Android Studio.
|
||||
@@ -126,7 +27,6 @@ gradle-app.setting
|
||||
# Built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
@@ -137,13 +37,15 @@ gradle-app.setting
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Signing files
|
||||
.signing/
|
||||
keystore.jks
|
||||
keystore.properties
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
@@ -166,13 +68,6 @@ captures/
|
||||
*~
|
||||
*.swp
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
|
||||
# Android Patch
|
||||
gen-external-apklibs
|
||||
|
||||
@@ -184,6 +79,7 @@ obj/
|
||||
|
||||
# IntelliJ IDEA
|
||||
*.iml
|
||||
*.iws
|
||||
/out/
|
||||
|
||||
# User-specific configurations
|
||||
@@ -208,11 +104,20 @@ obj/
|
||||
.idea/dynamic.xml
|
||||
.idea/uiDesigner.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/gradle.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/navEditor.xml
|
||||
.idea/deploymentTargetDropDown.xml
|
||||
|
||||
# OS-specific files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Legacy Eclipse project files
|
||||
.classpath
|
||||
.project
|
||||
.cproject
|
||||
.settings/
|
||||
|
||||
@@ -229,23 +134,22 @@ hs_err_pid*
|
||||
## Plugin-specific files:
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/mongoSettings.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
### AndroidStudio Patch ###
|
||||
|
||||
!/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/androidstudio,gradle,intellij+all
|
||||
|
||||
keystore.properties
|
||||
distribution/
|
||||
screenshots/
|
||||
claw-export.json
|
||||
.kotlin/
|
||||
.worktrees/
|
||||
# End of https://www.gitignore.io/api/androidstudio,gradle
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
/artifacts/
|
||||
/gradle.xml
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaProjectCodeInsightSettings">
|
||||
<excluded-names>
|
||||
<name>android.app.LauncherActivity.ListItem</name>
|
||||
<name>android.graphics.Color</name>
|
||||
<name>android.graphics.drawable.Icon</name>
|
||||
<name>android.graphics.fonts.FontFamily</name>
|
||||
<name>android.inputmethodservice.Keyboard.Row</name>
|
||||
<name>android.text.layout.Alignment</name>
|
||||
<name>android.view.RoundedCorner</name>
|
||||
<name>android.view.Surface</name>
|
||||
<name>android.widget.GridLayout.Alignment</name>
|
||||
<name>java.lang.reflect.Modifier</name>
|
||||
<name>java.nio.file.WatchEvent.Modifier</name>
|
||||
<name>java.time.format.TextStyle</name>
|
||||
<name>java.util.Observable</name>
|
||||
<name>org.w3c.dom.Text</name>
|
||||
</excluded-names>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,143 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="LINE_SEPARATOR" value=" " />
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value />
|
||||
</option>
|
||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||
<value>
|
||||
<package name="" alias="false" withSubpackages="true" />
|
||||
<package name="" alias="true" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="100" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="100" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
<option name="FIELD_ANNOTATION_WRAP" value="1" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,20 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven" />
|
||||
<option name="name" value="maven" />
|
||||
<option name="url" value="https://dl.bintray.com/kotlin/kotlinx" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenRepo" />
|
||||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven" />
|
||||
<option name="name" value="maven" />
|
||||
<option name="url" value="https://maven.pkg.jetbrains.space/public/p/compose/dev" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinScriptingSettings">
|
||||
<option name="suppressDefinitionsCheck" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KtfmtSettings">
|
||||
<option name="enableKtfmt" value="Enabled" />
|
||||
<option name="enabled" value="true" />
|
||||
<option name="uiFormatterStyle" value="Google (internal)" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,678 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.64.0] - 2026-05-26
|
||||
|
||||
### Added
|
||||
|
||||
- Allow logging in to avoid rate limiting problems.
|
||||
|
||||
### Changed
|
||||
|
||||
- The migration path for tag data format changes in 1.61.0 has been removed.
|
||||
- The app no longer uses the unsupported JSON endpoints on lobste.rs, instead relying
|
||||
on parsing the HTML pages instead.
|
||||
|
||||
## [1.63.0] - 2026-04-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- Comments are no longer indicated as unread on the first visit of a post
|
||||
|
||||
### Changed
|
||||
|
||||
- Post titles in the list view can now take up 2 lines to make ellipsizing less likely
|
||||
- Action buttons now have a prominent background to make their touch target more obvious
|
||||
- Action buttons now have additional padding to avoid accidental presses when interacting
|
||||
with system gestures
|
||||
|
||||
Thanks to Charles Lombardo for contributing the action button UI changes.
|
||||
|
||||
## [1.62.0] - 2026-03-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve comment scrolling performance
|
||||
- Ensure saved/read state for posts updates properly in UI
|
||||
- All strings in the app are now translatable, open to volunteer contributions.
|
||||
|
||||
## [1.61.0] - 2026-02-28
|
||||
|
||||
### Fixed
|
||||
|
||||
- Collapsed comments state are now persisted across navigation events
|
||||
|
||||
### Changed
|
||||
|
||||
- Tags use a more muted background color to avoid being distracting.
|
||||
- Tag blocks can now be time-bound
|
||||
|
||||
## [1.60.0] - 2026-02-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Rare crash in widget handling
|
||||
- Line-breaking behavior in comments now packs text densely
|
||||
- Tag filtering now applies to widgets as well
|
||||
|
||||
### Changed
|
||||
|
||||
- Links to other posts now open in the app itself
|
||||
- Widget handles bad network scenarios better
|
||||
|
||||
## [1.59.0] - 2026-01-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- Crashes from process death
|
||||
- Crashes in widget handling
|
||||
- Android Auto Backup works again
|
||||
|
||||
### Changed
|
||||
|
||||
- Added a tag filtering option to settings
|
||||
- Comment count badge has been restored
|
||||
- Navigation behavior has been improved to avoid too-deep hierarchies
|
||||
- Widget clicks are handled reliably
|
||||
- Widget UI has been touched up and a preview added for Android 16+
|
||||
- User profile page is now scrollable
|
||||
|
||||
## [1.58.0] - 2025-12-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Avoid crash from trying to import invalid backups
|
||||
- Avoid crash when posts move between pages
|
||||
|
||||
### Changed
|
||||
|
||||
- Widget update is more reliable
|
||||
- Increased network timeouts
|
||||
- Better network retries and rate limit avoidance
|
||||
- Temporarily remove comment count to fix crashes
|
||||
|
||||
## [1.57.0] - 2025-12-07
|
||||
|
||||
### Changed
|
||||
|
||||
- Navigation rewritten for larger screens
|
||||
- Comment expand/collapse now uses button UI
|
||||
- Comment rendering rewritten for better performance
|
||||
- Settings page redesigned
|
||||
|
||||
### Fixed
|
||||
|
||||
- Database concurrency improvements
|
||||
- Upgrade to Compose 2025-12 alpha
|
||||
- Swipe action trigger distance increased for Android 16 QPR1 Beta 1
|
||||
- Deeply nested comment tree collapsing
|
||||
- Post author indication in details
|
||||
- Dark mode toggle crash
|
||||
|
||||
## [1.55.0] - 2025-02-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Adapt to lobsters API change
|
||||
|
||||
## [1.54.0] - 2025-01-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- Upgrade to Compose 2025-01 releases
|
||||
- Upgrade all dependencies
|
||||
|
||||
## [1.53.0] - 2024-11-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- Tablet users can now navigate between hottest/newest/saved posts again
|
||||
|
||||
## [1.52.0] - 2024-11-26
|
||||
|
||||
### Added
|
||||
|
||||
- A brand new tablet-specific UI — Thanks [@ThanaReka](https://github.com/@ThanaReka)
|
||||
- Usernames of story authors are highlighted in comments
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add a workaround for rare crashes while loading SQLite with older devices
|
||||
- Fix comments page resetting collapsed state while scrolling
|
||||
- Images are cached for longer on disk
|
||||
- Navigation bar now renders a solid background on API levels 31 and below
|
||||
|
||||
## [1.51.0] - 2024-09-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Pull to refresh indicator is no longer hiding behind the top app bar
|
||||
- User profile page correctly renders contents below the top app bar
|
||||
- Revert unread comment handling changes that caused false positives
|
||||
|
||||
## [1.50.0] - 2024-08-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- Search bar draws under the status bar when hiding keyboard
|
||||
- Bottom of the comments page is cut off by the navigation bar
|
||||
- Rare crash in comments page
|
||||
|
||||
## [1.49.0] - 2024-08-29
|
||||
|
||||
### Added
|
||||
|
||||
- Opening posts you have previously seen will show the number of new comments since last visit
|
||||
|
||||
### Fixed
|
||||
|
||||
- Saving posts no longer triggers a page refresh that invalidates scroll position
|
||||
|
||||
### Changed
|
||||
|
||||
- Change submitter text to 'authored' when applicable
|
||||
- Unread comments now have a brighter background rather than a text badge
|
||||
- Bottom navigation bar has been redesigned
|
||||
|
||||
## [1.48.0] - 2024-06-05
|
||||
|
||||
### Added
|
||||
|
||||
- Swiping a post from left to right now offers a share action
|
||||
|
||||
### Fixed
|
||||
|
||||
- Try to work around an infrequent crash when scrolling hottest/newest posts
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Kotlin 2.0.0
|
||||
- Upgrade to Compose May beta releases
|
||||
|
||||
## [1.47.0] - 2024-05-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Bring back dividers between posts (I regret my earlier choices)
|
||||
- Upgrade to Compose May stable releases
|
||||
|
||||
### Fixed
|
||||
|
||||
- Navigating to user profiles now works when invoked from the search
|
||||
results page
|
||||
- Fix occasional crashes due to the app incorrectly trying to open
|
||||
multiple database connections
|
||||
|
||||
## [1.46.0] - 2024-04-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove dividers between posts
|
||||
- Post titles in lists are now truncated to be a single line
|
||||
|
||||
### Fixed
|
||||
|
||||
- Disable logging of network errors to Sentry
|
||||
- Add potential workaround for navigation-related crashes
|
||||
|
||||
## [1.45.0] - 2024-04-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Compose April releases
|
||||
- Story items are now more compact so you can see more items
|
||||
on your screen.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a crash when clicking an item on the bottom navigation bar
|
||||
too quickly
|
||||
- Removed buggy deep links
|
||||
- Clicking a username now correctly navigates to the right page in-app
|
||||
|
||||
## [1.44.0] - 2024-03-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a bug in the database code that was triggering crashes for
|
||||
a subset of upgrading users
|
||||
|
||||
## [1.43.0] - 2024-03-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix crash when trying to open a comments-only post from
|
||||
the home screen widget
|
||||
|
||||
## [1.42.0] - 2024-03-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Adapt to changes in lobste.rs API
|
||||
|
||||
## [1.41.0] - 2024-03-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- Downgrade dependency that was pulling in extraneous storage
|
||||
permissions which the app does not require.
|
||||
|
||||
## [1.40.0] - 2024-03-07
|
||||
|
||||
### Changed
|
||||
|
||||
- Update to Compose March releases
|
||||
- Improve performance of frequently invoked database queries
|
||||
- Refactor UI data model to improve state handling
|
||||
- Make saved posts update in the background more often
|
||||
- Consolidate widget display logic
|
||||
- Rework some theming logic to align with new Material guidelines
|
||||
|
||||
## [1.39.0] - 2024-01-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Add HTML bookmarks as an export format
|
||||
- Rework widget item layout for consistent touch targets
|
||||
- Add attribution for libraries used in the app
|
||||
- Redesign settings screen
|
||||
- Update to Compose December releases
|
||||
|
||||
## [1.38.0] - 2023-11-20
|
||||
|
||||
### Changed
|
||||
|
||||
- Add brand new icon by dzuk
|
||||
- Fix a bug that caused the app to fetch the same 20 posts over and over
|
||||
- Significantly improve scrolling performance when there are a lot of saved posts
|
||||
- Move search feature to its own screen
|
||||
- Make top app and bottom system bars use the same color
|
||||
- Reduce unnecessary API calls in search screen
|
||||
- Upgrade to Compose November releases
|
||||
- Enable logging of SQLite queries
|
||||
|
||||
## [1.37.0] - 2023-10-06
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a crash that infrequently happened in the comments page
|
||||
- Fixed a crash when trying to list saved posts
|
||||
- Fixed a crash in home screen widget when user had less than 50 saved posts
|
||||
- Upgraded to Compose 1.6.0-alpha07
|
||||
|
||||
## [1.36.0] - 2023-10-04
|
||||
|
||||
### Added
|
||||
|
||||
- Introduce an initial attempt at a home screen widget
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed a crash triggered when swiping items
|
||||
- Adjusted pull to refresh component to match Material You theme
|
||||
|
||||
## [1.35.0] - 2023-09-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed missing vertical spacing between post tags
|
||||
- Fixed user profile links not displaying correctly in-app
|
||||
- Update to Compose October release
|
||||
|
||||
## [1.34.0] - 2023-08-30
|
||||
|
||||
### Added
|
||||
|
||||
- Add a swipe action on each story to open the comments page on `lobste.rs`
|
||||
- Automatically mark posts as "read" and visually distinguish between them
|
||||
|
||||
### Changed
|
||||
|
||||
- Tweak list items to reduce vertical size and adjust colors
|
||||
- Drop bogus workarounds for native library crashes
|
||||
- Updated Jetpack Compose and SQLite
|
||||
|
||||
### Fixed
|
||||
|
||||
- Rework how comments are displayed to adapt to lobste.rs API change
|
||||
|
||||
## [1.33.0] - 2023-07-31
|
||||
|
||||
### Changed
|
||||
|
||||
- Yet another attempt at fixing native library crashes
|
||||
- Rework how baseline profiles are generated
|
||||
|
||||
## [1.32.0] - 2023-07-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to AndroidX July 26 release
|
||||
- Revert selectable text feature due to sporadic crashes
|
||||
|
||||
## [1.31.0] - 2023-07-25
|
||||
|
||||
### Added
|
||||
|
||||
- Added the ability to search for posts
|
||||
- Text in the comments page is now selectable
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Compose July release
|
||||
- Upgrade to Kotlin 1.9.0
|
||||
|
||||
## [1.30.0] - 2023-07-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Added another workaround for native library loading crash
|
||||
|
||||
## [1.29.0] - 2023-06-08
|
||||
|
||||
### Added
|
||||
|
||||
- Backup and restore options for saved posts
|
||||
|
||||
### Fixed
|
||||
|
||||
- Spamming the comments button no longer causes it to be opened multiple times
|
||||
- Saved posts screen now has a visual indication when you have nothing saved
|
||||
- Iconography has been updated across the board to be more consistent
|
||||
|
||||
## [1.28.0] - 2023-06-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Navigation transitions have been slightly sped up
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add workaround for a native library loading crash observed on some devices
|
||||
|
||||
## [1.27.0] - 2023-05-31
|
||||
|
||||
### Changed
|
||||
|
||||
- Small accessibility improvements
|
||||
- Slightly tweak the layout for story items to take less vertical space
|
||||
- Upgrade to Compose `1.5.0-beta01` release
|
||||
- Set accessibility web URI for profile screen
|
||||
- Directly use Material Icons from upstream artifacts
|
||||
- Upgrade dependencies
|
||||
- Add adaptive navigation support (thanks @Yash-Garg)
|
||||
- Use latest SQLite for backing databases
|
||||
|
||||
## [1.26.0] - 2023-05-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Compose May release
|
||||
- Disable Sentry performance reporting
|
||||
- Migrate to Compose Foundation `FlowRow` in `LobstersCard`
|
||||
|
||||
## [1.25.0] - 2023-04-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Make the app draw edge to edge
|
||||
- Simplify Top App Bar color scheme
|
||||
- Rework how baseline profiles are generated
|
||||
- Upgrade to Compose April release
|
||||
|
||||
## [1.24.0] - 2023-03-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Rebuild app icon assets to align better with Material guidelines
|
||||
- Adopt Slack's Compose lints and address issues found by it
|
||||
- Upgrade to OkHttp 4.x
|
||||
- Upgrade to Compose March release
|
||||
- Start work on support for logging in with lobste.rs
|
||||
|
||||
## [1.23.0] - 2023-03-02
|
||||
|
||||
### Added
|
||||
|
||||
- Introduce [Sentry](https://sentry.io) for error reporting and performance monitoring
|
||||
|
||||
## [1.22.0] - 2023-03-02
|
||||
|
||||
### Changed
|
||||
|
||||
- In-app browser now respects the user's choice for dark mode
|
||||
- Upgrade dependencies
|
||||
|
||||
## [1.21.0] - 2023-02-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Fix bug that caused the app to crash on launch
|
||||
|
||||
## [1.20.0] - 2023-02-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade dependencies
|
||||
|
||||
## [1.19.0] - 2023-02-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade dependencies
|
||||
- Optimize packaged baseline profiles
|
||||
|
||||
## [1.18.0] - 2023-01-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Special-case deleted stories in comments API
|
||||
- Fix regression where save button had the incorrect visual state
|
||||
|
||||
## [1.17.0] - 2023-01-12
|
||||
|
||||
### Added
|
||||
|
||||
- The app will now mark new comments as unread when returning to posts
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Kotlin 1.8.0
|
||||
- Revert questionable app bar scrolling behaviour change
|
||||
|
||||
## [1.16.0] - 2022-12-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Fix bug where the newest posts tab actually showed the hottest posts instead
|
||||
|
||||
## [1.15.0] - 2022-12-20
|
||||
|
||||
### Changed
|
||||
|
||||
- Fix a case where collapsing the parent of an already collapsed comment caused it to become expanded
|
||||
- Tweak progress bar UI to use a linear indicator
|
||||
- Allow app bar to hide when scrolling down
|
||||
- Fix bug where refreshing a list of posts caused it to jump around multiple times
|
||||
|
||||
## [1.14.0] - 2022-12-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Rework how often saved posts are updated
|
||||
- Collapsing a comment now collapses all comments under it similar to how it works on the website
|
||||
|
||||
## [1.13.0] - 2022-12-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Fix crash when viewing comments by ~jcs
|
||||
|
||||
## [1.12.0] - 2022-12-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Do not schedule post update job every time the app starts
|
||||
- Improve favicon loading to reduce unnecessary redraws
|
||||
- Fix post lists being reloaded unnecessarily
|
||||
|
||||
## [1.11.0] - 2022-12-07
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Compose 2022.12.00 release
|
||||
- Refresh packaged baseline profile
|
||||
- Improve how saved state of posts is queried
|
||||
|
||||
## [1.10.0] - 2022-11-30
|
||||
|
||||
### Changed
|
||||
|
||||
- Configure Coil image loader with better caching settings
|
||||
- Refresh packaged baseline profile
|
||||
|
||||
## [1.9.0] - 2022-11-28
|
||||
|
||||
### Changed
|
||||
|
||||
- A loading indicator is added to the bottom of the post list to identify when new posts are being fetched
|
||||
|
||||
## [1.8.0] - 2022-11-21
|
||||
|
||||
### Changed
|
||||
|
||||
- The release pipeline now automatically generates Play Store release notes from the changelog file
|
||||
|
||||
### Fixed
|
||||
|
||||
- Downgrade AGP to 8.0.0-alpha07 to fix Baseline Profiles not being packaged
|
||||
|
||||
## [1.7.0] - 2022-11-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Switch to Compose Material's swipe refresh feature
|
||||
- Change metadata extractor logic to no longer prefer canonical URLs. The submitted URL will be retained as-is now.
|
||||
- Remove navigation transition animations
|
||||
|
||||
## [1.6.0] - 2022-11-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrate dependency injection from Hilt to Anvil + Whetstone
|
||||
|
||||
## [1.5.0] - 2022-11-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Compose 2022.11.00 release
|
||||
|
||||
## [1.4.0] - 2022-11-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Misc fixes to link metadata extractor
|
||||
|
||||
## [1.3.0] - 2022-10-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Prevent errors during link metadata retrieval from crashing the app
|
||||
- Update Jetpack libraries to 2022-10-24 release
|
||||
|
||||
## [1.2.0] - 2022-10-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Lazily load link metadata to improve comment page loading speed
|
||||
- Upgrade to latest Compose release
|
||||
- Refactor code to align with Twitter's public Compose guidelines
|
||||
|
||||
## [1.1.0] - 2022-09-30
|
||||
|
||||
### Added
|
||||
|
||||
- Add score and relative time to comments
|
||||
|
||||
### Changed
|
||||
|
||||
- Update Jetpack Compose
|
||||
- Declare data backup and transfer rules
|
||||
- Improve legibility of links in comments and user profiles
|
||||
- Commonize and improve how network errors are displayed
|
||||
|
||||
## [1.0.0] - 2022-09-20
|
||||
|
||||
- Initial Play Store release
|
||||
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.64.0...HEAD
|
||||
[1.64.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.63.0...v1.64.0
|
||||
[1.63.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.62.0...v1.63.0
|
||||
[1.62.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.61.0...v1.62.0
|
||||
[1.61.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.60.0...v1.61.0
|
||||
[1.60.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.59.0...v1.60.0
|
||||
[1.59.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.58.0...v1.59.0
|
||||
[1.58.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.57.0...v1.58.0
|
||||
[1.57.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.55.0...v1.57.0
|
||||
[1.55.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.54.0...v1.55.0
|
||||
[1.54.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.53.0...v1.54.0
|
||||
[1.53.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.52.0...v1.53.0
|
||||
[1.52.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.51.0...v1.52.0
|
||||
[1.51.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.50.0...v1.51.0
|
||||
[1.50.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.49.0...v1.50.0
|
||||
[1.49.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.48.0...v1.49.0
|
||||
[1.48.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.47.0...v1.48.0
|
||||
[1.47.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.46.0...v1.47.0
|
||||
[1.46.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.45.0...v1.46.0
|
||||
[1.45.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.44.0...v1.45.0
|
||||
[1.44.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.43.0...v1.44.0
|
||||
[1.43.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.42.0...v1.43.0
|
||||
[1.42.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.41.0...v1.42.0
|
||||
[1.41.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.40.0...v1.41.0
|
||||
[1.40.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.39.0...v1.40.0
|
||||
[1.39.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.38.0...v1.39.0
|
||||
[1.38.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.37.0...v1.38.0
|
||||
[1.37.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.36.0...v1.37.0
|
||||
[1.36.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.35.0...v1.36.0
|
||||
[1.35.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.34.0...v1.35.0
|
||||
[1.34.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.33.0...v1.34.0
|
||||
[1.33.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.32.0...v1.33.0
|
||||
[1.32.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.31.0...v1.32.0
|
||||
[1.31.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.30.0...v1.31.0
|
||||
[1.30.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.29.0...v1.30.0
|
||||
[1.29.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.28.0...v1.29.0
|
||||
[1.28.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.27.0...v1.28.0
|
||||
[1.27.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.26.0...v1.27.0
|
||||
[1.26.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.25.0...v1.26.0
|
||||
[1.25.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.24.0...v1.25.0
|
||||
[1.24.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.23.0...v1.24.0
|
||||
[1.23.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.22.0...v1.23.0
|
||||
[1.22.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.21.0...v1.22.0
|
||||
[1.21.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.20.0...v1.21.0
|
||||
[1.20.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.19.0...v1.20.0
|
||||
[1.19.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.18.0...v1.19.0
|
||||
[1.18.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.17.0...v1.18.0
|
||||
[1.17.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.16.0...v1.17.0
|
||||
[1.16.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.15.0...v1.16.0
|
||||
[1.15.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.14.0...v1.15.0
|
||||
[1.14.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.13.0...v1.14.0
|
||||
[1.13.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.12.0...v1.13.0
|
||||
[1.12.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.11.0...v1.12.0
|
||||
[1.11.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.10.0...v1.11.0
|
||||
[1.10.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.9.0...v1.10.0
|
||||
[1.9.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.8.0...v1.9.0
|
||||
[1.8.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.7.0...1.8.0
|
||||
[1.7.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.6.0...v1.7.0
|
||||
[1.6.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.5.0...v1.6.0
|
||||
[1.5.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.4.0...v1.5.0
|
||||
[1.4.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.3.0...v1.4.0
|
||||
[1.3.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.2.0...v1.3.0
|
||||
[1.2.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.1.0...v1.2.0
|
||||
[1.1.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.0.0...v1.1.0
|
||||
[1.0.0]: https://github.com/msfjarvis/compose-lobsters/compare/29c374859b17c5fcef03585b8a01c00070de9097...v1.0.0
|
||||
@@ -0,0 +1,17 @@
|
||||
# Hacking on compose-lobsters
|
||||
|
||||
This app is built as a very lightweight frontend for [lobste.rs](https://lobste.rs), mainly as an experiment in using [Jetpack Compose](https://d.android.com/jetpack/compose).
|
||||
|
||||
The codebase is fairly simple, and the package structure follows [Buffer's](https://buffer.com/resources/android-rethinking-package-structure/).
|
||||
|
||||
## Subproject breakdown
|
||||
|
||||
- `:api`: Retrofit-backed interface to the [lobste.rs] API
|
||||
|
||||
- `:app`: The guts of the Android client
|
||||
|
||||
- `:common`: Contains the abstractions we use for sharing strings and browser handling across desktop and mobile
|
||||
|
||||
- `:database`: Database layer implemented using SQLDelight to persist saved posts
|
||||
|
||||
- `:desktop`: Proof-of-concept desktop client that needs significantly more love than it has received so far
|
||||
@@ -1,23 +0,0 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Privacy policy for Claw
|
||||
|
||||
The app does not collect any information about you and thus does not share anything with me or any other third-party. The only difference between using this app versus the [lobste.rs website](https://lobste.rs/) is the `User-Agent` header which is set to `Claw/$VERSION_NAME/msfjarvis` in the app.
|
||||
@@ -1,33 +1,39 @@
|
||||
# Claw for [lobste.rs](https://lobste.rs) [](https://github.com/msfjarvis/compose-lobsters/actions/workflows/ci.yml)
|
||||
# Claw for [lobste.rs](https://lobste.rs)
|
||||
|
||||
Unofficial Android app for read-only access to [lobste.rs](https://lobste.rs), built with [Jetpack Compose](https://developer.android.com/jetpack/compose).
|
||||
Unofficial Android app for read-only access to [lobste.rs](https://lobste.rs) community, built with [Jetpack Compose](https://developer.android.com/jetpack/compose).
|
||||
|
||||
> [!WARNING]
|
||||
> This app uses a JSON API that Lobsters does not expose by choice and [the maintainer has no interest in having](https://github.com/lobsters/lobsters/issues/1663#issuecomment-3074472781), so it may break in any manner at any point in time. File an issue [here](https://github.com/msfjarvis/compose-lobsters/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen) if you come across any bugs, do not bother the upstream developers about a feature they don't support.
|
||||
Requires Android Studio 2020.3.1 Arctic Fox to work.
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=dev.msfjarvis.claw.android">
|
||||
<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"
|
||||
alt="Get it on Google Play"
|
||||
height="80" />
|
||||
</a>
|
||||
## Distribution
|
||||
|
||||
<a href="https://android.izzysoft.de/repo/apk/dev.msfjarvis.claw.android">
|
||||
<img src="./.github/izzy-badge.webp"
|
||||
alt="Get it on IzzyOnDroid"
|
||||
height="80" />
|
||||
</a>
|
||||
The app will be published to Google Play soon, in the mean time snapshots from the development branch are available [here](https://dl.msfjarvis.dev/Claw)
|
||||
|
||||
> [!NOTE]
|
||||
> Daily builds are published to the Google Play 'Open Testing' channel (if there are any changes), which can be opted into via this link: [[Android]](https://play.google.com/store/apps/details?id=dev.msfjarvis.claw.android) [[Web]](https://play.google.com/apps/testing/dev.msfjarvis.claw.android)
|
||||
## Licence
|
||||
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature_light.webp#gh-light-mode-only"
|
||||
alt="A grid of screenshots from the app, in clockwise order: the main screen, the comments page, the search page and the saved posts page"
|
||||
width="550" />
|
||||
```
|
||||
Copyright (c) 2020 Harsh Shandilya
|
||||
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature_dark.webp#gh-dark-mode-only"
|
||||
alt="A grid of screenshots from the app, in clockwise order: the main screen, the comments page, the search page and the saved posts page"
|
||||
width="550" />
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
## License
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
See [LICENSE](LICENSE)
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
```
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
/apk/
|
||||
/bundle/
|
||||
@@ -1,143 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import dev.msfjarvis.claw.gradle.addTestDependencies
|
||||
import dev.zacsweers.metro.gradle.DiagnosticSeverity
|
||||
|
||||
plugins {
|
||||
id("dev.msfjarvis.claw.android-application")
|
||||
id("dev.msfjarvis.claw.rename-artifacts")
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
id("dev.msfjarvis.claw.sentry")
|
||||
id("dev.msfjarvis.claw.versioning-plugin")
|
||||
id("kotlin-parcelize")
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
alias(libs.plugins.modulegraphassert)
|
||||
alias(libs.plugins.baselineprofile)
|
||||
alias(libs.plugins.licensee)
|
||||
alias(libs.plugins.kotlin.composeCompiler)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.dependencyAnalysis)
|
||||
alias(libs.plugins.metro)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.msfjarvis.claw.android"
|
||||
defaultConfig.applicationId = "dev.msfjarvis.claw.android"
|
||||
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
buildFeatures.compose = true
|
||||
buildTypes.create("internal") {
|
||||
matchingFallbacks += "release"
|
||||
signingConfig = signingConfigs["debug"]
|
||||
applicationIdSuffix = ".internal"
|
||||
isDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
aboutLibraries.collect.gitHubApiToken = providers.environmentVariable("GITHUB_TOKEN").orNull
|
||||
|
||||
baselineProfile {
|
||||
mergeIntoMain = true
|
||||
saveInSrc = true
|
||||
}
|
||||
|
||||
licensee {
|
||||
allow("Apache-2.0")
|
||||
allow("MIT")
|
||||
allow("BSD-3-Clause")
|
||||
ignoreDependencies("com.michael-bull.kotlin-result") { because("kotlin-result is ISC licensed") }
|
||||
ignoreDependencies("org.commonmark") { because("Commonmark is BSD licensed") }
|
||||
allowUrl("https://jsoup.org/license") { because("Jsoup is MIT licensed") }
|
||||
allowUrl("https://opensource.org/license/MIT") { because("Ksoup is MIT licensed") }
|
||||
}
|
||||
|
||||
metro { unusedGraphInputsSeverity = DiagnosticSeverity.ERROR }
|
||||
|
||||
moduleGraphAssert {
|
||||
assertOnAnyBuild = true
|
||||
maxHeight = 4
|
||||
restricted = arrayOf(":core -X> :.*")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
baselineProfile(projects.benchmark)
|
||||
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.aboutLibraries.compose.core)
|
||||
implementation(libs.aboutLibraries.core)
|
||||
implementation(libs.aboutLibraries.m3)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.animation)
|
||||
implementation(libs.androidx.compose.animation.core)
|
||||
implementation(libs.androidx.compose.foundation)
|
||||
implementation(libs.androidx.compose.foundation.layout)
|
||||
implementation(libs.androidx.compose.glance)
|
||||
implementation(libs.androidx.compose.glance.core)
|
||||
implementation(libs.androidx.compose.glance.m3)
|
||||
implementation(libs.androidx.compose.material.icons.core)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material3.adaptive)
|
||||
implementation(libs.androidx.compose.material3.adaptive.layout)
|
||||
implementation(libs.androidx.compose.material3.window.size)
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
implementation(libs.androidx.compose.runtime.saveable)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
implementation(libs.androidx.compose.ui.unit)
|
||||
implementation(libs.androidx.annotation)
|
||||
implementation(libs.androidx.collection)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.androidx.lifecycle.common)
|
||||
implementation(libs.androidx.lifecycle.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel)
|
||||
implementation(libs.androidx.material3.navigation3)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
implementation(libs.androidx.paging.common)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.eithernet)
|
||||
implementation(libs.haze)
|
||||
implementation(libs.haze.blur)
|
||||
implementation(libs.kotlin.parcelize.runtime)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.metrox.android)
|
||||
implementation(libs.metrox.viewmodel)
|
||||
implementation(libs.metrox.viewmodel.compose)
|
||||
implementation(libs.okhttp.core)
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.sentry)
|
||||
implementation(libs.sentry.android.core)
|
||||
implementation(libs.sqldelight.extensions.coroutines)
|
||||
implementation(libs.sqldelight.runtime)
|
||||
implementation(libs.swipe)
|
||||
implementation(libs.unfurl)
|
||||
implementation(projects.api)
|
||||
implementation(projects.common)
|
||||
implementation(projects.core)
|
||||
implementation(projects.database.core)
|
||||
implementation(projects.database.impl)
|
||||
implementation(projects.model)
|
||||
|
||||
compileOnly(libs.androidx.compose.glance.preview)
|
||||
|
||||
debugRuntimeOnly(libs.androidx.compose.glance)
|
||||
|
||||
runtimeOnly(libs.androidx.profileinstaller)
|
||||
|
||||
addTestDependencies(project)
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 9.2.1" type="baseline" client="gradle" dependencies="true" name="AGP (9.2.1)" variant="all" version="9.2.1">
|
||||
|
||||
<issue
|
||||
id="Instantiatable"
|
||||
message="This class should provide a default constructor (a public constructor with no arguments) (`dev.msfjarvis.claw.android.MainActivity`)"
|
||||
errorLine1=" android:name=".MainActivity""
|
||||
errorLine2=" ~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="28"
|
||||
column="21"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DeprecatedCall"
|
||||
message="android.webkit.WebView.loadUrl is deprecated; consider using an alternative."
|
||||
errorLine1=" webViewRef?.loadUrl(LOGIN_URL)"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="69"
|
||||
column="15"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DeprecatedCall"
|
||||
message="android.webkit.WebView.loadUrl is deprecated; consider using an alternative."
|
||||
errorLine1=" webViewRef?.loadUrl(LOGIN_URL)"
|
||||
errorLine2=" ~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="69"
|
||||
column="35"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DeprecatedCall"
|
||||
message="android.webkit.WebView.WebView is deprecated; consider using an alternative."
|
||||
errorLine1=" WebView(context)"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="78"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DeprecatedCall"
|
||||
message="android.webkit.WebView.WebView is deprecated; consider using an alternative."
|
||||
errorLine1=" WebView(context)"
|
||||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="78"
|
||||
column="21"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DeprecatedCall"
|
||||
message="android.webkit.WebView.getSettings is deprecated; consider using an alternative."
|
||||
errorLine1=" settings.javaScriptEnabled = true"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="81"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DeprecatedCall"
|
||||
message="android.webkit.WebView.setWebViewClient is deprecated; consider using an alternative."
|
||||
errorLine1=" webViewClient ="
|
||||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="82"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DeprecatedCall"
|
||||
message="android.webkit.WebView.setWebViewClient is deprecated; consider using an alternative."
|
||||
errorLine1=" object : WebViewClient() {"
|
||||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="83"
|
||||
column="19"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DeprecatedCall"
|
||||
message="android.webkit.WebView.loadUrl is deprecated; consider using an alternative."
|
||||
errorLine1=" loadUrl(LOGIN_URL)"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="118"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DeprecatedCall"
|
||||
message="android.webkit.WebView.loadUrl is deprecated; consider using an alternative."
|
||||
errorLine1=" loadUrl(LOGIN_URL)"
|
||||
errorLine2=" ~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="118"
|
||||
column="25"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="MemberExtensionConflict"
|
||||
message="`KeepUntilTransitionsFinished` is defined both as a member in class `androidx.compose.animation.ExitTransition.Companion` and an extension in package `androidx.compose.animation`. The defined behavior for this is to use the member, but since the extension is explicitly imported into this file, there's a chance that this was not expected. (One common way this happens is for members to be added to a class after code was already written to use an extension)."
|
||||
errorLine1=" ) togetherWith ExitTransition.KeepUntilTransitionsFinished"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt"
|
||||
line="259"
|
||||
column="49"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
||||
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 42 KiB |
@@ -1,19 +0,0 @@
|
||||
-dontwarn androidx.window.extensions.WindowExtensions
|
||||
-dontwarn androidx.window.extensions.WindowExtensionsProvider
|
||||
-dontwarn androidx.window.extensions.area.ExtensionWindowAreaPresentation
|
||||
-dontwarn androidx.window.extensions.core.util.function.Consumer
|
||||
-dontwarn androidx.window.extensions.core.util.function.Function
|
||||
-dontwarn androidx.window.extensions.core.util.function.Predicate
|
||||
-dontwarn androidx.window.extensions.layout.DisplayFeature
|
||||
-dontwarn androidx.window.extensions.layout.FoldingFeature
|
||||
-dontwarn androidx.window.extensions.layout.WindowLayoutComponent
|
||||
-dontwarn androidx.window.extensions.layout.WindowLayoutInfo
|
||||
-dontwarn androidx.window.sidecar.SidecarDeviceState
|
||||
-dontwarn androidx.window.sidecar.SidecarDisplayFeature
|
||||
-dontwarn androidx.window.sidecar.SidecarInterface$SidecarCallback
|
||||
-dontwarn androidx.window.sidecar.SidecarInterface
|
||||
-dontwarn androidx.window.sidecar.SidecarProvider
|
||||
-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo
|
||||
-dontwarn io.mcarle.konvert.api.GeneratedKonverter
|
||||
-dontwarn com.google.re2j.Matcher
|
||||
-dontwarn com.google.re2j.Pattern
|
||||
@@ -1,2 +0,0 @@
|
||||
-dontobfuscate
|
||||
-keepattributes SourceFile, LineNumberTable
|
||||
@@ -1,13 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Performing the heap growth analysis in process requires more heap. -->
|
||||
<application
|
||||
android:largeHeap="true"/>
|
||||
</manifest>
|
||||
@@ -1,46 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="claw-debug"
|
||||
android:host="comments" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="claw-debug"
|
||||
android:host="hottest" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="claw-debug"
|
||||
android:host="newest" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="claw-debug"
|
||||
android:host="saved" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,9 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">Claw β</string>
|
||||
</resources>
|
||||
@@ -1,9 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">Claw 🤫</string>
|
||||
</resources>
|
||||
@@ -1,122 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application
|
||||
android:name=".ClawApplication"
|
||||
android:appComponentFactory="dev.msfjarvis.claw.android.BackupSafeAppComponentFactory"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/full_backup_content"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Claw.SplashScreen"
|
||||
tools:replace="android:appComponentFactory"
|
||||
tools:targetApi="tiramisu">
|
||||
<profileable
|
||||
android:shell="true"
|
||||
tools:targetApi="29" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="assetsPaths|colorMode|density|fontScale|fontWeightAdjustment|grammaticalGender|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|resourcesUnused|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="claw" android:host="comments" android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="claw" android:host="hottest" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="claw" android:host="newest" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="claw" android:host="saved" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".glance.WidgetClickActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity="" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
<receiver
|
||||
android:name=".glance.SavedPostsWidgetReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/saved_posts_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".glance.HottestPostsWidgetReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/hottest_posts_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<!-- Required: set your sentry.io project identifier (DSN) -->
|
||||
<meta-data
|
||||
android:name="io.sentry.dsn"
|
||||
android:value="${sentryDsn}" />
|
||||
<!-- enable automatic breadcrumbs for user interactions (clicks, swipes, scrolls) -->
|
||||
<meta-data
|
||||
android:name="io.sentry.traces.user-interaction.enable"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="io.sentry.breadcrumbs.user-interaction"
|
||||
android:value="true" />
|
||||
<!-- enable screenshot for crashes -->
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-screenshot"
|
||||
android:value="true" />
|
||||
<!-- enable view hierarchy for crashes -->
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-view-hierarchy"
|
||||
android:value="true" />
|
||||
<!-- set the environment for this build -->
|
||||
<meta-data android:name="io.sentry.environment" android:value="${sentryEnvironment}" />
|
||||
<meta-data android:name="io.sentry.auto-init" android:value="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android
|
||||
|
||||
import android.app.Application
|
||||
import dev.zacsweers.metrox.android.MetroAppComponentFactory
|
||||
|
||||
/**
|
||||
* Custom variant of [android.app.AppComponentFactory] that skips [ClawApplication] init when the
|
||||
* app is running in a backup process for Android Auto Backup. In all normal cases, it will defer to
|
||||
* [MetroAppComponentFactory].
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class BackupSafeAppComponentFactory : MetroAppComponentFactory() {
|
||||
|
||||
override fun instantiateApplicationCompat(cl: ClassLoader, className: String): Application {
|
||||
if (isBackupAgentProcess()) {
|
||||
return Application()
|
||||
}
|
||||
return super.instantiateApplicationCompat(cl, className)
|
||||
}
|
||||
|
||||
private fun isBackupAgentProcess(): Boolean {
|
||||
return try {
|
||||
val processName = Application.getProcessName()
|
||||
processName?.contains("backup") == true
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import dev.msfjarvis.claw.android.glance.SavedPostsWidgetReceiver
|
||||
import dev.msfjarvis.claw.android.injection.AppGraph
|
||||
import dev.msfjarvis.claw.android.work.SavedPostUpdaterWorker
|
||||
import dev.msfjarvis.claw.android.work.TagExpirationCleanupWorker
|
||||
import dev.zacsweers.metro.createGraphFactory
|
||||
import dev.zacsweers.metrox.android.MetroAppComponentProviders
|
||||
import dev.zacsweers.metrox.android.MetroApplication
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ClawApplication : Application(), MetroApplication {
|
||||
|
||||
val appGraph by lazy { createGraphFactory<AppGraph.Factory>().create(this) }
|
||||
|
||||
override val appComponentProviders: MetroAppComponentProviders
|
||||
get() = appGraph
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
appGraph.plugins.forEach { plugin -> plugin.apply(this) }
|
||||
val postUpdateWorkRequest =
|
||||
PeriodicWorkRequestBuilder<SavedPostUpdaterWorker>(24, TimeUnit.HOURS)
|
||||
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
.build()
|
||||
appGraph.workManager.enqueueUniquePeriodicWork(
|
||||
uniqueWorkName = "updateSavedPosts",
|
||||
existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.KEEP,
|
||||
request = postUpdateWorkRequest,
|
||||
)
|
||||
val tagCleanupWorkRequest =
|
||||
PeriodicWorkRequestBuilder<TagExpirationCleanupWorker>(24, TimeUnit.HOURS).build()
|
||||
appGraph.workManager.enqueueUniquePeriodicWork(
|
||||
uniqueWorkName = "cleanupExpiredTags",
|
||||
existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.KEEP,
|
||||
request = tagCleanupWorkRequest,
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
GlobalScope.launch(appGraph.mainDispatcher) {
|
||||
try {
|
||||
GlanceAppWidgetManager(applicationContext)
|
||||
.setWidgetPreviews(SavedPostsWidgetReceiver::class)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Workaround for Motorola Android 15 bug where AppWidgetServiceImpl incorrectly
|
||||
// reports registered widgets as invalid when setting previews.
|
||||
// See: https://claw.sentry.io/share/issue/f8ee09821d8840b9b86293d3ffb627d3/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistContent
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Comments
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Hottest
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Newest
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Saved
|
||||
import dev.msfjarvis.claw.android.ui.screens.LobstersPostsScreen
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.core.injection.InjectedViewModelFactory
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoMap
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.binding
|
||||
import dev.zacsweers.metrox.android.ActivityKey
|
||||
import dev.zacsweers.metrox.viewmodel.LocalMetroViewModelFactory
|
||||
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
@ContributesIntoMap(AppScope::class, binding<Activity>())
|
||||
@ActivityKey
|
||||
@Inject
|
||||
class MainActivity(
|
||||
private val uriHandler: UriHandler,
|
||||
private val viewModelFactory: InjectedViewModelFactory,
|
||||
) : ComponentActivity() {
|
||||
|
||||
override val defaultViewModelProviderFactory: ViewModelProvider.Factory
|
||||
get() = viewModelFactory
|
||||
|
||||
var webUri: String? = null
|
||||
var deepLinkDestination by mutableStateOf<NavKey?>(null)
|
||||
private set
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
installSplashScreen()
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT)
|
||||
// Don't set navigation bar style, the default matches the platform behavior.
|
||||
)
|
||||
handleIntent(intent)
|
||||
setContent {
|
||||
LobstersTheme(
|
||||
dynamicColor = true,
|
||||
providedValues =
|
||||
arrayOf(
|
||||
LocalUriHandler provides uriHandler,
|
||||
LocalMetroViewModelFactory provides viewModelFactory,
|
||||
),
|
||||
) {
|
||||
val windowSizeClass = calculateWindowSizeClass(this)
|
||||
LobstersPostsScreen(
|
||||
uriHandler = uriHandler,
|
||||
windowSizeClass = windowSizeClass,
|
||||
setWebUri = { url -> webUri = url },
|
||||
deepLinkDestination = deepLinkDestination,
|
||||
clearDeepLink = { deepLinkDestination = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent) {
|
||||
val data = intent.data
|
||||
if (data != null && data.scheme == BuildConfig.DEEPLINK_SCHEME) {
|
||||
when (data.host) {
|
||||
"comments" -> {
|
||||
val postId = data.pathSegments.firstOrNull()
|
||||
if (postId != null) {
|
||||
deepLinkDestination = Comments(postId)
|
||||
}
|
||||
}
|
||||
"newest" -> deepLinkDestination = Newest
|
||||
"hottest" -> deepLinkDestination = Hottest
|
||||
"saved" -> deepLinkDestination = Saved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent?) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
val uri = webUri
|
||||
if (outContent != null) {
|
||||
if (uri != null) {
|
||||
outContent.webUri = uri.toUri()
|
||||
} else {
|
||||
outContent.webUri = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.LocalContext
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.action.actionStartActivity
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.appwidget.lazy.items
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.background
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.msfjarvis.claw.android.BuildConfig
|
||||
import dev.msfjarvis.claw.android.ClawApplication
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.database.local.CachedRemotePost
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.fromCachedRemotePost
|
||||
import dev.msfjarvis.claw.model.toUIPost
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class HottestPostsWidget : GlanceAppWidget() {
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val appGraph = (context.applicationContext as ClawApplication).appGraph
|
||||
val cachedRemotePostsRepository = appGraph.cachedRemotePostsRepository
|
||||
val tagBlockRepository = appGraph.tagBlockRepository
|
||||
val filteredTags =
|
||||
try {
|
||||
tagBlockRepository.getSavedTags().first()
|
||||
} catch (_: Exception) {
|
||||
emptySet()
|
||||
}
|
||||
val posts =
|
||||
when (val postsResult = appGraph.lobstersApi.getHottestPosts(1)) {
|
||||
is ApiResult.Success -> {
|
||||
val uiPosts = postsResult.value.posts.map(LobstersPost::toUIPost)
|
||||
// Cache the posts for future use when network is unavailable
|
||||
// Using try-catch to prevent widget rendering failures if caching fails
|
||||
try {
|
||||
val cachedPosts = uiPosts.mapIndexed { index, post ->
|
||||
CachedRemotePost(
|
||||
shortId = post.shortId,
|
||||
title = post.title,
|
||||
url = post.url,
|
||||
createdAt = post.createdAt,
|
||||
commentCount = post.commentCount,
|
||||
commentsUrl = post.commentsUrl,
|
||||
submitterName = post.submitter,
|
||||
tags = post.tags,
|
||||
description = post.description,
|
||||
userIsAuthor = post.userIsAuthor,
|
||||
insertionOrder = index,
|
||||
)
|
||||
}
|
||||
cachedRemotePostsRepository.savePosts(cachedPosts)
|
||||
} catch (_: Exception) {
|
||||
// Silently ignore caching failures - widget should still render
|
||||
}
|
||||
uiPosts.toImmutableList()
|
||||
}
|
||||
else -> {
|
||||
// Fall back to cached posts when network fails
|
||||
cachedRemotePostsRepository
|
||||
.getCachedPosts()
|
||||
.map(UIPost::fromCachedRemotePost)
|
||||
.toImmutableList()
|
||||
}
|
||||
}
|
||||
// Filter out posts with tags that are in the filtered tags set
|
||||
val filteredPosts =
|
||||
if (filteredTags.isEmpty()) {
|
||||
posts
|
||||
} else {
|
||||
posts
|
||||
.filter { post -> post.tags.none { tag -> filteredTags.contains(tag) } }
|
||||
.toImmutableList()
|
||||
}
|
||||
provideContent { LobstersGlanceTheme { Content(context, filteredPosts) } }
|
||||
}
|
||||
|
||||
override suspend fun providePreview(context: Context, widgetCategory: Int) {
|
||||
provideContent { Content(context, samplePosts()) }
|
||||
}
|
||||
|
||||
@SuppressLint("ComposeUnstableReceiver")
|
||||
@Composable
|
||||
private fun Content(
|
||||
context: Context,
|
||||
posts: ImmutableList<UIPost>,
|
||||
modifier: GlanceModifier = GlanceModifier,
|
||||
) {
|
||||
WidgetContainer(
|
||||
"Hottest posts",
|
||||
listContent = {
|
||||
items(posts) { post ->
|
||||
Box(GlanceModifier.padding(horizontal = 16.dp, vertical = 4.dp)) { WidgetPostEntry(post) }
|
||||
}
|
||||
item {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = GlanceModifier.fillMaxWidth().padding(16.dp),
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier =
|
||||
GlanceModifier.background(GlanceTheme.colors.primary)
|
||||
.cornerRadius(20.dp)
|
||||
.padding(horizontal = 24.dp, vertical = 10.dp)
|
||||
.clickable(
|
||||
actionStartActivity(
|
||||
Intent(Intent.ACTION_VIEW, "${BuildConfig.DEEPLINK_SCHEME}://hottest".toUri())
|
||||
.apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
setClass(LocalContext.current, MainActivity::class.java)
|
||||
}
|
||||
)
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = context.getString(R.string.see_more_posts),
|
||||
style =
|
||||
TextStyle(color = GlanceTheme.colors.onPrimary, textAlign = TextAlign.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoMap
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.binding
|
||||
import dev.zacsweers.metrox.android.BroadcastReceiverKey
|
||||
|
||||
@ContributesIntoMap(AppScope::class, binding<BroadcastReceiver>())
|
||||
@BroadcastReceiverKey
|
||||
@Inject
|
||||
class HottestPostsWidgetReceiver() : GlanceAppWidgetReceiver() {
|
||||
|
||||
override val glanceAppWidget: GlanceAppWidget
|
||||
get() = HottestPostsWidget()
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.glance.GlanceComposable
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.LocalContext
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.action.actionStartActivity
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.appwidget.lazy.items
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.background
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.preview.ExperimentalGlancePreviewApi
|
||||
import androidx.glance.preview.Preview
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import dev.msfjarvis.claw.android.BuildConfig
|
||||
import dev.msfjarvis.claw.android.ClawApplication
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.fromSavedPost
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class SavedPostsWidget : GlanceAppWidget() {
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val appGraph = (context.applicationContext as ClawApplication).appGraph
|
||||
val posts = appGraph.savedPostsRepository.getRecentPosts(50)
|
||||
val postWindow = posts.first().map(UIPost::fromSavedPost).toImmutableList()
|
||||
provideContent { LobstersGlanceTheme { Content(postWindow) } }
|
||||
}
|
||||
|
||||
override suspend fun providePreview(context: Context, widgetCategory: Int) {
|
||||
provideContent { Content(samplePosts()) }
|
||||
}
|
||||
|
||||
@SuppressLint("ComposeUnstableReceiver")
|
||||
@Composable
|
||||
private fun Content(posts: ImmutableList<UIPost>, modifier: GlanceModifier = GlanceModifier) {
|
||||
val context = LocalContext.current
|
||||
WidgetContainer(
|
||||
title = context.getString(R.string.saved_posts),
|
||||
listContent = {
|
||||
items(posts) { post ->
|
||||
Box(GlanceModifier.padding(horizontal = 16.dp, vertical = 4.dp)) { WidgetPostEntry(post) }
|
||||
}
|
||||
item {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = GlanceModifier.fillMaxWidth().padding(16.dp),
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier =
|
||||
GlanceModifier.background(GlanceTheme.colors.primary)
|
||||
.cornerRadius(20.dp)
|
||||
.padding(horizontal = 24.dp, vertical = 10.dp)
|
||||
.clickable(
|
||||
actionStartActivity(
|
||||
Intent(Intent.ACTION_VIEW, "${BuildConfig.DEEPLINK_SCHEME}://saved".toUri())
|
||||
.apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
setClass(LocalContext.current, MainActivity::class.java)
|
||||
}
|
||||
)
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = context.getString(R.string.see_more_posts),
|
||||
style =
|
||||
TextStyle(color = GlanceTheme.colors.onPrimary, textAlign = TextAlign.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("ComposeUnstableReceiver")
|
||||
@OptIn(ExperimentalGlancePreviewApi::class)
|
||||
@Preview
|
||||
@GlanceComposable
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
Content(samplePosts())
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoMap
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.binding
|
||||
import dev.zacsweers.metrox.android.BroadcastReceiverKey
|
||||
|
||||
@ContributesIntoMap(AppScope::class, binding<BroadcastReceiver>())
|
||||
@BroadcastReceiverKey
|
||||
@Inject
|
||||
class SavedPostsWidgetReceiver() : GlanceAppWidgetReceiver() {
|
||||
|
||||
override val glanceAppWidget: GlanceAppWidget
|
||||
get() = SavedPostsWidget()
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.net.toUri
|
||||
import dev.msfjarvis.claw.android.BuildConfig
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import io.sentry.Breadcrumb
|
||||
import io.sentry.Sentry
|
||||
import io.sentry.SentryLevel
|
||||
|
||||
/**
|
||||
* Trampoline activity for handling widget clicks with explicit components.
|
||||
*
|
||||
* This activity is required to fix COMPOSE-LOBSTERS-9X/9P/8M. Glance's LazyColumn requires all
|
||||
* actionStartActivity intents to have an explicit component set. For external URLs, we route
|
||||
* through this activity which then launches the appropriate intent (either internal deep link or
|
||||
* external browser).
|
||||
*
|
||||
* @see:
|
||||
* https://developer.android.com/reference/kotlin/androidx/glance/appwidget/action/package-summary#actionStartActivity(android.content.Intent,androidx.glance.action.ActionParameters)
|
||||
*/
|
||||
class WidgetClickActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val url = intent.getStringExtra(EXTRA_URL)
|
||||
val shortId = intent.getStringExtra(EXTRA_SHORT_ID)
|
||||
val action = intent.action
|
||||
|
||||
when {
|
||||
// Handle comments deep link
|
||||
action == ACTION_OPEN_COMMENTS && shortId != null -> {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb().apply {
|
||||
category = "widget_click_debug"
|
||||
message = "Comment deeplink"
|
||||
level = SentryLevel.INFO
|
||||
}
|
||||
)
|
||||
val commentsIntent =
|
||||
Intent(Intent.ACTION_VIEW, "${BuildConfig.DEEPLINK_SCHEME}://comments/$shortId".toUri())
|
||||
.apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
setClass(this@WidgetClickActivity, MainActivity::class.java)
|
||||
}
|
||||
startActivity(commentsIntent)
|
||||
}
|
||||
// Handle external URL
|
||||
url != null && (url.startsWith("http://") || url.startsWith("https://")) -> {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb().apply {
|
||||
category = "widget_click_debug"
|
||||
message = "External URL"
|
||||
level = SentryLevel.INFO
|
||||
}
|
||||
)
|
||||
val urlIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(urlIntent)
|
||||
}
|
||||
// Handle internal/relative URL as comments link
|
||||
url != null && (url.startsWith('/') || url.isEmpty()) -> {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb().apply {
|
||||
category = "widget_click_debug"
|
||||
message = "Relative URL, parsed as a comment link"
|
||||
level = SentryLevel.INFO
|
||||
}
|
||||
)
|
||||
// For internal URLs without shortId, extract it from the URL or use comments action
|
||||
val commentsIntent =
|
||||
Intent(Intent.ACTION_VIEW, "${BuildConfig.DEEPLINK_SCHEME}://comments/$shortId".toUri())
|
||||
.apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
setClass(this@WidgetClickActivity, MainActivity::class.java)
|
||||
}
|
||||
startActivity(commentsIntent)
|
||||
}
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_OPEN_COMMENTS = "dev.msfjarvis.claw.OPEN_COMMENTS"
|
||||
const val EXTRA_URL = "dev.msfjarvis.claw.EXTRA_URL"
|
||||
const val EXTRA_SHORT_ID = "dev.msfjarvis.claw.EXTRA_SHORT_ID"
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.ColorFilter
|
||||
import androidx.glance.GlanceComposable
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.LocalContext
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.action.actionStartActivity
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.background
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.preview.ExperimentalGlancePreviewApi
|
||||
import androidx.glance.preview.Preview
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
@SuppressLint("ArgInFormattedQuantityStringRes")
|
||||
@Composable
|
||||
@GlanceComposable
|
||||
fun WidgetPostEntry(post: UIPost, modifier: GlanceModifier = GlanceModifier) {
|
||||
val titleStyle = MaterialTheme.typography.bodyMedium
|
||||
val context = LocalContext.current
|
||||
val commentsAction =
|
||||
actionStartActivity(
|
||||
Intent(context, WidgetClickActivity::class.java).apply {
|
||||
action = WidgetClickActivity.ACTION_OPEN_COMMENTS
|
||||
putExtra(WidgetClickActivity.EXTRA_SHORT_ID, post.shortId)
|
||||
}
|
||||
)
|
||||
val postAction =
|
||||
if (post.url.startsWith('/') || post.url.isEmpty()) commentsAction
|
||||
else
|
||||
actionStartActivity(
|
||||
Intent(context, WidgetClickActivity::class.java).apply {
|
||||
putExtra(WidgetClickActivity.EXTRA_URL, post.url)
|
||||
putExtra(WidgetClickActivity.EXTRA_SHORT_ID, post.shortId)
|
||||
}
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
modifier.fillMaxWidth().cornerRadius(8.dp).background(GlanceTheme.colors.secondaryContainer),
|
||||
) {
|
||||
Text(
|
||||
text = post.title,
|
||||
modifier =
|
||||
GlanceModifier.defaultWeight()
|
||||
.clickable(postAction)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.padding(start = 4.dp),
|
||||
maxLines = 1,
|
||||
style =
|
||||
TextStyle(color = GlanceTheme.colors.onSecondaryContainer, fontSize = titleStyle.fontSize),
|
||||
)
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.ic_comment),
|
||||
contentDescription =
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.comments,
|
||||
post.commentCount,
|
||||
post.commentCount,
|
||||
),
|
||||
colorFilter = ColorFilter.tint(GlanceTheme.colors.onSecondaryContainer),
|
||||
modifier =
|
||||
GlanceModifier.clickable(commentsAction)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalGlancePreviewApi::class)
|
||||
@Preview
|
||||
@GlanceComposable
|
||||
@Composable
|
||||
private fun WidgetPostEntryPreview() {
|
||||
WidgetPostEntry(samplePosts(1).first())
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.glance.GlanceComposable
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.appwidget.lazy.LazyColumn
|
||||
import androidx.glance.appwidget.lazy.LazyListScope
|
||||
import androidx.glance.background
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.material3.ColorProviders
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import dev.msfjarvis.claw.common.posts.TEST_POST
|
||||
import dev.msfjarvis.claw.common.theme.DarkThemeColors
|
||||
import dev.msfjarvis.claw.common.theme.LightThemeColors
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Composable
|
||||
fun LobstersGlanceTheme(content: @GlanceComposable @Composable () -> Unit) {
|
||||
GlanceTheme(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) GlanceTheme.colors
|
||||
else ColorProviders(light = LightThemeColors, dark = DarkThemeColors),
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WidgetContainer(
|
||||
title: String,
|
||||
listContent: LazyListScope.() -> Unit,
|
||||
modifier: GlanceModifier = GlanceModifier,
|
||||
) {
|
||||
Column(
|
||||
modifier.fillMaxSize().background(GlanceTheme.colors.widgetBackground).cornerRadius(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = GlanceModifier.padding(16.dp).fillMaxWidth(),
|
||||
) {
|
||||
Text(text = title, style = TextStyle(fontSize = 18.sp, color = GlanceTheme.colors.onSurface))
|
||||
}
|
||||
LazyColumn(content = listContent)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("VisibleForTests")
|
||||
fun samplePosts(
|
||||
count: Int = 5,
|
||||
mutator: UIPost.(idx: Int) -> UIPost = { this },
|
||||
): PersistentList<UIPost> {
|
||||
return buildList(count) { repeat(count) { add(TEST_POST.mutator(it)) } }.toPersistentList()
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkManager
|
||||
import dev.msfjarvis.claw.android.viewmodel.CachedRemotePostsRepository
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.common.tags.TagBlockRepository
|
||||
import dev.msfjarvis.claw.core.coroutines.MainDispatcher
|
||||
import dev.msfjarvis.claw.core.injection.AppPlugin
|
||||
import dev.msfjarvis.claw.core.injection.InjectedWorkerFactory
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.DependencyGraph
|
||||
import dev.zacsweers.metro.Multibinds
|
||||
import dev.zacsweers.metro.Provides
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import dev.zacsweers.metrox.android.MetroAppComponentProviders
|
||||
import dev.zacsweers.metrox.viewmodel.ViewModelGraph
|
||||
import kotlin.reflect.KClass
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
@DependencyGraph(AppScope::class)
|
||||
interface AppGraph : MetroAppComponentProviders, ViewModelGraph {
|
||||
val plugins: Set<AppPlugin>
|
||||
|
||||
val workManager: WorkManager
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesWorkManager(application: Context): WorkManager {
|
||||
return WorkManager.getInstance(application)
|
||||
}
|
||||
|
||||
@Multibinds
|
||||
val workerProviders:
|
||||
Map<KClass<out ListenableWorker>, () -> InjectedWorkerFactory.WorkerInstanceFactory<*>>
|
||||
|
||||
val workerFactory: InjectedWorkerFactory
|
||||
|
||||
val savedPostsRepository: SavedPostsRepository
|
||||
|
||||
val cachedRemotePostsRepository: CachedRemotePostsRepository
|
||||
|
||||
val lobstersApi: LobstersApi
|
||||
|
||||
val tagBlockRepository: TagBlockRepository
|
||||
|
||||
@MainDispatcher val mainDispatcher: CoroutineDispatcher
|
||||
|
||||
@DependencyGraph.Factory
|
||||
fun interface Factory {
|
||||
fun create(@Provides context: Context): AppGraph
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
|
||||
import android.util.Log
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.BindingContainer
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.Provides
|
||||
import me.saket.unfurl.UnfurlLogger
|
||||
import me.saket.unfurl.Unfurler
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
@BindingContainer
|
||||
@ContributesTo(AppScope::class)
|
||||
object MetadataExtractorModule {
|
||||
@Provides
|
||||
fun provideUnfurlLogger(): UnfurlLogger {
|
||||
return object : UnfurlLogger {
|
||||
override fun log(message: String) {
|
||||
Log.d("Unfurler", message)
|
||||
}
|
||||
|
||||
override fun log(e: Throwable, message: String) {
|
||||
Log.e("Unfurler", message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideUnfurler(okHttpClient: OkHttpClient, logger: UnfurlLogger): Unfurler {
|
||||
return Unfurler(httpClient = okHttpClient, logger = logger)
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
|
||||
import android.app.Application
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.core.injection.AppPlugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.sentry.SentryOptions
|
||||
import io.sentry.TypeCheckHint.OKHTTP_REQUEST
|
||||
import io.sentry.android.core.SentryAndroid
|
||||
import okhttp3.Request
|
||||
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class SentryConfigurationPlugin : AppPlugin {
|
||||
override fun apply(application: Application) {
|
||||
SentryAndroid.init(application) { options ->
|
||||
options.beforeSend = SentryOptions.BeforeSendCallback { event, hint ->
|
||||
val request = hint.getAs(OKHTTP_REQUEST, Request::class.java)
|
||||
|
||||
// Drop all OkHttp errors that are not about Lobsters specifically.
|
||||
if (request != null && !LobstersApi.BASE_URL.contains(request.url.host)) {
|
||||
null
|
||||
} else {
|
||||
event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerFactory
|
||||
import dev.msfjarvis.claw.core.injection.AppPlugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class WorkManagerPlugin(private val workerFactory: WorkerFactory) : AppPlugin {
|
||||
override fun apply(application: Application) {
|
||||
WorkManager.initialize(
|
||||
application,
|
||||
Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.paging
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.slack.eithernet.ApiResult.Failure
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import dev.msfjarvis.claw.api.toError
|
||||
import dev.msfjarvis.claw.core.coroutines.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.toUIPost
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@AssistedInject
|
||||
class LobstersPagingSource(
|
||||
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
|
||||
@param:IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : PagingSource<Int, UIPost>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UIPost> {
|
||||
val page = params.key ?: STARTING_PAGE_INDEX
|
||||
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
|
||||
is Success ->
|
||||
LoadResult.Page(
|
||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||
data = result.value.map(LobstersPost::toUIPost),
|
||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||
nextKey = page + 1,
|
||||
)
|
||||
is Failure.NetworkFailure -> LoadResult.Error(result.error)
|
||||
is Failure.UnknownFailure -> LoadResult.Error(result.error)
|
||||
is Failure.HttpFailure -> LoadResult.Error(result.toError())
|
||||
is Failure.ApiFailure -> LoadResult.Error(IOException("API returned an invalid response"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, UIPost>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(remoteFetcher: RemoteFetcher<LobstersPost>): LobstersPagingSource
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PAGE_SIZE = 25
|
||||
const val STARTING_PAGE_INDEX = 1
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.paging
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
|
||||
/** SAM interface to abstract over a remote API that fetches paginated content. */
|
||||
fun interface RemoteFetcher<T> {
|
||||
suspend fun getItemsAtPage(page: Int): ApiResult<List<T>, Unit>
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.paging
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.api.toError
|
||||
import dev.msfjarvis.claw.core.coroutines.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.toUIPost
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Specialized variant of [LobstersPagingSource] to prevent hammering the Lobsters search endpoint
|
||||
* with unnecessary requests. Rather than abstract out the API call this paging source takes in the
|
||||
* relevant parameters to short-circuit when there is no query specified in order to avoid calling
|
||||
* the API at all.
|
||||
*/
|
||||
@AssistedInject
|
||||
class SearchPagingSource(
|
||||
private val searchApi: LobstersSearchApi,
|
||||
@Assisted private val queryProvider: () -> String,
|
||||
@param:IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : PagingSource<Int, UIPost>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UIPost> {
|
||||
val searchQuery = queryProvider()
|
||||
// If there is no query, we don't need to call the API at all.
|
||||
if (searchQuery.isEmpty()) {
|
||||
return LoadResult.Page(itemsBefore = 0, data = emptyList(), prevKey = null, nextKey = null)
|
||||
}
|
||||
val page = params.key ?: STARTING_PAGE_INDEX
|
||||
val result = withContext(ioDispatcher) { searchApi.searchPosts(searchQuery, page) }
|
||||
return when (result) {
|
||||
is ApiResult.Success -> {
|
||||
// Optimization: prevent fetching more pages if we found no items, this means
|
||||
// there is no active search query.
|
||||
val nextKey = if (result.value.isEmpty()) null else page + 1
|
||||
LoadResult.Page(
|
||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||
data = result.value.map(LobstersPost::toUIPost),
|
||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||
nextKey = nextKey,
|
||||
)
|
||||
}
|
||||
is ApiResult.Failure.NetworkFailure -> LoadResult.Error(result.error)
|
||||
is ApiResult.Failure.UnknownFailure -> LoadResult.Error(result.error)
|
||||
is ApiResult.Failure.HttpFailure -> LoadResult.Error(result.toError())
|
||||
is ApiResult.Failure.ApiFailure ->
|
||||
LoadResult.Error(IOException("API returned an invalid response"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, UIPost>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(queryProvider: () -> String): SearchPagingSource
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.decorations
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Tune
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Search
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Settings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ClawAppBar(
|
||||
activity: Activity?,
|
||||
isTopLevel: Boolean,
|
||||
navigateTo: (NavKey) -> Unit,
|
||||
popBackStack: () -> NavKey?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier.shadow(8.dp),
|
||||
navigationIcon = {
|
||||
if (!isTopLevel) {
|
||||
IconButton(onClick = { if (popBackStack() == null) activity?.finish() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.go_back_to_previous_screen),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||
contentDescription = stringResource(R.string.the_app_icon_for_claw),
|
||||
modifier = Modifier.size(48.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
if (isTopLevel) {
|
||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (isTopLevel) {
|
||||
IconButton(onClick = { navigateTo(Search) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Search,
|
||||
contentDescription = stringResource(R.string.search_posts),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { navigateTo(Settings) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Tune,
|
||||
contentDescription = stringResource(R.string.settings),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.decorations
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.blur.HazeBlurDefaults
|
||||
import dev.chrisbanes.haze.blur.blurEffect
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.msfjarvis.claw.android.ui.navigation.AppDestinations
|
||||
import dev.msfjarvis.claw.common.ui.FloatingNavigationBar
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
const val AnimationDuration = 100
|
||||
|
||||
@Composable
|
||||
fun ClawNavigationBar(
|
||||
items: ImmutableList<NavigationItem>,
|
||||
currentNavKey: NavKey?,
|
||||
navigateTo: (NavKey) -> Unit,
|
||||
isVisible: Boolean,
|
||||
hazeState: HazeState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
enter =
|
||||
slideInVertically(
|
||||
// Enters by sliding up from offset 0 to fullHeight.
|
||||
initialOffsetY = { fullHeight -> fullHeight },
|
||||
animationSpec = tween(durationMillis = AnimationDuration, easing = LinearOutSlowInEasing),
|
||||
),
|
||||
exit =
|
||||
slideOutVertically(
|
||||
// Exits by sliding up from offset 0 to -fullHeight.
|
||||
targetOffsetY = { fullHeight -> fullHeight },
|
||||
animationSpec = tween(durationMillis = AnimationDuration, easing = FastOutLinearInEasing),
|
||||
),
|
||||
label = "",
|
||||
content = {
|
||||
val surfaceColor = MaterialTheme.colorScheme.surface
|
||||
FloatingNavigationBar(
|
||||
tonalElevation = 16.dp,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
modifier =
|
||||
modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.navigationBarsPadding()
|
||||
.clip(MaterialTheme.shapes.extraLarge)
|
||||
.hazeEffect(hazeState) {
|
||||
blurEffect {
|
||||
backgroundColor = surfaceColor
|
||||
colorEffects = emptyList()
|
||||
blurRadius = 24.dp
|
||||
noiseFactor = 0f
|
||||
}
|
||||
},
|
||||
containerColor = if (HazeBlurDefaults.blurEnabled()) Color.Transparent else surfaceColor,
|
||||
) {
|
||||
items.forEach { navItem ->
|
||||
val isSelected = currentNavKey == navItem.navKey
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Crossfade(isSelected, label = "nav-label") {
|
||||
Icon(
|
||||
imageVector = if (it) navItem.selectedIcon else navItem.icon,
|
||||
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { Text(text = navItem.label) },
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (isSelected) {
|
||||
navItem.listStateResetCallback()
|
||||
} else {
|
||||
navigateTo(navItem.navKey)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.testTag(navItem.label.uppercase()),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
class NavigationItem
|
||||
private constructor(
|
||||
val icon: ImageVector,
|
||||
val label: String,
|
||||
val navKey: NavKey,
|
||||
val selectedIcon: ImageVector,
|
||||
val listStateResetCallback: () -> Unit,
|
||||
) {
|
||||
constructor(
|
||||
destination: AppDestinations,
|
||||
listStateResetCallback: () -> Unit,
|
||||
) : this(
|
||||
destination.icon,
|
||||
destination.label,
|
||||
destination.navKey,
|
||||
destination.selectedIcon,
|
||||
listStateResetCallback,
|
||||
) {}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.decorations
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationRail
|
||||
import androidx.compose.material3.NavigationRailItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun ClawNavigationRail(
|
||||
items: ImmutableList<NavigationItem>,
|
||||
currentNavKey: NavKey?,
|
||||
navigateTo: (NavKey) -> Unit,
|
||||
isVisible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
enter =
|
||||
slideInHorizontally(
|
||||
// Enters by sliding in from offset -fullWidth to 0.
|
||||
initialOffsetX = { fullWidth -> -fullWidth },
|
||||
animationSpec = tween(durationMillis = AnimationDuration, easing = LinearOutSlowInEasing),
|
||||
),
|
||||
exit =
|
||||
slideOutHorizontally(
|
||||
// Exits by sliding out from offset 0 to -fullWidth.
|
||||
targetOffsetX = { fullWidth -> -fullWidth },
|
||||
animationSpec = tween(durationMillis = AnimationDuration, easing = FastOutLinearInEasing),
|
||||
),
|
||||
modifier = Modifier,
|
||||
) {
|
||||
NavigationRail(modifier = modifier) {
|
||||
Spacer(Modifier.weight(1f))
|
||||
items.forEach { navItem ->
|
||||
val isSelected = currentNavKey == navItem.navKey
|
||||
NavigationRailItem(
|
||||
icon = {
|
||||
Crossfade(isSelected, label = "nav-label") {
|
||||
Icon(
|
||||
imageVector = if (it) navItem.selectedIcon else navItem.icon,
|
||||
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { Text(text = navItem.label) },
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (isSelected) {
|
||||
navItem.listStateResetCallback()
|
||||
} else {
|
||||
navigateTo(navItem.navKey)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.testTag(navItem.label.uppercase()),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
fun PostActions(
|
||||
context: Context,
|
||||
uriHandler: UriHandler,
|
||||
viewModel: ClawViewModel,
|
||||
navigateToComments: (String) -> Unit,
|
||||
): PostActions {
|
||||
return object : PostActions {
|
||||
override fun viewPost(postId: String, postUrl: String, commentsUrl: String) {
|
||||
viewModel.markPostAsRead(postId)
|
||||
uriHandler.openUri(postUrl.ifEmpty { commentsUrl })
|
||||
}
|
||||
|
||||
override fun viewComments(postId: String) {
|
||||
viewModel.markPostAsRead(postId)
|
||||
navigateToComments(postId)
|
||||
}
|
||||
|
||||
override fun viewCommentsPage(post: UIPost) {
|
||||
uriHandler.openUri(post.commentsUrl)
|
||||
}
|
||||
|
||||
override fun toggleSave(post: UIPost) {
|
||||
viewModel.toggleSave(post)
|
||||
}
|
||||
|
||||
override fun share(post: UIPost) {
|
||||
val sendIntent: Intent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, post.url.ifEmpty { post.commentsUrl })
|
||||
putExtra(Intent.EXTRA_TITLE, post.title)
|
||||
type = "text/plain"
|
||||
}
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
context.startActivity(shareIntent)
|
||||
}
|
||||
|
||||
override fun isPostRead(post: UIPost): Boolean = viewModel.isPostRead(post)
|
||||
|
||||
override fun isPostSaved(post: UIPost): Boolean = viewModel.isPostSaved(post)
|
||||
|
||||
override suspend fun getLinkMetadata(url: String): LinkMetadata {
|
||||
return viewModel.getLinkMetadata(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.lists
|
||||
|
||||
import androidx.activity.compose.ReportDrawn
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Inbox
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.ui.decorations.MonthHeader
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun DatabasePosts(
|
||||
items: ImmutableMap<String, List<UIPost>>,
|
||||
listState: LazyListState,
|
||||
postActions: PostActions,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ReportDrawn()
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
if (items.isEmpty()) {
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Inbox,
|
||||
contentDescription = stringResource(R.string.empty_inbox_icon),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally).size(36.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.no_saved_posts),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(state = listState, contentPadding = contentPadding) {
|
||||
items.forEach { (month, posts) ->
|
||||
stickyHeader(contentType = "month-header") { MonthHeader(label = month) }
|
||||
items(items = posts, key = { it.shortId }, contentType = { "LobstersItem" }) { item ->
|
||||
val isSaved = postActions.isPostSaved(item)
|
||||
val isRead = postActions.isPostRead(item)
|
||||
LobstersListItem(
|
||||
item = item,
|
||||
isSaved = isSaved,
|
||||
isRead = isRead,
|
||||
postActions = postActions,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.lists
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Reply
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.android.glance.samplePosts
|
||||
import dev.msfjarvis.claw.common.posts.LobstersCard
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.posts.TEST_POST_ACTIONS
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import me.saket.swipe.SwipeAction
|
||||
import me.saket.swipe.SwipeableActionsBox
|
||||
|
||||
@Composable
|
||||
fun LobstersListItem(
|
||||
item: UIPost,
|
||||
isSaved: Boolean,
|
||||
isRead: Boolean,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val commentsAction =
|
||||
SwipeAction(
|
||||
icon = rememberVectorPainter(Icons.AutoMirrored.Filled.Reply),
|
||||
background = MaterialTheme.colorScheme.tertiary,
|
||||
onSwipe = { postActions.viewCommentsPage(item) },
|
||||
)
|
||||
val shareAction =
|
||||
SwipeAction(
|
||||
icon = rememberVectorPainter(Icons.Filled.Share),
|
||||
background = MaterialTheme.colorScheme.tertiary,
|
||||
onSwipe = { postActions.share(item) },
|
||||
)
|
||||
SwipeableActionsBox(
|
||||
startActions = listOf(shareAction),
|
||||
endActions = listOf(commentsAction),
|
||||
swipeThreshold = 80.dp,
|
||||
backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceVariant,
|
||||
) {
|
||||
LobstersCard(
|
||||
post = item,
|
||||
isSaved = isSaved,
|
||||
isRead = isRead,
|
||||
postActions = postActions,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun ItemPreview() {
|
||||
LobstersTheme {
|
||||
LobstersListItem(
|
||||
item = samplePosts(1).first(),
|
||||
isSaved = true,
|
||||
isRead = true,
|
||||
postActions = TEST_POST_ACTIONS,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.lists
|
||||
|
||||
import androidx.activity.compose.ReportDrawnWhen
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemContentType
|
||||
import dev.msfjarvis.claw.android.glance.samplePosts
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.posts.TEST_POST_ACTIONS
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.NetworkError
|
||||
import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||
import dev.msfjarvis.claw.common.ui.preview.DevicePreviews
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NetworkPosts(
|
||||
lazyPagingItems: LazyPagingItems<UIPost>,
|
||||
listState: LazyListState,
|
||||
postActions: PostActions,
|
||||
contentPadding: PaddingValues,
|
||||
filteredTags: ImmutableSet<String>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ReportDrawnWhen { lazyPagingItems.itemCount > 0 }
|
||||
val refreshLoadState by rememberUpdatedState(lazyPagingItems.loadState.refresh)
|
||||
val state = rememberPullToRefreshState()
|
||||
PullToRefreshBox(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
isRefreshing = refreshLoadState == LoadState.Loading,
|
||||
state = state,
|
||||
onRefresh = { lazyPagingItems.refresh() },
|
||||
indicator = {
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = state,
|
||||
isRefreshing = refreshLoadState == LoadState.Loading,
|
||||
modifier = Modifier.align(Alignment.TopCenter).padding(contentPadding),
|
||||
)
|
||||
},
|
||||
) {
|
||||
if (lazyPagingItems.itemCount == 0 && refreshLoadState is LoadState.Error) {
|
||||
NetworkError(
|
||||
label = "Failed to load posts",
|
||||
error = (refreshLoadState as LoadState.Error).error,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
} else {
|
||||
LazyColumn(contentPadding = contentPadding, state = listState) {
|
||||
items(
|
||||
count = lazyPagingItems.itemCount,
|
||||
// Intentionally keyed on just the index to avoid crashes
|
||||
key = { index -> index },
|
||||
contentType = lazyPagingItems.itemContentType { "LobstersItem" },
|
||||
) { index ->
|
||||
val item = lazyPagingItems[index]
|
||||
if (item != null) {
|
||||
val shouldShowPost = item.tags.none { tag -> filteredTags.contains(tag) }
|
||||
if (shouldShowPost) {
|
||||
val isSaved = postActions.isPostSaved(item)
|
||||
val isRead = postActions.isPostRead(item)
|
||||
LobstersListItem(
|
||||
item = item,
|
||||
isSaved = isSaved,
|
||||
isRead = isRead,
|
||||
postActions = postActions,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lazyPagingItems.loadState.append == LoadState.Loading) {
|
||||
item(key = "progressbar") {
|
||||
ProgressBar(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
.padding(vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DevicePreviews
|
||||
@Composable
|
||||
private fun ListPreview() {
|
||||
val items = samplePosts(20) { this.copy(shortId = "${this.shortId}${it}") }
|
||||
val flow = MutableStateFlow(PagingData.from(items))
|
||||
LobstersTheme {
|
||||
NetworkPosts(
|
||||
lazyPagingItems = flow.collectAsLazyPagingItems(),
|
||||
listState = rememberLazyListState(),
|
||||
postActions = TEST_POST_ACTIONS,
|
||||
contentPadding = PaddingValues(),
|
||||
filteredTags = persistentSetOf(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.lists
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.ui.SearchBar
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
@Composable
|
||||
fun SearchList(
|
||||
lazyPagingItems: LazyPagingItems<UIPost>,
|
||||
listState: LazyListState,
|
||||
postActions: PostActions,
|
||||
searchQuery: String,
|
||||
setSearchQuery: (String) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
filteredTags: ImmutableSet<String>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val triggerSearch = { query: String ->
|
||||
setSearchQuery(query)
|
||||
lazyPagingItems.refresh()
|
||||
}
|
||||
Column(modifier.padding(contentPadding).fillMaxWidth()) {
|
||||
SearchBar(
|
||||
value = searchQuery,
|
||||
onValueChange = setSearchQuery,
|
||||
onSearch = { triggerSearch(it) },
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp).testTag("search_bar"),
|
||||
)
|
||||
NetworkPosts(
|
||||
lazyPagingItems = lazyPagingItems,
|
||||
listState = listState,
|
||||
postActions = postActions,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
filteredTags = filteredTags,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.navigation
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.NewReleases
|
||||
import androidx.compose.material.icons.filled.Whatshot
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.Whatshot
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
interface TopLevelDestination
|
||||
|
||||
interface NonStackable
|
||||
|
||||
@Parcelize @Serializable data object Hottest : NavKey, TopLevelDestination, Parcelable
|
||||
|
||||
@Parcelize @Serializable data object Newest : NavKey, TopLevelDestination, Parcelable
|
||||
|
||||
@Parcelize @Serializable data object Saved : NavKey, TopLevelDestination, Parcelable
|
||||
|
||||
@Parcelize @Serializable data class Comments(val postId: String) : NavKey, Parcelable, NonStackable
|
||||
|
||||
@Parcelize @Serializable data class User(val username: String) : NavKey, Parcelable, NonStackable
|
||||
|
||||
@Parcelize @Serializable data object Search : NavKey, Parcelable
|
||||
|
||||
@Parcelize @Serializable data object Settings : NavKey, Parcelable
|
||||
|
||||
@Parcelize @Serializable data object AboutLibraries : NavKey, Parcelable
|
||||
|
||||
@Parcelize @Serializable data object TagFiltering : NavKey, Parcelable
|
||||
|
||||
@Parcelize @Serializable data object Login : NavKey, Parcelable
|
||||
|
||||
enum class AppDestinations(
|
||||
val icon: ImageVector,
|
||||
val label: String,
|
||||
val navKey: NavKey,
|
||||
val selectedIcon: ImageVector,
|
||||
) {
|
||||
HOTTEST(
|
||||
icon = Icons.Outlined.Whatshot,
|
||||
label = "Hottest",
|
||||
navKey = Hottest,
|
||||
selectedIcon = Icons.Filled.Whatshot,
|
||||
),
|
||||
NEWEST(
|
||||
icon = Icons.Outlined.NewReleases,
|
||||
label = "Newest",
|
||||
navKey = Newest,
|
||||
selectedIcon = Icons.Filled.NewReleases,
|
||||
),
|
||||
SAVED(
|
||||
icon = Icons.Outlined.FavoriteBorder,
|
||||
label = "Saved",
|
||||
navKey = Saved,
|
||||
selectedIcon = Icons.Filled.Favorite,
|
||||
),
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.navigation
|
||||
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
|
||||
enum class ClawNavigationType {
|
||||
BOTTOM_NAVIGATION,
|
||||
NAVIGATION_RAIL;
|
||||
|
||||
companion object {
|
||||
fun fromSize(windowSize: WindowWidthSizeClass): ClawNavigationType {
|
||||
return when (windowSize) {
|
||||
WindowWidthSizeClass.Medium,
|
||||
WindowWidthSizeClass.Expanded -> NAVIGATION_RAIL
|
||||
else -> BOTTOM_NAVIGATION
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
/** Taken from https://github.com/getsentry/sentry-java/pull/5023 */
|
||||
@file:Suppress("UnstableApiUsage") // Sentry internal APIs are used by this module
|
||||
|
||||
package dev.msfjarvis.claw.android.ui.navigation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import io.sentry.Breadcrumb
|
||||
import io.sentry.BuildConfig
|
||||
import io.sentry.Hint
|
||||
import io.sentry.IScopes
|
||||
import io.sentry.ITransaction
|
||||
import io.sentry.ScopesAdapter
|
||||
import io.sentry.SentryIntegrationPackageStorage
|
||||
import io.sentry.SentryLevel
|
||||
import io.sentry.SentryOptions
|
||||
import io.sentry.SpanStatus
|
||||
import io.sentry.TransactionContext
|
||||
import io.sentry.TransactionOptions
|
||||
import io.sentry.TypeCheckHint
|
||||
import io.sentry.protocol.TransactionNameSource
|
||||
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
private const val TRACE_ORIGIN = "auto.navigation.navigation3"
|
||||
private const val NAVIGATION_OP = "navigation"
|
||||
|
||||
/** Holder for the active navigation transaction in a composition scope. */
|
||||
internal class NavigationTransactionHolder {
|
||||
internal var activeTransaction: ITransaction? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* A Composable that observes a back stack [List] and captures a [Breadcrumb] and starts an
|
||||
* [ITransaction] for each navigation event, sending them to Sentry.
|
||||
*
|
||||
* This integration is designed for Android Navigation 3 which uses a back stack-based approach with
|
||||
* Compose state observation instead of traditional listeners.
|
||||
*
|
||||
* Each invocation of this composable maintains independent transaction state, allowing multiple
|
||||
* navigation instances (e.g., in split-screen or multi-pane layouts) to coexist without
|
||||
* interference.
|
||||
*
|
||||
* @param T The type of keys in the back stack
|
||||
* @param backStack The current back stack to observe for navigation changes
|
||||
* @param enableNavigationBreadcrumbs Whether the integration should capture breadcrumbs for
|
||||
* navigation events.
|
||||
* @param enableNavigationTracing Whether the integration should start a new idle [ITransaction]
|
||||
* with [SentryOptions.idleTimeout] for navigation events.
|
||||
* @param keyToRoute A function to extract a route name from a back stack key. Defaults to
|
||||
* [Any.toString].
|
||||
* @param scopes The [IScopes] instance to use for capturing events. Defaults to the singleton
|
||||
* instance.
|
||||
*/
|
||||
@Composable
|
||||
fun <T> SentryNavigation3Traced(
|
||||
@SuppressLint("ComposeUnstableCollections") backStack: List<T>,
|
||||
enableNavigationBreadcrumbs: Boolean = true,
|
||||
enableNavigationTracing: Boolean = true,
|
||||
keyToRoute: (T) -> String? = { it.toString() },
|
||||
scopes: IScopes = ScopesAdapter.getInstance(),
|
||||
) {
|
||||
val backStackSnapshot by rememberUpdatedState(backStack)
|
||||
val enableBreadcrumbsSnapshot by rememberUpdatedState(enableNavigationBreadcrumbs)
|
||||
val enableTracingSnapshot by rememberUpdatedState(enableNavigationTracing)
|
||||
val keyToRouteSnapshot by rememberUpdatedState(keyToRoute)
|
||||
val scopesSnapshot by rememberUpdatedState(scopes)
|
||||
|
||||
val transactionHolder = remember { NavigationTransactionHolder() }
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
addIntegrationToSdkVersion("Navigation3")
|
||||
|
||||
val observer =
|
||||
SentryBackStackObserver(
|
||||
enableNavigationBreadcrumbs = enableBreadcrumbsSnapshot,
|
||||
enableNavigationTracing = enableTracingSnapshot,
|
||||
keyToRoute = keyToRouteSnapshot,
|
||||
scopes = scopesSnapshot,
|
||||
transactionHolder = transactionHolder,
|
||||
)
|
||||
|
||||
@SuppressLint("RawDispatchersUse")
|
||||
val scope =
|
||||
kotlinx.coroutines.CoroutineScope(
|
||||
kotlinx.coroutines.Dispatchers.Main + kotlinx.coroutines.SupervisorJob()
|
||||
)
|
||||
@SuppressLint("DenyListedApi")
|
||||
val job =
|
||||
snapshotFlow { backStackSnapshot }
|
||||
.drop(1) // Skip initial state
|
||||
.onEach { currentStack ->
|
||||
val currentKey = currentStack.lastOrNull()
|
||||
val previousKey = observer.previousKey
|
||||
if (currentKey != null && currentKey != previousKey) {
|
||||
observer.handleNavigation(currentKey, currentStack)
|
||||
observer.previousKey = currentKey
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
onDispose { job.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Internal observer that monitors back stack changes and creates Sentry events. */
|
||||
internal class SentryBackStackObserver<T>(
|
||||
private val enableNavigationBreadcrumbs: Boolean,
|
||||
private val enableNavigationTracing: Boolean,
|
||||
private val keyToRoute: (T) -> String?,
|
||||
private val scopes: IScopes,
|
||||
private val transactionHolder: NavigationTransactionHolder,
|
||||
) {
|
||||
internal var previousKey: T? = null
|
||||
|
||||
private val isPerformanceEnabled: Boolean
|
||||
get() = scopes.options.isTracingEnabled && enableNavigationTracing
|
||||
|
||||
init {
|
||||
SentryIntegrationPackageStorage.getInstance()
|
||||
.addPackage("maven:io.sentry:sentry-android-navigation3", BuildConfig.VERSION_NAME)
|
||||
}
|
||||
|
||||
internal fun handleNavigation(currentKey: T, currentStack: List<T>) {
|
||||
val currentRoute = keyToRoute(currentKey)
|
||||
if (currentRoute != null) {
|
||||
addBreadcrumb(currentRoute, currentStack)
|
||||
|
||||
if (scopes.options.isEnableScreenTracking) {
|
||||
scopes.configureScope { it.screen = currentRoute }
|
||||
}
|
||||
|
||||
startTracing(currentRoute, currentStack)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addBreadcrumb(toRoute: String, currentStack: List<T>) {
|
||||
if (!enableNavigationBreadcrumbs) {
|
||||
return
|
||||
}
|
||||
|
||||
val breadcrumb =
|
||||
Breadcrumb().apply {
|
||||
type = NAVIGATION_OP
|
||||
category = NAVIGATION_OP
|
||||
|
||||
val fromKey = previousKey
|
||||
if (fromKey != null) {
|
||||
val fromRoute = keyToRoute(fromKey)
|
||||
if (fromRoute != null) {
|
||||
data["from"] = fromRoute
|
||||
}
|
||||
}
|
||||
|
||||
data["to"] = toRoute
|
||||
|
||||
// Capture back stack keys as a list
|
||||
val backStackKeys = currentStack.mapNotNull { keyToRoute(it) }
|
||||
if (backStackKeys.isNotEmpty()) {
|
||||
data["back_stack"] = backStackKeys
|
||||
}
|
||||
|
||||
level = SentryLevel.INFO
|
||||
}
|
||||
|
||||
val hint = Hint()
|
||||
hint.set(TypeCheckHint.ANDROID_NAV_DESTINATION, toRoute)
|
||||
scopes.addBreadcrumb(breadcrumb, hint)
|
||||
}
|
||||
|
||||
private fun startTracing(routeName: String, currentStack: List<T>) {
|
||||
if (!isPerformanceEnabled) {
|
||||
io.sentry.util.TracingUtils.startNewTrace(scopes)
|
||||
return
|
||||
}
|
||||
|
||||
// Finish previous transaction in this navigation scope
|
||||
if (transactionHolder.activeTransaction != null) {
|
||||
stopTracing()
|
||||
}
|
||||
|
||||
val transactionOptions =
|
||||
TransactionOptions().also {
|
||||
it.isWaitForChildren = true
|
||||
it.idleTimeout = scopes.options.idleTimeout
|
||||
|
||||
// Set deadline timeout based on configured option
|
||||
val deadlineTimeoutMillis = scopes.options.deadlineTimeout
|
||||
// No deadline when zero or negative value is set
|
||||
it.deadlineTimeout = if (deadlineTimeoutMillis <= 0) null else deadlineTimeoutMillis
|
||||
|
||||
it.isTrimEnd = true
|
||||
}
|
||||
|
||||
val transaction =
|
||||
scopes.startTransaction(
|
||||
TransactionContext(routeName, TransactionNameSource.ROUTE, NAVIGATION_OP),
|
||||
transactionOptions,
|
||||
)
|
||||
|
||||
transaction.spanContext.origin = TRACE_ORIGIN
|
||||
|
||||
// Capture back stack keys as data
|
||||
val backStackKeys = currentStack.mapNotNull { keyToRoute(it) }
|
||||
if (backStackKeys.isNotEmpty()) {
|
||||
transaction.setData("back_stack", backStackKeys)
|
||||
}
|
||||
|
||||
scopes.configureScope { scope ->
|
||||
scope.withTransaction { tx ->
|
||||
if (tx == null) {
|
||||
scope.transaction = transaction
|
||||
}
|
||||
}
|
||||
}
|
||||
transactionHolder.activeTransaction = transaction
|
||||
}
|
||||
|
||||
private fun stopTracing() {
|
||||
val status = transactionHolder.activeTransaction?.status ?: SpanStatus.OK
|
||||
transactionHolder.activeTransaction?.finish(status)
|
||||
|
||||
// clear transaction from scope so others can bind to it
|
||||
scopes.configureScope { scope ->
|
||||
scope.withTransaction { tx ->
|
||||
if (tx == transactionHolder.activeTransaction) {
|
||||
scope.clearTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transactionHolder.activeTransaction = null
|
||||
}
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
|
||||
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.android.ui.PostActions
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawAppBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
|
||||
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
|
||||
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
|
||||
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
|
||||
import dev.msfjarvis.claw.android.ui.navigation.AboutLibraries
|
||||
import dev.msfjarvis.claw.android.ui.navigation.AppDestinations
|
||||
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Comments
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Hottest
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Login
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Newest
|
||||
import dev.msfjarvis.claw.android.ui.navigation.NonStackable
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Saved
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Search
|
||||
import dev.msfjarvis.claw.android.ui.navigation.SentryNavigation3Traced
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Settings
|
||||
import dev.msfjarvis.claw.android.ui.navigation.TagFiltering
|
||||
import dev.msfjarvis.claw.android.ui.navigation.TopLevelDestination
|
||||
import dev.msfjarvis.claw.android.ui.navigation.User
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.android.viewmodel.SettingsViewModel
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.tags.TagFilterViewModel
|
||||
import dev.msfjarvis.claw.common.tags.TagList
|
||||
import dev.msfjarvis.claw.common.user.UserProfile
|
||||
import dev.zacsweers.metrox.viewmodel.metroViewModel
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun LobstersPostsScreen(
|
||||
uriHandler: UriHandler,
|
||||
windowSizeClass: WindowSizeClass,
|
||||
setWebUri: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
deepLinkDestination: NavKey? = null,
|
||||
clearDeepLink: () -> Unit = {},
|
||||
viewModel: ClawViewModel = metroViewModel(),
|
||||
settingsViewModel: SettingsViewModel = metroViewModel(),
|
||||
tagFilterViewModel: TagFilterViewModel = metroViewModel(key = "tag_filter"),
|
||||
) {
|
||||
val navigationType = ClawNavigationType.fromSize(windowSizeClass.widthSizeClass)
|
||||
|
||||
val libraries by produceLibraries()
|
||||
val backStack = rememberNavBackStack(Hottest)
|
||||
val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()
|
||||
|
||||
// region Pain
|
||||
val context = LocalContext.current
|
||||
val activity = LocalActivity.current
|
||||
val hottestListState = rememberLazyListState()
|
||||
val newestListState = rememberLazyListState()
|
||||
val savedListState = rememberLazyListState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val hazeState = remember { HazeState() }
|
||||
|
||||
val savedPosts by viewModel.savedPostsByMonth.collectAsStateWithLifecycle(persistentMapOf())
|
||||
val savedPostsCount by viewModel.savedPostsCount.collectAsStateWithLifecycle(0L)
|
||||
|
||||
val filteredTags by tagFilterViewModel.filteredTags.collectAsStateWithLifecycle(persistentSetOf())
|
||||
val isLoggedIn by settingsViewModel.isLoggedIn.collectAsStateWithLifecycle(false)
|
||||
val username by settingsViewModel.username.collectAsStateWithLifecycle(null)
|
||||
|
||||
LaunchedEffect(deepLinkDestination) {
|
||||
if (deepLinkDestination != null) {
|
||||
navigateTo(backStack, deepLinkDestination, allowStacking = true)
|
||||
clearDeepLink()
|
||||
}
|
||||
}
|
||||
|
||||
val navItems =
|
||||
persistentListOf(
|
||||
NavigationItem(AppDestinations.HOTTEST) {
|
||||
coroutineScope.launch { hottestListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
NavigationItem(AppDestinations.NEWEST) {
|
||||
coroutineScope.launch { newestListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
NavigationItem(AppDestinations.SAVED) {
|
||||
coroutineScope.launch { if (savedPosts.isNotEmpty()) savedListState.scrollToItem(0) }
|
||||
},
|
||||
)
|
||||
// endregion
|
||||
|
||||
val postActions = remember {
|
||||
PostActions(context, uriHandler, viewModel) {
|
||||
navigateTo(backStack, Comments(it), allowStacking = false)
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = backStack.size > 1) { popBackStack(backStack) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
ClawAppBar(
|
||||
activity = activity,
|
||||
isTopLevel = backStack.lastOrNull() is TopLevelDestination,
|
||||
navigateTo = { destination -> navigateTo(backStack, destination) },
|
||||
popBackStack = { popBackStack(backStack) },
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
val currentDestination = backStack.lastOrNull()
|
||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
|
||||
ClawNavigationBar(
|
||||
items = navItems,
|
||||
currentNavKey = currentDestination,
|
||||
navigateTo = { destination -> navigateTo(backStack, destination) },
|
||||
isVisible = backStack.lastOrNull() is TopLevelDestination,
|
||||
hazeState = hazeState,
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
modifier = Modifier.semantics { testTagsAsResourceId = true },
|
||||
) { contentPadding ->
|
||||
SentryNavigation3Traced(backStack = backStack)
|
||||
Row {
|
||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.NAVIGATION_RAIL) {
|
||||
val currentDestination = backStack.lastOrNull()
|
||||
ClawNavigationRail(
|
||||
items = navItems,
|
||||
currentNavKey = currentDestination,
|
||||
navigateTo = { destination -> navigateTo(backStack, destination) },
|
||||
isVisible = backStack.lastOrNull() is TopLevelDestination,
|
||||
)
|
||||
}
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
modifier = modifier.hazeSource(hazeState),
|
||||
sceneStrategies = listOf(listDetailStrategy),
|
||||
entryDecorators = listOf(rememberSaveableStateHolderNavEntryDecorator()),
|
||||
onBack = {
|
||||
if (popBackStack(backStack) == null) {
|
||||
activity?.finish()
|
||||
}
|
||||
},
|
||||
predictivePopTransitionSpec = {
|
||||
slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(200)) togetherWith
|
||||
slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(200))
|
||||
},
|
||||
entryProvider =
|
||||
entryProvider {
|
||||
entry<Hottest>(
|
||||
metadata = ListDetailSceneStrategy.listPane(detailPlaceholder = { Placeholder() })
|
||||
) {
|
||||
setWebUri("https://lobste.rs/")
|
||||
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
|
||||
NetworkPosts(
|
||||
lazyPagingItems = hottestPosts,
|
||||
listState = hottestListState,
|
||||
postActions = postActions,
|
||||
contentPadding = contentPadding,
|
||||
filteredTags = filteredTags,
|
||||
)
|
||||
}
|
||||
entry<Newest>(
|
||||
metadata = ListDetailSceneStrategy.listPane(detailPlaceholder = { Placeholder() })
|
||||
) {
|
||||
setWebUri("https://lobste.rs/")
|
||||
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
|
||||
NetworkPosts(
|
||||
lazyPagingItems = newestPosts,
|
||||
listState = newestListState,
|
||||
postActions = postActions,
|
||||
contentPadding = contentPadding,
|
||||
filteredTags = filteredTags,
|
||||
)
|
||||
}
|
||||
entry<Saved>(
|
||||
metadata = ListDetailSceneStrategy.listPane(detailPlaceholder = { Placeholder() })
|
||||
) {
|
||||
setWebUri(null)
|
||||
DatabasePosts(
|
||||
items = savedPosts,
|
||||
listState = savedListState,
|
||||
postActions = postActions,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
entry<Comments>(metadata = ListDetailSceneStrategy.detailPane()) { dest ->
|
||||
val commentsPostActions = remember {
|
||||
PostActions(context, uriHandler, viewModel) {
|
||||
navigateTo(backStack, Comments(it), allowStacking = true)
|
||||
}
|
||||
}
|
||||
CommentsPage(
|
||||
postId = dest.postId,
|
||||
postActions = commentsPostActions,
|
||||
contentPadding = contentPadding,
|
||||
openUserProfile = { navigateTo(backStack, User(it)) },
|
||||
)
|
||||
}
|
||||
entry<User>(
|
||||
metadata =
|
||||
NavDisplay.transitionSpec {
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { it },
|
||||
animationSpec = tween(200),
|
||||
) togetherWith ExitTransition.KeepUntilTransitionsFinished
|
||||
} +
|
||||
NavDisplay.popTransitionSpec {
|
||||
// Slide old content down, revealing the new content in place underneath
|
||||
EnterTransition.None togetherWith
|
||||
slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(200))
|
||||
} +
|
||||
ListDetailSceneStrategy.extraPane()
|
||||
) { dest ->
|
||||
UserProfile(
|
||||
username = dest.username,
|
||||
contentPadding = contentPadding,
|
||||
openUserProfile = { navigateTo(backStack, User(it)) },
|
||||
)
|
||||
}
|
||||
entry<Settings>(metadata = ListDetailSceneStrategy.extraPane()) {
|
||||
SettingsScreen(
|
||||
openInputStream = context.contentResolver::openInputStream,
|
||||
openOutputStream = context.contentResolver::openOutputStream,
|
||||
openLibrariesScreen = { navigateTo(backStack, AboutLibraries) },
|
||||
openRepository = {
|
||||
uriHandler.openUri("https://github.com/msfjarvis/compose-lobsters")
|
||||
},
|
||||
openTagFiltering = { navigateTo(backStack, TagFiltering) },
|
||||
openLoginScreen = { navigateTo(backStack, Login) },
|
||||
onLogout = { settingsViewModel.logout() },
|
||||
isLoggedIn = isLoggedIn,
|
||||
username = username,
|
||||
importPosts = viewModel::importPosts,
|
||||
exportPostsAsJson = viewModel::exportPostsAsJson,
|
||||
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
||||
savedPostsCount = savedPostsCount,
|
||||
snackbarHostState = snackbarHostState,
|
||||
contentPadding = contentPadding,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
entry<Login>(metadata = ListDetailSceneStrategy.extraPane()) {
|
||||
LoginScreen(
|
||||
onLoginSuccess = settingsViewModel::saveCookie,
|
||||
popBackStack = { popBackStack(backStack) },
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
entry<Search>(metadata = ListDetailSceneStrategy.extraPane()) {
|
||||
setWebUri("https://lobste.rs/search")
|
||||
SearchScreen(
|
||||
viewModel = viewModel,
|
||||
postActions = postActions,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
entry<AboutLibraries>(metadata = ListDetailSceneStrategy.extraPane()) {
|
||||
LibrariesContainer(
|
||||
libraries = libraries,
|
||||
contentPadding = contentPadding,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
entry<TagFiltering> { TagList(contentPadding = contentPadding) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
fun navigateTo(
|
||||
backStack: MutableList<NavKey>,
|
||||
destination: NavKey,
|
||||
allowStacking: Boolean = false,
|
||||
) {
|
||||
if (destination is TopLevelDestination) {
|
||||
backStack.clear()
|
||||
if (destination != Hottest) {
|
||||
backStack.add(Hottest)
|
||||
}
|
||||
backStack.add(destination)
|
||||
return
|
||||
}
|
||||
|
||||
if (destination is NonStackable) {
|
||||
if (allowStacking && destination is Comments) {
|
||||
val lastItem = backStack.lastOrNull()
|
||||
if (lastItem is Comments && lastItem.postId == destination.postId) {
|
||||
return
|
||||
}
|
||||
backStack.add(destination)
|
||||
return
|
||||
}
|
||||
|
||||
val existingEntry = backStack.firstOrNull {
|
||||
it is NonStackable && it::class.java.isAssignableFrom(destination::class.java)
|
||||
}
|
||||
|
||||
if (existingEntry != null) {
|
||||
backStack.remove(existingEntry)
|
||||
}
|
||||
}
|
||||
|
||||
backStack.add(destination)
|
||||
}
|
||||
|
||||
private fun popBackStack(backStack: MutableList<NavKey>): NavKey? {
|
||||
if (backStack.size <= 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return backStack.removeAt(backStack.lastIndex)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Placeholder(modifier: Modifier = Modifier) {
|
||||
Text(text = stringResource(R.string.no_post_selected), modifier = modifier)
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
|
||||
private const val LOGIN_URL = "https://lobste.rs/login"
|
||||
private const val LOBSTERS_URL = "https://lobste.rs"
|
||||
|
||||
internal fun parseAuthenticatedUsername(usernameResult: String): String? {
|
||||
return usernameResult.removeSurrounding("\"").takeUnless { it.isBlank() || it == "null" }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onLoginSuccess: (String, String) -> Unit,
|
||||
popBackStack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
var hasError by remember { mutableStateOf(false) }
|
||||
var webViewRef by remember { mutableStateOf<WebView?>(null) }
|
||||
|
||||
Scaffold(modifier = modifier) { contentPadding ->
|
||||
Box(modifier = Modifier.padding(contentPadding).fillMaxSize()) {
|
||||
if (hasError) {
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text("Failed to load login page.")
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
hasError = false
|
||||
isLoading = true
|
||||
webViewRef?.loadUrl(LOGIN_URL)
|
||||
}
|
||||
) {
|
||||
Text("Retry")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
WebView(context)
|
||||
.apply {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
settings.javaScriptEnabled = true
|
||||
webViewClient =
|
||||
object : WebViewClient() {
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||
isLoading = true
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
isLoading = false
|
||||
if (!url.contains("/login")) {
|
||||
@SuppressLint("DeprecatedCall")
|
||||
view.evaluateJavascript(
|
||||
"document.body && document.body.getAttribute('data-username')"
|
||||
) { usernameResult ->
|
||||
val username = parseAuthenticatedUsername(usernameResult)
|
||||
if (username != null) {
|
||||
val cookie = CookieManager.getInstance().getCookie(LOBSTERS_URL)
|
||||
if (cookie != null) {
|
||||
onLoginSuccess(cookie, username)
|
||||
popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceError,
|
||||
) {
|
||||
if (request.isForMainFrame) {
|
||||
isLoading = false
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
loadUrl(LOGIN_URL)
|
||||
}
|
||||
.also { webViewRef = it }
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading && !hasError) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import dev.msfjarvis.claw.android.ui.lists.SearchList
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.tags.TagFilterViewModel
|
||||
import dev.zacsweers.metrox.viewmodel.metroViewModel
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
postActions: PostActions,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = metroViewModel(),
|
||||
tagFilterViewModel: TagFilterViewModel = metroViewModel(key = "tag_filter"),
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val searchResults = viewModel.searchResults.collectAsLazyPagingItems()
|
||||
val filteredTags by tagFilterViewModel.filteredTags.collectAsStateWithLifecycle(emptySet())
|
||||
|
||||
SearchList(
|
||||
lazyPagingItems = searchResults,
|
||||
listState = listState,
|
||||
postActions = postActions,
|
||||
searchQuery = viewModel.searchQuery,
|
||||
contentPadding = contentPadding,
|
||||
setSearchQuery = { query -> viewModel.searchQuery = query },
|
||||
filteredTags = filteredTags.toPersistentSet(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -1,404 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Bookmarks
|
||||
import androidx.compose.material.icons.filled.Code
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.filled.ImportExport
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.android.BuildConfig
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
import io.sentry.Sentry
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val JSON_MIME_TYPE = "application/json"
|
||||
private const val HTML_MIME_TYPE = "application/html"
|
||||
|
||||
internal fun loggedInAccountText(username: String?): String {
|
||||
return if (username.isNullOrBlank()) {
|
||||
"Sign out of your lobste.rs account"
|
||||
} else {
|
||||
"Logged in as $username"
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
openLibrariesScreen: () -> Unit,
|
||||
openRepository: () -> Unit,
|
||||
openTagFiltering: () -> Unit,
|
||||
openLoginScreen: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
isLoggedIn: Boolean,
|
||||
username: String?,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
openInputStream: (Uri) -> InputStream?,
|
||||
openOutputStream: (Uri) -> OutputStream?,
|
||||
importPosts: suspend (InputStream) -> Result<Unit>,
|
||||
exportPostsAsJson: suspend (OutputStream) -> Unit,
|
||||
exportPostsAsHtml: suspend (OutputStream) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
savedPostsCount: Long,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Column(modifier.padding(contentPadding)) {
|
||||
// Account Section
|
||||
SectionHeader(title = "Account")
|
||||
if (isLoggedIn) {
|
||||
ListItem(
|
||||
headlineContent = { Text("Log out") },
|
||||
supportingContent = { Text(loggedInAccountText(username)) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AccountCircle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = onLogout),
|
||||
)
|
||||
} else {
|
||||
ListItem(
|
||||
headlineContent = { Text("Log in with lobste.rs") },
|
||||
supportingContent = { Text("Sign in to your lobste.rs account") },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AccountCircle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = openLoginScreen),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Data Management Section
|
||||
SectionHeader(title = stringResource(R.string.data_management))
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.data_transfer)) },
|
||||
supportingContent = { Text(stringResource(R.string.import_and_export_your_saved_posts)) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ImportExport,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
ImportPosts(openInputStream, coroutineScope, snackbarHostState, importPosts)
|
||||
ExportPosts(
|
||||
coroutineScope,
|
||||
snackbarHostState,
|
||||
openOutputStream,
|
||||
exportPostsAsJson,
|
||||
exportPostsAsHtml,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// App Information Section
|
||||
SectionHeader(title = stringResource(R.string.app_information))
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.version)) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.version_placeholder,
|
||||
BuildConfig.VERSION_NAME,
|
||||
BuildConfig.VERSION_CODE,
|
||||
)
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Code,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.saved_posts)) },
|
||||
supportingContent = { Text(stringResource(R.string.posts_saved_locally, savedPostsCount)) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Bookmarks,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.tag_filtering)) },
|
||||
supportingContent = { Text(stringResource(R.string.filter_posts_by_tags)) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.FilterList,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = openTagFiltering),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// About Section
|
||||
SectionHeader(title = stringResource(R.string.about))
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.libraries)) },
|
||||
supportingContent = {
|
||||
Text(stringResource(R.string.view_open_source_libraries_used_in_this_))
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.LibraryBooks,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = openLibrariesScreen),
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.source_code)) },
|
||||
supportingContent = { Text(stringResource(R.string.view_the_source_code_on_github)) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Code,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = openRepository),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionHeader(title: String, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImportPosts(
|
||||
openInputStream: (Uri) -> InputStream?,
|
||||
coroutineScope: CoroutineScope,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
importPosts: suspend (InputStream) -> Result<Unit>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val importAction =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
openInputStream(uri)?.use { stream ->
|
||||
val result = importPosts(stream)
|
||||
if (result.isSuccess) {
|
||||
snackbarHostState.showSnackbarDismissing("Successfully imported posts")
|
||||
} else {
|
||||
snackbarHostState.showSnackbarDismissing("Failed to import posts")
|
||||
}
|
||||
} ?: snackbarHostState.showSnackbarDismissing("Unable to open selected file")
|
||||
} catch (e: Exception) {
|
||||
snackbarHostState.showSnackbarDismissing("Failed to open file: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
importAction.launch(JSON_MIME_TYPE)
|
||||
Sentry.metrics().count("import", 1.0)
|
||||
},
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(text = stringResource(R.string.import_action))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun ExportPosts(
|
||||
coroutineScope: CoroutineScope,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
crossinline openOutputStream: (Uri) -> OutputStream?,
|
||||
crossinline exportPostsAsJson: suspend (OutputStream) -> Unit,
|
||||
crossinline exportPostsAsHtml: suspend (OutputStream) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val jsonExportAction =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(JSON_MIME_TYPE)) { uri
|
||||
->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
openOutputStream(uri)?.use { stream ->
|
||||
exportPostsAsJson(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
Sentry.metrics().count("json_export_success", 1.0)
|
||||
}
|
||||
?: run {
|
||||
snackbarHostState.showSnackbarDismissing("Unable to open output file")
|
||||
Sentry.metrics().count("json_export_failure", 1.0)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
snackbarHostState.showSnackbarDismissing("Failed to export posts: ${e.message}")
|
||||
Sentry.metrics().count("json_export_failure", 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
val htmlExportAction =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(HTML_MIME_TYPE)) { uri
|
||||
->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
openOutputStream(uri)?.use { stream ->
|
||||
exportPostsAsHtml(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
Sentry.metrics().count("html_export_success", 1.0)
|
||||
}
|
||||
?: run {
|
||||
snackbarHostState.showSnackbarDismissing("Unable to open output file")
|
||||
Sentry.metrics().count("html_export_failure", 1.0)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
snackbarHostState.showSnackbarDismissing("Failed to export posts: ${e.message}")
|
||||
Sentry.metrics().count("html_export_failure", 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
OutlinedButton(
|
||||
onClick = { expanded = true },
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(text = stringResource(R.string.export))
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.json)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
jsonExportAction.launch("claw-export.json")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Code,
|
||||
contentDescription = stringResource(R.string.export_as_json),
|
||||
)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.bookmarks)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
htmlExportAction.launch("claw-export.html")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Bookmarks,
|
||||
contentDescription = stringResource(R.string.export_as_browser_bookmarks),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows a Snackbar but dismisses any existing ones first. */
|
||||
private suspend inline fun SnackbarHostState.showSnackbarDismissing(text: String) {
|
||||
currentSnackbarData?.dismiss()
|
||||
showSnackbar(text)
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun SettingsScreenPreview() {
|
||||
LobstersTheme {
|
||||
SettingsScreen(
|
||||
openLibrariesScreen = {},
|
||||
openRepository = {},
|
||||
openTagFiltering = {},
|
||||
openLoginScreen = {},
|
||||
onLogout = {},
|
||||
isLoggedIn = false,
|
||||
username = null,
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
openInputStream = { null },
|
||||
openOutputStream = { null },
|
||||
importPosts = { Result.success(Unit) },
|
||||
exportPostsAsJson = {},
|
||||
exportPostsAsHtml = {},
|
||||
contentPadding = PaddingValues(),
|
||||
savedPostsCount = 42,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import dev.msfjarvis.claw.android.BuildConfig
|
||||
import dev.msfjarvis.claw.core.coroutines.DatabaseReadDispatcher
|
||||
import dev.msfjarvis.claw.core.coroutines.DatabaseWriteDispatcher
|
||||
import dev.msfjarvis.claw.database.local.CachedRemotePost
|
||||
import dev.msfjarvis.claw.database.local.CachedRemotePostQueries
|
||||
import dev.zacsweers.metro.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Inject
|
||||
class CachedRemotePostsRepository(
|
||||
private val cachedRemotePostQueries: CachedRemotePostQueries,
|
||||
@param:DatabaseReadDispatcher private val readDispatcher: CoroutineDispatcher,
|
||||
@param:DatabaseWriteDispatcher private val writeDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
val cachedPosts = cachedRemotePostQueries.selectAllPosts().asFlow().mapToList(readDispatcher)
|
||||
|
||||
fun getRecentPosts(limit: Long) =
|
||||
cachedRemotePostQueries.selectRecentPosts(limit).asFlow().mapToList(readDispatcher)
|
||||
|
||||
suspend fun getCachedPosts(): List<CachedRemotePost> {
|
||||
return withContext(readDispatcher) { cachedRemotePostQueries.selectAllPosts().executeAsList() }
|
||||
}
|
||||
|
||||
suspend fun savePosts(posts: List<CachedRemotePost>) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Caching remote posts: ${posts.joinToString(",") { it.shortId }}")
|
||||
}
|
||||
withContext(writeDispatcher) {
|
||||
cachedRemotePostQueries.transaction {
|
||||
cachedRemotePostQueries.deleteAllPosts()
|
||||
posts.forEach { post -> cachedRemotePostQueries.insertOrReplacePost(post) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "CachedRemotePostsRepository"
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import dev.msfjarvis.claw.android.glance.SavedPostsWidget
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
|
||||
import dev.msfjarvis.claw.android.paging.SearchPagingSource
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.api.toPostsResult
|
||||
import dev.msfjarvis.claw.core.coroutines.IODispatcher
|
||||
import dev.msfjarvis.claw.core.coroutines.MainDispatcher
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.fromSavedPost
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoMap
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.binding
|
||||
import dev.zacsweers.metrox.viewmodel.ViewModelKey
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Inject
|
||||
@ViewModelKey
|
||||
@ContributesIntoMap(scope = AppScope::class, binding = binding<ViewModel>())
|
||||
class ClawViewModel(
|
||||
context: Context,
|
||||
private val api: LobstersApi,
|
||||
private val readPostsRepository: ReadPostsRepository,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val linkMetadataRepository: LinkMetadataRepository,
|
||||
private val dataTransferRepository: DataTransferRepository,
|
||||
private val pagingSourceFactory: LobstersPagingSource.Factory,
|
||||
private val searchPagingSourceFactory: SearchPagingSource.Factory,
|
||||
@param:IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
@param:MainDispatcher private val mainDispatcher: CoroutineDispatcher,
|
||||
) : AndroidViewModel(context as Application) {
|
||||
private var _hottestPosts: Flow<PagingData<UIPost>>? = null
|
||||
val hottestPosts: Flow<PagingData<UIPost>>
|
||||
get() {
|
||||
if (_hottestPosts == null) {
|
||||
_hottestPosts =
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = PAGE_SIZE),
|
||||
initialKey = STARTING_PAGE_INDEX,
|
||||
pagingSourceFactory = {
|
||||
pagingSourceFactory.create { page -> api.getHottestPosts(page).toPostsResult() }
|
||||
},
|
||||
)
|
||||
.flow
|
||||
.cachedIn(viewModelScope)
|
||||
}
|
||||
return requireNotNull(_hottestPosts) { "_hottestPosts should be initialized here" }
|
||||
}
|
||||
|
||||
private var _newestPosts: Flow<PagingData<UIPost>>? = null
|
||||
val newestPosts: Flow<PagingData<UIPost>>
|
||||
get() {
|
||||
if (_newestPosts == null) {
|
||||
_newestPosts =
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = PAGE_SIZE),
|
||||
initialKey = STARTING_PAGE_INDEX,
|
||||
pagingSourceFactory = {
|
||||
pagingSourceFactory.create { page -> api.getNewestPosts(page).toPostsResult() }
|
||||
},
|
||||
)
|
||||
.flow
|
||||
.cachedIn(viewModelScope)
|
||||
}
|
||||
return requireNotNull(_newestPosts) { "_newestPosts should be initialized here" }
|
||||
}
|
||||
|
||||
val searchResults =
|
||||
Pager(
|
||||
PagingConfig(pageSize = PAGE_SIZE),
|
||||
initialKey = STARTING_PAGE_INDEX,
|
||||
pagingSourceFactory = { searchPagingSourceFactory.create { searchQuery } },
|
||||
)
|
||||
.flow
|
||||
val savedPosts = savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) }
|
||||
val savedPostsCount = savedPostsRepository.savedPosts.map { it.size.toLong() }
|
||||
val savedPostsByMonth
|
||||
get() =
|
||||
savedPostsRepository.savedPostsSortedByDate.map { posts ->
|
||||
posts
|
||||
.map(UIPost.Companion::fromSavedPost)
|
||||
.groupBy { post ->
|
||||
val time = post.createdAt.toLocalDateTime()
|
||||
"${time.month.name.lowercase().capitalize(Locale.current)} ${time.year}"
|
||||
}
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
var searchQuery by mutableStateOf("")
|
||||
|
||||
private var _readPosts by mutableStateOf(emptySet<String>())
|
||||
private var _savedPosts by mutableStateOf(emptySet<String>())
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
savedPosts.collectLatest { _savedPosts = it.map(UIPost::shortId).toSet() }
|
||||
}
|
||||
viewModelScope.launch {
|
||||
readPostsRepository.readPosts.collectLatest { _readPosts = it.toSet() }
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSave(post: UIPost) {
|
||||
viewModelScope.launch {
|
||||
savedPostsRepository.toggleSave(post)
|
||||
withContext(mainDispatcher) { SavedPostsWidget().updateAll(getApplication()) }
|
||||
}
|
||||
}
|
||||
|
||||
fun isPostRead(post: UIPost): Boolean {
|
||||
return _readPosts.contains(post.shortId)
|
||||
}
|
||||
|
||||
fun isPostSaved(post: UIPost): Boolean {
|
||||
return _savedPosts.contains(post.shortId)
|
||||
}
|
||||
|
||||
suspend fun getLinkMetadata(url: String) =
|
||||
withContext(ioDispatcher) { linkMetadataRepository.getLinkMetadata(url) }
|
||||
|
||||
suspend fun importPosts(input: InputStream): Result<Unit> =
|
||||
dataTransferRepository.importPosts(input)
|
||||
|
||||
suspend fun exportPostsAsJson(output: OutputStream) =
|
||||
dataTransferRepository.exportPostsAsJson(output)
|
||||
|
||||
suspend fun exportPostsAsHtml(output: OutputStream) =
|
||||
dataTransferRepository.exportPostsAsHTML(output)
|
||||
|
||||
fun markPostAsRead(postId: String) {
|
||||
viewModelScope.launch { readPostsRepository.markRead(postId) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a given [String] into a [LocalDateTime]. This method is only intended to be used for
|
||||
* dates in the format returned by the Lobsters API, and is not a general purpose parsing
|
||||
* solution.
|
||||
*/
|
||||
private fun String.toLocalDateTime(): LocalDateTime {
|
||||
if (isEmpty()) return LocalDateTime.now(ZoneId.systemDefault())
|
||||
return LocalDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(this))
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.core.coroutines.IODispatcher
|
||||
import dev.msfjarvis.claw.database.SavedPostSerializer
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.zacsweers.metro.Inject
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.encodeToStream
|
||||
|
||||
@Inject
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
class DataTransferRepository(
|
||||
private val json: Json,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
@param:IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val serializer = ListSerializer(SavedPostSerializer)
|
||||
|
||||
suspend fun importPosts(input: InputStream): Result<Unit> {
|
||||
return try {
|
||||
val posts: List<SavedPost> =
|
||||
withContext(ioDispatcher) { json.decodeFromStream(serializer, input) }
|
||||
savedPostsRepository.savePosts(posts)
|
||||
Result.success(Unit)
|
||||
} catch (t: Throwable) {
|
||||
Result.failure(t)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun exportPostsAsJson(output: OutputStream) {
|
||||
val posts = savedPostsRepository.savedPosts.first()
|
||||
withContext(ioDispatcher) { json.encodeToStream(serializer, posts, output) }
|
||||
}
|
||||
|
||||
suspend fun exportPostsAsHTML(output: OutputStream) {
|
||||
fun computeTimestamp(post: SavedPost): Long {
|
||||
val temporal = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(post.createdAt)
|
||||
val instant = Instant.from(temporal)
|
||||
return instant.toEpochMilli()
|
||||
}
|
||||
|
||||
val posts = savedPostsRepository.savedPosts.first()
|
||||
val header =
|
||||
"""
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<!-- This is an automatically generated file.
|
||||
It will be read and overwritten.
|
||||
DO NOT EDIT! -->
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
"""
|
||||
.trimIndent()
|
||||
val html = buildString {
|
||||
append(header)
|
||||
append("<DD><p>\n")
|
||||
for (post in posts) {
|
||||
append(
|
||||
"""
|
||||
<DT><A HREF="${post.url.ifEmpty { post.commentsUrl }}" ADD_DATE="${computeTimestamp(post)}" PRIVATE="0" TAGS="${post.tags.joinToString(",")}">${post.title}</A>
|
||||
<DD>${post.title}
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
append(
|
||||
"""
|
||||
<DT><A HREF="https://example.com/" ADD_DATE="0" PRIVATE="0" TAGS="delete,me,pls">Padding post</A>
|
||||
<DD>Linkding ignores the last entry so this pads the difference for imports
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
append("</DD></p>\n")
|
||||
}
|
||||
withContext(ioDispatcher) { output.bufferedWriter().use { writer -> writer.write(html) } }
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.core.coroutines.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.zacsweers.metro.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.saket.unfurl.Unfurler
|
||||
|
||||
@Inject
|
||||
class LinkMetadataRepository(
|
||||
private val unfurler: Unfurler,
|
||||
@param:IODispatcher private val dispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
suspend fun getLinkMetadata(url: String): LinkMetadata {
|
||||
val result = withContext(dispatcher) { unfurler.unfurl(url) }
|
||||
return LinkMetadata(url, result?.favicon?.toString())
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import dev.msfjarvis.claw.core.coroutines.DatabaseReadDispatcher
|
||||
import dev.msfjarvis.claw.core.coroutines.DatabaseWriteDispatcher
|
||||
import dev.msfjarvis.claw.database.local.ReadPostsQueries
|
||||
import dev.zacsweers.metro.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Inject
|
||||
class ReadPostsRepository(
|
||||
private val readPostsQueries: ReadPostsQueries,
|
||||
@param:DatabaseReadDispatcher private val readDispatcher: CoroutineDispatcher,
|
||||
@param:DatabaseWriteDispatcher private val writeDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
val readPosts = readPostsQueries.selectAllPosts().asFlow().mapToList(readDispatcher)
|
||||
|
||||
suspend fun markRead(postId: String) {
|
||||
withContext(writeDispatcher) { readPostsQueries.markRead(postId) }
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import dev.msfjarvis.claw.android.BuildConfig
|
||||
import dev.msfjarvis.claw.core.coroutines.DatabaseReadDispatcher
|
||||
import dev.msfjarvis.claw.core.coroutines.DatabaseWriteDispatcher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.database.local.SavedPostQueries
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import dev.zacsweers.metro.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Inject
|
||||
class SavedPostsRepository(
|
||||
private val savedPostQueries: SavedPostQueries,
|
||||
@param:DatabaseReadDispatcher private val readDispatcher: CoroutineDispatcher,
|
||||
@param:DatabaseWriteDispatcher private val writeDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(readDispatcher)
|
||||
val savedPostsSortedByDate =
|
||||
savedPostQueries.selectAllPostsSortedByDate().asFlow().mapToList(readDispatcher)
|
||||
|
||||
fun getPostsFromLastNDays(days: Long) =
|
||||
savedPostQueries.selectPostsFromLastNDays(days.toString()).asFlow().mapToList(readDispatcher)
|
||||
|
||||
fun getRecentPosts(limit: Long) =
|
||||
savedPostQueries.selectRecentPosts(limit).asFlow().mapToList(readDispatcher)
|
||||
|
||||
suspend fun toggleSave(post: UIPost) {
|
||||
val exists =
|
||||
withContext(readDispatcher) { savedPostQueries.postExists(post.shortId).executeAsOne() }
|
||||
if (exists) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Removing post: ${post.shortId}")
|
||||
}
|
||||
withContext(writeDispatcher) { savedPostQueries.deletePost(post.shortId) }
|
||||
} else {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Saving post: ${post.shortId}")
|
||||
}
|
||||
withContext(writeDispatcher) { savedPostQueries.insertOrReplacePost(post.toSavedPost()) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun savePosts(posts: List<SavedPost>) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Saving posts: ${posts.joinToString(",") { it.shortId }}")
|
||||
}
|
||||
withContext(writeDispatcher) {
|
||||
savedPostQueries.transaction {
|
||||
posts.forEach { post -> savedPostQueries.insertOrReplacePost(post) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "SavedPostsRepository"
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dev.msfjarvis.claw.core.network.SessionCookieStore
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoMap
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.binding
|
||||
import dev.zacsweers.metrox.viewmodel.ViewModelKey
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
@Inject
|
||||
@ViewModelKey
|
||||
@ContributesIntoMap(scope = AppScope::class, binding = binding<ViewModel>())
|
||||
class SettingsViewModel(
|
||||
private val sessionCookieStore: SessionCookieStore,
|
||||
private val webViewCookieStore: WebViewCookieStore,
|
||||
) : ViewModel() {
|
||||
|
||||
val isLoggedIn =
|
||||
sessionCookieStore
|
||||
.isLoggedIn()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
val username =
|
||||
sessionCookieStore
|
||||
.username()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
fun logout() {
|
||||
sessionCookieStore.clear()
|
||||
webViewCookieStore.clearLobstersCookies()
|
||||
}
|
||||
|
||||
fun saveCookie(cookie: String, username: String) {
|
||||
sessionCookieStore.set(cookie, username)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
||||
interface WebViewCookieStore {
|
||||
fun clearLobstersCookies()
|
||||
}
|
||||
|
||||
@Inject
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AndroidWebViewCookieStore : WebViewCookieStore {
|
||||
override fun clearLobstersCookies() {
|
||||
CookieManager.getInstance().removeAllCookies(null)
|
||||
CookieManager.getInstance().flush()
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.work
|
||||
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import dev.msfjarvis.claw.android.glance.SavedPostsWidget
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.core.injection.InjectedWorkerFactory
|
||||
import dev.msfjarvis.claw.core.injection.WorkerKey
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.ContributesIntoMap
|
||||
import dev.zacsweers.metro.binding
|
||||
import kotlin.random.Random
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/**
|
||||
* WorkManager-backed [CoroutineWorker] that gets all the posts from [SavedPostsRepository] that
|
||||
* were created in the last 30 days, fetches their newest state using the [LobstersApi] and then
|
||||
* writes them back to the database. API calls are staggered with a configurable delay to avoid rate
|
||||
* limiting. This allows saved posts to stay up-to-date with current comment counts and other
|
||||
* metadata changes.
|
||||
*/
|
||||
@AssistedInject
|
||||
class SavedPostUpdaterWorker(
|
||||
context: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val lobstersApi: LobstersApi,
|
||||
) : CoroutineWorker(context, params) {
|
||||
override suspend fun doWork(): Result {
|
||||
val postsToUpdate = savedPostsRepository.getPostsFromLastNDays(DAYS_TO_UPDATE).first()
|
||||
|
||||
if (postsToUpdate.isEmpty()) {
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val updatedPosts = mutableListOf<SavedPost>()
|
||||
|
||||
for ((index, post) in postsToUpdate.withIndex()) {
|
||||
when (val result = lobstersApi.getPostDetails(post.shortId)) {
|
||||
is Success -> {
|
||||
updatedPosts.add(result.value.toSavedPost())
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
if (index < postsToUpdate.lastIndex) {
|
||||
// Add a random delay between API calls to avoid rate limiting
|
||||
val delayMs = Random.nextLong(MIN_DELAY_MS, MAX_DELAY_MS)
|
||||
delay(delayMs)
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedPosts.isNotEmpty()) {
|
||||
savedPostsRepository.savePosts(updatedPosts)
|
||||
SavedPostsWidget().updateAll(applicationContext)
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DAYS_TO_UPDATE = 30L
|
||||
const val MIN_DELAY_MS = 500L
|
||||
const val MAX_DELAY_MS = 2000L
|
||||
}
|
||||
|
||||
@WorkerKey(SavedPostUpdaterWorker::class)
|
||||
@ContributesIntoMap(
|
||||
AppScope::class,
|
||||
binding = binding<InjectedWorkerFactory.WorkerInstanceFactory<*>>(),
|
||||
)
|
||||
@AssistedFactory
|
||||
abstract class Factory : InjectedWorkerFactory.WorkerInstanceFactory<SavedPostUpdaterWorker>
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.work
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import dev.msfjarvis.claw.common.tags.TagBlockRepository
|
||||
import dev.msfjarvis.claw.core.injection.InjectedWorkerFactory
|
||||
import dev.msfjarvis.claw.core.injection.WorkerKey
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.ContributesIntoMap
|
||||
import dev.zacsweers.metro.binding
|
||||
|
||||
@AssistedInject
|
||||
class TagExpirationCleanupWorker(
|
||||
context: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
private val tagBlockRepository: TagBlockRepository,
|
||||
) : CoroutineWorker(context, params) {
|
||||
override suspend fun doWork(): Result {
|
||||
tagBlockRepository.removeExpiredTags()
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
@WorkerKey(TagExpirationCleanupWorker::class)
|
||||
@ContributesIntoMap(
|
||||
AppScope::class,
|
||||
binding = binding<InjectedWorkerFactory.WorkerInstanceFactory<*>>(),
|
||||
)
|
||||
@AssistedFactory
|
||||
abstract class Factory : InjectedWorkerFactory.WorkerInstanceFactory<TagExpirationCleanupWorker>
|
||||
}
|
||||
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,17 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21.99,4c0,-1.1 -0.89,-2 -1.99,-2L4,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h14l4,4 -0.01,-18zM20,4v13.17L18.83,16L4,16L4,4h16zM6,12h12v2L6,14zM6,9h12v2L6,11zM6,6h12v2L6,8z"/>
|
||||
</vector>
|
||||
@@ -1,20 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1080"
|
||||
android:viewportHeight="1080">
|
||||
<group android:scaleX="0.7"
|
||||
android:scaleY="0.7"
|
||||
android:translateX="162"
|
||||
android:translateY="162">
|
||||
<path
|
||||
android:pathData="M431.3,341.2C435.9,337 580.3,236 710.5,223.8C747.3,220.3 790.7,254.7 833.2,312.5C875.9,370.7 892.9,433.4 875.9,443C863.2,450.2 838.6,407.4 758.8,396.3C743.4,394.2 730.4,393.4 719.4,393.5C738.5,423.4 745.6,451.1 737.2,458C733.1,461.4 673.6,438.7 612.9,436.5C570.1,435 535.2,440.9 506.7,451.3C496.9,454.9 486,453.8 477.2,448.3C444.5,427.9 424.2,406.2 418.4,376.2C415.8,362.8 420.9,349.4 431.3,341.2ZM274.7,394.8C285.8,370.1 312.2,325.9 346.3,305.4C383.6,282.9 335,390.3 427.3,489.8C519.5,589.3 586.5,623.8 655.1,581.5C723.7,539.2 746.5,518.1 753,518.7C764,519.6 772.9,536.1 764.4,589.5C763,598.2 761.3,606.5 759.3,614.2C828.7,575 871.7,516.3 889,520C912,524.8 918.3,617.8 852.1,721C811.8,784 768.8,830.7 741.4,857.5C724.3,874 699.7,880 677,873.1C512,822.6 315.5,733.8 202.9,652C193.8,645.2 186.8,635.9 183,625.1C171.8,593.7 161.3,548.1 168.5,495.3C179.5,414.2 222.7,352.9 245.2,335.9C261.9,323.2 267.1,362.4 274.7,394.8Z"
|
||||
android:fillColor="#FF4529"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,9 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#540C00</color>
|
||||
</resources>
|
||||
@@ -1,44 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name">Claw</string>
|
||||
<string name="saved_posts_widget_description">View your saved Lobsters posts</string>
|
||||
<string name="hottest_posts_widget_description">View the hottest posts on Lobsters</string>
|
||||
<string name="saved_posts">Saved posts</string>
|
||||
<string name="see_more_posts">See more posts</string>
|
||||
<plurals name="comments">
|
||||
<item quantity="one">%1$d comment</item>
|
||||
<item quantity="other">%1$d comments</item>
|
||||
</plurals>
|
||||
<string name="no_post_selected">No post selected</string>
|
||||
<string name="data_management">Data Management</string>
|
||||
<string name="data_transfer">Data transfer</string>
|
||||
<string name="import_and_export_your_saved_posts">Import and export your saved posts</string>
|
||||
<string name="app_information">App Information</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="version_placeholder">%1$s (%2$s)</string>
|
||||
<string name="posts_saved_locally">%1$s posts saved locally</string>
|
||||
<string name="tag_filtering">Tag filtering</string>
|
||||
<string name="filter_posts_by_tags">Filter posts by tags</string>
|
||||
<string name="about">About</string>
|
||||
<string name="libraries">Libraries</string>
|
||||
<string name="view_open_source_libraries_used_in_this_">View open source libraries used in this app</string>
|
||||
<string name="source_code">Source code</string>
|
||||
<string name="view_the_source_code_on_github">View the source code on GitHub</string>
|
||||
<string name="import_action">Import</string>
|
||||
<string name="export">Export</string>
|
||||
<string name="json">JSON</string>
|
||||
<string name="export_as_json">Export as JSON</string>
|
||||
<string name="bookmarks">Bookmarks</string>
|
||||
<string name="export_as_browser_bookmarks">Export as browser bookmarks</string>
|
||||
<string name="go_back_to_previous_screen">Go back to previous screen</string>
|
||||
<string name="the_app_icon_for_claw">The app icon for Claw</string>
|
||||
<string name="search_posts">Search posts</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="empty_inbox_icon">Empty inbox icon</string>
|
||||
<string name="no_saved_posts">No saved posts</string>
|
||||
</resources>
|
||||
@@ -1,17 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Claw.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/ic_launcher_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
||||
<item name="windowSplashScreenAnimationDuration">1000</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.Claw</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Claw" parent="android:Theme.Material.NoActionBar" />
|
||||
</resources>
|
||||
@@ -1,19 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<include
|
||||
domain="database"
|
||||
path="." />
|
||||
</cloud-backup>
|
||||
|
||||
<device-transfer>
|
||||
<include
|
||||
domain="database"
|
||||
path="." />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<full-backup-content>
|
||||
<include
|
||||
domain="database"
|
||||
path="." />
|
||||
</full-backup-content>
|
||||
@@ -1,17 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/hottest_posts_widget_description"
|
||||
android:previewImage="@drawable/hottest_posts_widget_preview"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="349dp"
|
||||
android:minHeight="110dp"
|
||||
android:minResizeWidth="349dp"
|
||||
android:minResizeHeight="110dp"
|
||||
android:resizeMode="vertical|horizontal"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen" />
|
||||
@@ -1,17 +0,0 @@
|
||||
<!--
|
||||
~ Copyright © Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/saved_posts_widget_description"
|
||||
android:previewImage="@drawable/saved_posts_widget_preview"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="349dp"
|
||||
android:minHeight="110dp"
|
||||
android:minResizeWidth="349dp"
|
||||
android:minResizeHeight="110dp"
|
||||
android:resizeMode="vertical|horizontal"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen" />
|
||||