Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4dc74c795 | ||
|
|
15d5913637 | ||
|
|
b0a10b37e6 | ||
|
|
f572e1255a | ||
|
|
5a491447fe | ||
|
|
81457f6c5b | ||
|
|
cda5c2d2d8 | ||
|
|
f9f11e51c8 | ||
|
|
19638658ec | ||
|
|
f8d220083a | ||
|
|
648f4502e6 | ||
|
|
5e2c2cbdde | ||
|
|
52035cde13 | ||
|
|
ea57889959 | ||
|
|
038ded5abd | ||
|
|
1b5d9a87e6 | ||
|
|
6f625a0f6f | ||
|
|
da1948c0e6 | ||
|
|
177d0b7162 | ||
|
|
5c350628ff | ||
|
|
cd014f2ba6 | ||
|
|
08ec5c267c | ||
|
|
d930450a6f | ||
|
|
d09b5cb306 | ||
|
|
89dde6e050 | ||
|
|
4b99abbfa4 | ||
|
|
edc3a017be | ||
|
|
ff69ccb384 | ||
|
|
09b3bd4e74 | ||
|
|
de8dd8a3cf | ||
|
|
928878928a | ||
|
|
58c5cfa5b6 | ||
|
|
3bf0849f1c | ||
|
|
85e65fed92 | ||
|
|
499fd23342 | ||
|
|
595dbd96e9 | ||
|
|
b62bc8dc89 | ||
|
|
a909f1274d | ||
|
|
94a7ad788b | ||
|
|
cfa8bc4971 | ||
|
|
a4ad701aff | ||
|
|
00e94eb1cb | ||
|
|
ff478b3365 | ||
|
|
7bf599ce19 | ||
|
|
a4b9e227bf | ||
|
|
cb9039e927 | ||
|
|
bf65027105 | ||
|
|
28d655efc2 | ||
|
|
e711781eb0 | ||
|
|
8576bad580 | ||
|
|
56e089ffa9 | ||
|
|
6625ebcfc7 | ||
|
|
540ea7ba43 | ||
|
|
dea2d8b468 | ||
|
|
ba292bf709 | ||
|
|
2ce4d1de2e | ||
|
|
9d6712d94b | ||
|
|
4e920328e1 | ||
|
|
1865c62db8 | ||
|
|
d61206a7d2 | ||
|
|
a94c48028d | ||
|
|
909c615ae6 | ||
|
|
5d87bb077f | ||
|
|
72b5b2c795 | ||
|
|
ce91650788 | ||
|
|
1b2534eca6 | ||
|
|
5e3356e1d4 | ||
|
|
ddc7a7598d | ||
|
|
2cfe9d73a2 | ||
|
|
dbed41bdbd | ||
|
|
a0296f5d5a | ||
|
|
39cca12708 | ||
|
|
0c4e3db59c | ||
|
|
8dea17cbf2 | ||
|
|
b428c73602 | ||
|
|
8c9ce29a7a | ||
|
|
f304304bc5 | ||
|
|
f14941cf27 | ||
|
|
c83afb3cd7 | ||
|
|
dfc6d80127 | ||
|
|
3ad1b07ff8 | ||
|
|
d3c2861939 | ||
|
|
8632f8fd7a | ||
|
|
83585b4a8a | ||
|
|
e2e3186bb4 | ||
|
|
29765f0024 | ||
|
|
43edeaff04 | ||
|
|
2b7cf7d3cb | ||
|
|
fdf13fd201 | ||
|
|
92ba77a838 | ||
|
|
d910810dd4 | ||
|
|
5f8853c69d | ||
|
|
0f9b35efb1 | ||
|
|
7096e62948 | ||
|
|
acb7c0e670 | ||
|
|
bb91e6e083 | ||
|
|
67a922362d | ||
|
|
56be44b918 | ||
|
|
15493aec1a | ||
|
|
9aef7582bd | ||
|
|
5378ec6854 | ||
|
|
79dbb64275 | ||
|
|
b8912e747a | ||
|
|
a9dcd0157a | ||
|
|
45e8993d75 | ||
|
|
39bd1fa2eb | ||
|
|
5b33fbc20b | ||
|
|
196195d867 | ||
|
|
20b8076235 | ||
|
|
5f49c8149a | ||
|
|
fb4beb08ee | ||
|
|
6750eff9fa | ||
|
|
692cc8ce67 | ||
|
|
a59b6411b1 | ||
|
|
1345709450 | ||
|
|
8433f38f6e | ||
|
|
91c3d0f9a6 | ||
|
|
579a2ffe2e | ||
|
|
918f28feab | ||
|
|
7b13d40797 | ||
|
|
002588a45a | ||
|
|
1ae0cbb95d | ||
|
|
3e2d32e4c9 | ||
|
|
403f7d47b1 | ||
|
|
d1fcebdf4c | ||
|
|
a983bd729c | ||
|
|
8d311a1193 | ||
|
|
6bbd2c73b5 | ||
|
|
f851e2d626 | ||
|
|
97aafc764f | ||
|
|
183e10dba8 | ||
|
|
f5deaaa8ff | ||
|
|
55537ea191 | ||
|
|
98818023e9 | ||
|
|
e67df253de | ||
|
|
53524c8ef8 | ||
|
|
bc90a8941a | ||
|
|
5c413d1f12 | ||
|
|
8b5ab33730 | ||
|
|
d4d1288900 | ||
|
|
f4f07d0ce3 | ||
|
|
6282f4d6e0 | ||
|
|
19f5f8a0ca | ||
|
|
a296d2b827 | ||
|
|
45872f1653 | ||
|
|
074420056b | ||
|
|
9f7e05d79a | ||
|
|
955299bd89 | ||
|
|
6520758722 | ||
|
|
583137aa25 | ||
|
|
8afce7706c | ||
|
|
7ae0a0bae6 | ||
|
|
f34d6c5222 | ||
|
|
002f4f9309 | ||
|
|
bf77a9aaed | ||
|
|
d17c178eaa | ||
|
|
2c0ba0fae9 | ||
|
|
0396a488cd | ||
|
|
f5ef483cd9 | ||
|
|
78960f6d4e | ||
|
|
2be9408560 | ||
|
|
46dc6fbe91 | ||
|
|
e411cb3d7f | ||
|
|
00535dc859 | ||
|
|
465e6931ad | ||
|
|
7e64100491 | ||
|
|
e199f00ca0 | ||
|
|
8722636b13 | ||
|
|
5db4a1ee8d | ||
|
|
b0bae12e6b | ||
|
|
585d42c6aa | ||
|
|
47448616dd | ||
|
|
ccd64b46e3 | ||
|
|
bce227357f | ||
|
|
2ec1633c51 | ||
|
|
ed8528aea4 | ||
|
|
3cc74f9bcf | ||
|
|
015c9929b3 | ||
|
|
76737b0eed | ||
|
|
28c454261f | ||
|
|
327ec9b6a5 | ||
|
|
fc617a5dfa | ||
|
|
c6289908c4 | ||
|
|
ca0706f94b | ||
|
|
350a64d479 | ||
|
|
058561e4d6 | ||
|
|
a4bc5476a4 | ||
|
|
1fe559ff04 | ||
|
|
78f050f115 | ||
|
|
0d964c4a5d | ||
|
|
c24c433c0e | ||
|
|
fd9010d468 | ||
|
|
90f40075e8 | ||
|
|
f621f785f8 | ||
|
|
76d62906c8 | ||
|
|
dbeafbb569 | ||
|
|
fab5cb45c4 | ||
|
|
7b593806eb | ||
|
|
f6064616b0 | ||
|
|
36d999ebb4 | ||
|
|
79920f6180 | ||
|
|
36ab72226c | ||
|
|
7cc81047d4 | ||
|
|
82252dbe2d | ||
|
|
da1825498a | ||
|
|
947a04d82d | ||
|
|
75ab508dc3 | ||
|
|
1d2da7c2f8 | ||
|
|
dd019279ec | ||
|
|
485282a48d | ||
|
|
eeef2c347d | ||
|
|
b7229c6d15 | ||
|
|
da80605ae1 | ||
|
|
4066a86ad6 | ||
|
|
0f595f7b02 | ||
|
|
42f3068b01 | ||
|
|
0312c3f431 | ||
|
|
7d324ddb8c | ||
|
|
c7b7a786f3 | ||
|
|
26f69c2027 | ||
|
|
450601f26b | ||
|
|
2ab69de686 | ||
|
|
0083aa69b5 | ||
|
|
18d82a5eb3 | ||
|
|
0d668ac3aa | ||
|
|
b1a32496ba | ||
|
|
4db59db7a1 | ||
|
|
0843352514 | ||
|
|
aa50d6449d | ||
|
|
6a41b966c5 | ||
|
|
943b9bd091 | ||
|
|
010f69031a | ||
|
|
168271a8f4 | ||
|
|
ea92d64972 | ||
|
|
766f14cd22 | ||
|
|
33210319bb | ||
|
|
7ef0a8fc51 | ||
|
|
1f850435b1 | ||
|
|
9d8fd77a08 | ||
|
|
e2c760e7f3 | ||
|
|
51a8538d6a | ||
|
|
4b9b148519 | ||
|
|
ced16c3794 | ||
|
|
e824294ea0 | ||
|
|
ffce7c2b3e | ||
|
|
f0d615bd44 | ||
|
|
df546947bb | ||
|
|
3e0544279c | ||
|
|
25bfed3c71 | ||
|
|
6c88ecf5d5 | ||
|
|
4f60db29bd | ||
|
|
e6eaadbaa1 | ||
|
|
30fa04db56 | ||
|
|
74684fe115 | ||
|
|
d36bcd6b26 | ||
|
|
a2f3faad0f | ||
|
|
9b25cd0fea | ||
|
|
db75d1edfe | ||
|
|
ac98ebda3f | ||
|
|
c8d03c515a | ||
|
|
a77f098822 | ||
|
|
608720ac35 | ||
|
|
f1b68b238c | ||
|
|
977e3cbc8e | ||
|
|
fe2c3f630b | ||
|
|
4fac861656 | ||
|
|
0ec03fc979 | ||
|
|
46a9658dcd | ||
|
|
387881181a | ||
|
|
cb619ca6a9 | ||
|
|
37fc528053 | ||
|
|
b19617f8c7 | ||
|
|
3829abeb8f | ||
|
|
bd5cf0f167 | ||
|
|
a8ba0ed3d9 | ||
|
|
1267cf81e3 | ||
|
|
f48ef0206f | ||
|
|
7b31dc740b | ||
|
|
69c76a06c2 | ||
|
|
83e01c7cbc | ||
|
|
b6421b01ba | ||
|
|
9bc4a8c81b | ||
|
|
8cae135784 | ||
|
|
eba5d64998 | ||
|
|
8eec09e832 | ||
|
|
69537da95b | ||
|
|
a679b88497 | ||
|
|
e570b9d88f | ||
|
|
ce37523e11 | ||
|
|
3601c190ac | ||
|
|
b0044dd0fc | ||
|
|
b2c2be45bf | ||
|
|
6f219b6da2 | ||
|
|
8d6c567c84 | ||
|
|
2b2010a22c | ||
|
|
27af06e180 | ||
|
|
499088cf43 | ||
|
|
ce579f21f9 | ||
|
|
5a4643dc57 | ||
|
|
2a65dc4a9e | ||
|
|
d08aa25f76 | ||
|
|
410c7ccd1d | ||
|
|
5443cc1486 | ||
|
|
683cb0cfbb | ||
|
|
cbf89b4158 | ||
|
|
bbf315fecc | ||
|
|
238d410245 | ||
|
|
41adf7566e | ||
|
|
9f6d231912 | ||
|
|
84e09fd5e1 | ||
|
|
e12fc5ae6a | ||
|
|
9aa50637e0 | ||
|
|
8e4330aea3 | ||
|
|
6b05a2991d | ||
|
|
6cd57b72b7 | ||
|
|
a03ff1a60b | ||
|
|
d59a151944 | ||
|
|
44733a05cd | ||
|
|
98c489a32a | ||
|
|
f5b731a6e1 | ||
|
|
180aafc71b | ||
|
|
2320917c46 | ||
|
|
22081227c0 | ||
|
|
b893aff077 | ||
|
|
1558bc769d | ||
|
|
7abe629744 | ||
|
|
f31d388c04 | ||
|
|
8e9ec3ab16 | ||
|
|
86dd862469 | ||
|
|
c90e5b2fe5 | ||
|
|
9f5722a170 | ||
|
|
5d8a08de2f | ||
|
|
4b2382692e | ||
|
|
20d527246a | ||
|
|
e4499ab8d3 | ||
|
|
eaf6d55108 | ||
|
|
9b2da12451 | ||
|
|
9cccf2f42e | ||
|
|
c437d32b51 | ||
|
|
6249364c5e | ||
|
|
b3b896694f | ||
|
|
00f164e347 | ||
|
|
e6ebb10bff | ||
|
|
960222c489 | ||
|
|
b82f93b459 | ||
|
|
d3a6a82b50 | ||
|
|
f429faf8b8 | ||
|
|
15d18414df | ||
|
|
f7b2b0f9bc | ||
|
|
2c1fdd51f8 | ||
|
|
a10ba38950 | ||
|
|
f7b4f4383c | ||
|
|
fb8bbf85c9 | ||
|
|
ee80a5a52e | ||
|
|
8a5a898be3 | ||
|
|
ed23fab7d9 | ||
|
|
0eba2aeaf4 | ||
|
|
b728103b13 | ||
|
|
abe8ec77cc | ||
|
|
fa62d871f8 | ||
|
|
aaf67de8d4 | ||
|
|
4ac8882bec | ||
|
|
6a8893a5aa | ||
|
|
839773e48e | ||
|
|
aa2a109587 | ||
|
|
0232a4179a | ||
|
|
4a87682b68 | ||
|
|
52562afbd1 | ||
|
|
15abd31b03 | ||
|
|
157fe2c221 | ||
|
|
93db17e6d7 | ||
|
|
c75d1959a6 | ||
|
|
72acf9c850 | ||
|
|
77af00229f | ||
|
|
0390336d50 | ||
|
|
0bf60812e0 | ||
|
|
8a4fee5f7c | ||
|
|
005dc9ab83 | ||
|
|
9d41b7e819 | ||
|
|
b8965902ba | ||
|
|
ef5242f115 | ||
|
|
0dd2a3be6f | ||
|
|
10eecce01c | ||
|
|
4fe7c64e42 | ||
|
|
6d88335e13 | ||
|
|
e3e14edb5d | ||
|
|
56d47d1f32 | ||
|
|
ce37903ea0 | ||
|
|
3c0a8dd392 | ||
|
|
e7228d35df | ||
|
|
28c6f62b81 | ||
|
|
4a48a2ba4c | ||
|
|
18ed012154 | ||
|
|
653787c946 | ||
|
|
e90c2f5a7d | ||
|
|
5a175c162c | ||
|
|
1373198df0 | ||
|
|
0ab452f943 | ||
|
|
07e0de1b0d | ||
|
|
f92fb48752 | ||
|
|
dc7ef55461 | ||
|
|
6581e56c31 | ||
|
|
da40cf3892 | ||
|
|
2eb0a6a31c | ||
|
|
2d44ad2055 | ||
|
|
8af4bddfe1 | ||
|
|
0be0952e04 | ||
|
|
8b2ba790bc | ||
|
|
6ed377af94 | ||
|
|
cfc479a9ee | ||
|
|
7552fb1c36 | ||
|
|
bcd8a412b5 | ||
|
|
defe4914df | ||
|
|
621e93939b | ||
|
|
247949f92e | ||
|
|
5700b2150e | ||
|
|
530ad44b27 | ||
|
|
51c7306cfa | ||
|
|
79778466b9 | ||
|
|
7d7989d5c0 | ||
|
|
923361ee1b | ||
|
|
6adf339e08 | ||
|
|
9e37246810 | ||
|
|
7be1157f22 | ||
|
|
cd7d82d378 | ||
|
|
b30425d97d | ||
|
|
c89589adcb | ||
|
|
6523b85f07 | ||
|
|
33f8b73938 | ||
|
|
bb47119c84 | ||
|
|
6adb45ff7c | ||
|
|
a87b1cf38c | ||
|
|
b112585533 | ||
|
|
5750bbebd3 | ||
|
|
067b762a31 | ||
|
|
658a2f36fd | ||
|
|
6a4d0c2d3a | ||
|
|
5319669a8e | ||
|
|
720f946f41 | ||
|
|
a1390a0f05 | ||
|
|
655b20e496 | ||
|
|
8508a8646f | ||
|
|
790874e1a7 | ||
|
|
0ac7c9876e | ||
|
|
daba852e6d | ||
|
|
47f7f97aa2 | ||
|
|
80bfb34e5b | ||
|
|
42310a6a33 | ||
|
|
c554459191 | ||
|
|
1560574e15 | ||
|
|
a1f3950042 | ||
|
|
d7bafbddbc | ||
|
|
ef5e4a952e | ||
|
|
5711bab007 | ||
|
|
b1fca935a8 | ||
|
|
2780f7b204 | ||
|
|
22231606cb | ||
|
|
c797853055 | ||
|
|
b003f0c283 | ||
|
|
cc55687292 | ||
|
|
531234e036 | ||
|
|
ed5bbdd571 | ||
|
|
23cd91a97a | ||
|
|
6761659c7f | ||
|
|
6bd3dd81f0 | ||
|
|
765a7f6f0b | ||
|
|
46f4d9a397 | ||
|
|
2cc9e38e77 | ||
|
|
dc954c7ba5 | ||
|
|
15dc9c3dc7 | ||
|
|
a4be2d4bf2 | ||
|
|
5329b6e3aa | ||
|
|
204a3c0bb5 | ||
|
|
69eed8a23a | ||
|
|
3c741a7fe1 | ||
|
|
ccfcd48438 | ||
|
|
79aba9a187 | ||
|
|
4337a19abc | ||
|
|
a53e9f0065 | ||
|
|
e6ca8365ca | ||
|
|
f1805396e5 | ||
|
|
6d70d997a3 | ||
|
|
4e872bfa4a | ||
|
|
fdc9ae8968 | ||
|
|
f31debc685 | ||
|
|
74c265538d | ||
|
|
4607a68de0 | ||
|
|
8652d4ceaa | ||
|
|
484fac5779 | ||
|
|
0d7a7a0e22 | ||
|
|
8470e95f6b | ||
|
|
ad72c80130 | ||
|
|
be7840125f | ||
|
|
cf3549f09a | ||
|
|
6ee00f6a27 | ||
|
|
d963fd8ae4 | ||
|
|
32ca4f2115 | ||
|
|
95cbe8dfe4 | ||
|
|
7e7e00f06e | ||
|
|
8eee42ae0f | ||
|
|
8060c347cc | ||
|
|
8851071d58 | ||
|
|
54c4b01411 | ||
|
|
176bc9b022 | ||
|
|
245130ef20 | ||
|
|
394558d634 | ||
|
|
0904740980 | ||
|
|
4212ca41cf | ||
|
|
33b8503f19 | ||
|
|
eb8be2f909 | ||
|
|
e1d16e9f36 | ||
|
|
dd76c7bed7 | ||
|
|
a1ab32c43e | ||
|
|
069d7e5e05 | ||
|
|
8f927613f3 | ||
|
|
f5de95af03 | ||
|
|
535c8a5808 | ||
|
|
02416a3606 | ||
|
|
293d12454f | ||
|
|
d178bebebd | ||
|
|
38f1fdb6a3 | ||
|
|
46bfafd681 | ||
|
|
999f2dfb01 | ||
|
|
52ac8346b3 | ||
|
|
1d761b1f6e | ||
|
|
16404e6654 | ||
|
|
0f684a3728 | ||
|
|
2685111c70 | ||
|
|
b867804bdb | ||
|
|
37bb6e1d36 | ||
|
|
78c321d269 | ||
|
|
cab3f74ce0 | ||
|
|
f0cff549b4 | ||
|
|
5ab0774689 | ||
|
|
8835e07ac6 | ||
|
|
85de847692 | ||
|
|
ccc14dee4f |
@@ -0,0 +1,93 @@
|
||||
# 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
|
||||
@@ -3,3 +3,6 @@
|
||||
e7f4c8a7ebe4e907f65af46e6dddfd3a542415c0
|
||||
ab2713154f846b3db6d44625d1022bf421b275d0
|
||||
cbdfa67d075ec1f8ceec0f3559498cbca01783a0
|
||||
|
||||
# Renamed Renovate config file
|
||||
f304304bc5b64edf189aaa473b05df912067a8bd
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
* text=auto eol=lf
|
||||
**/dependencies/** linguist-generated
|
||||
**/clawicons/** linguist-generated
|
||||
android/src/main/baseline-prof.txt linguist-generated -diff
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 127 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
":dependencyDashboard",
|
||||
"github>msfjarvis/shared-workflows//renovate/automerge"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"managers": [
|
||||
"gradle"
|
||||
],
|
||||
"packagePatterns": [
|
||||
"^org.jetbrains.kotlin",
|
||||
"^com.squareup.okhttp3"
|
||||
],
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"regexManagers": [
|
||||
{
|
||||
"fileMatch": [
|
||||
".kt$"
|
||||
],
|
||||
"matchStrings": [
|
||||
"KTFMT_VERSION = \"(?<currentValue>.*)\""
|
||||
],
|
||||
"datasourceTemplate": "maven",
|
||||
"depNameTemplate": "com.facebook:ktfmt"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
":dependencyDashboard",
|
||||
"helpers:pinGitHubActionDigests",
|
||||
"github>msfjarvis/shared-workflows//renovate/automerge"
|
||||
],
|
||||
"branchConcurrentLimit": 15,
|
||||
"packageRules": [
|
||||
{
|
||||
matchDatasources: [
|
||||
"maven"
|
||||
],
|
||||
registryUrls: [
|
||||
"https://maven.google.com/",
|
||||
"https://repo1.maven.org/maven2",
|
||||
"https://plugins.gradle.org/m2/",
|
||||
],
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^kotlin",
|
||||
"^org.jetbrains.kotlin",
|
||||
"^androidx.compose.compiler"
|
||||
],
|
||||
"groupName": "kotlin"
|
||||
},
|
||||
{
|
||||
"managers": [
|
||||
"gradle"
|
||||
],
|
||||
"packagePatterns": [
|
||||
// We want to stay in sync with Retrofit
|
||||
"^com.squareup.okhttp3",
|
||||
// Renovate refuses to stop resolving this from the snapshots repo
|
||||
"^app.cash.sqldelight",
|
||||
],
|
||||
"enabled": false
|
||||
},
|
||||
],
|
||||
"regexManagers": [
|
||||
{
|
||||
"fileMatch": [
|
||||
"SpotlessPlugin\.kt$"
|
||||
],
|
||||
"matchStrings": [
|
||||
"KTFMT_VERSION = \"(?<currentValue>.*)\""
|
||||
],
|
||||
"datasourceTemplate": "maven",
|
||||
"depNameTemplate": "com.facebook:ktfmt",
|
||||
"registryUrlTemplate": "https://repo1.maven.org/maven2/",
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"Detekt\.kt$"
|
||||
],
|
||||
"matchStrings": [
|
||||
"TWITTER_RULES_VERSION = \"(?<currentValue>.*)\""
|
||||
],
|
||||
"datasourceTemplate": "maven",
|
||||
"depNameTemplate": "com.twitter.compose.rules:detekt",
|
||||
"registryUrlTemplate": "https://repo1.maven.org/maven2/",
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"gradle/libs.versions.toml"
|
||||
],
|
||||
"matchStrings": [
|
||||
"composeCompiler = \"(?<currentValue>.*)\""
|
||||
],
|
||||
"datasourceTemplate": "maven",
|
||||
"depNameTemplate": "androidx.compose.compiler:compiler",
|
||||
"registryUrlTemplate": "https://maven.google.com",
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"qodana.yaml"
|
||||
],
|
||||
"matchStrings": [
|
||||
"jetbrains/qodana-jvm-android:(?<currentValue>.*)"
|
||||
],
|
||||
"datasourceTemplate": "docker",
|
||||
"depNameTemplate": "jetbrains/qodana-jvm-android",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
name: Baseline profile generation
|
||||
|
||||
on:
|
||||
# every day at 00:43
|
||||
schedule:
|
||||
- cron: '43 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
AVD_API_LEVEL: 31
|
||||
AVD_ARCH: x86_64
|
||||
|
||||
jobs:
|
||||
baseline-profile:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
TERM: dumb
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@3f07048e3d294f56e9b90ac5ea2c6f74e9ad0f98 # v3.10.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Set up Git author
|
||||
shell: bash
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email noreply@github.com
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2.3.3
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
# Restore AVD from cache
|
||||
- name: Cache AVD
|
||||
uses: actions/cache@627f0f41f6904a5b1efbaed9f96d9eb58e92e920 # v3.2.4
|
||||
id: avd-cache
|
||||
with:
|
||||
path: |
|
||||
~/.android/avd/*
|
||||
~/.android/adb*
|
||||
key: ${{ runner.os }}-avd-${{ env.AVD_API_LEVEL }}-${{ env.AVD_ARCH }}
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$ENCRYPT_KEY"
|
||||
env:
|
||||
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
|
||||
|
||||
# This allows us to build most of what we need without the emulator running
|
||||
# and using resources
|
||||
- name: Build app and benchmark
|
||||
run: ./gradlew :benchmark:assembleBenchmark :android:assembleBenchmark
|
||||
|
||||
# Now use reactivecircus/android-emulator-runner to spin up an emulator. We're gonna use it again
|
||||
# to actually run the test once the emulator has booted.
|
||||
- name: Create AVD snapshot for caching
|
||||
if: steps.avd-cache.outputs.cache-hit != 'true'
|
||||
uses: reactivecircus/android-emulator-runner@50986b1464923454c95e261820bc626f38490ec0 # v2
|
||||
with:
|
||||
api-level: ${{ env.AVD_API_LEVEL }}
|
||||
arch: ${{ env.AVD_ARCH }}
|
||||
channel: canary
|
||||
target: aosp_atd
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: false
|
||||
disk-size: 8G
|
||||
script: echo "Generated AVD snapshot for caching."
|
||||
|
||||
# Run our baseline profile generator. We need to manually pull the baseline profiles off the
|
||||
# emulator when using the GA runner
|
||||
- name: Run benchmark
|
||||
uses: reactivecircus/android-emulator-runner@50986b1464923454c95e261820bc626f38490ec0 # v2
|
||||
with:
|
||||
api-level: ${{ env.AVD_API_LEVEL }}
|
||||
arch: ${{ env.AVD_ARCH }}
|
||||
channel: canary
|
||||
target: aosp_atd
|
||||
disable-animations: true
|
||||
disable-spellchecker: true
|
||||
disk-size: 8G
|
||||
script: |
|
||||
# Run our benchmark, enabling only tests using BaselineProfile
|
||||
./gradlew connectedBenchmarkAndroidTest -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile --stacktrace
|
||||
# Need to manually pull the generated profiles from the emulator
|
||||
adb pull /sdcard/Android/media/dev.msfjarvis.claw.benchmark benchmark/build/outputs/baseline-prof/
|
||||
|
||||
# If we're on main branch, copy over the baseline profile and
|
||||
# commit it to the repository (if changed)
|
||||
- name: Commit baseline profile into main
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
# Pull down any changes which may have been committed while this workflow has been running
|
||||
git pull
|
||||
# Sort the baseline profile, saving it to app/
|
||||
sort -o android/src/main/baseline-prof.txt benchmark/build/outputs/baseline-prof/BaselineProfileGenerator_startup-baseline-prof.txt
|
||||
# If the baseline profile has changed, commit it
|
||||
if [[ $(git diff --stat android/src/main/baseline-prof.txt) != '' ]]; then
|
||||
git add android/src/main/baseline-prof.txt
|
||||
git commit -m "chore(android): refresh baseline profile" && git push
|
||||
fi
|
||||
|
||||
# Upload the entire generated folder and attach it to the CI run
|
||||
- name: Attach baseline profile
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: Baseline profile output
|
||||
path: benchmark/build/outputs/baseline-prof
|
||||
|
||||
- name: Clean secrets
|
||||
if: always()
|
||||
run: scripts/signing-cleanup.sh
|
||||
+24
-10
@@ -10,28 +10,39 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3.5.1
|
||||
uses: actions/setup-java@3f07048e3d294f56e9b90ac5ea2c6f74e9ad0f98 # v3.10.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Run unit tests
|
||||
uses: gradle/gradle-build-action@v2.3.2
|
||||
uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2.3.3
|
||||
with:
|
||||
arguments: check -PslimTests
|
||||
arguments: --no-configuration-cache --stacktrace check -PslimTests
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
- name: Run Detekt
|
||||
uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2.3.3
|
||||
with:
|
||||
arguments: --no-configuration-cache --stacktrace detektMain
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
- name: (Fail-only) Upload test report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: Test report
|
||||
path: android/build/reports
|
||||
@@ -42,24 +53,27 @@ jobs:
|
||||
needs: [ "check" ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3.5.1
|
||||
uses: actions/setup-java@3f07048e3d294f56e9b90ac5ea2c6f74e9ad0f98 # v3.10.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: scripts/signing-setup.sh "$ENCRYPT_KEY"
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$ENCRYPT_KEY"
|
||||
env:
|
||||
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
|
||||
|
||||
- name: Build release app
|
||||
uses: gradle/gradle-build-action@v2.3.2
|
||||
uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2.3.3
|
||||
with:
|
||||
arguments: collectReleaseApks
|
||||
arguments: --no-configuration-cache --stacktrace collectReleaseApks
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
- name: Clean secrets
|
||||
run: scripts/signing-cleanup.sh
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8775e868027fa230df8586bdf502bbd9b618a477 # v2
|
||||
with:
|
||||
languages: java
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@8775e868027fa230df8586bdf502bbd9b618a477 # v2
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8775e868027fa230df8586bdf502bbd9b618a477 # v2
|
||||
with:
|
||||
category: "/language:java"
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Qodana
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
qodana:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Qodana Scan'
|
||||
uses: JetBrains/qodana-action@584b706efdfc1149cfb5234066b3bbf73d48249e # v2022.3.3
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
@@ -0,0 +1,111 @@
|
||||
name: Release to Google Play
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish-google-play-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@3f07048e3d294f56e9b90ac5ea2c6f74e9ad0f98 # v3.10.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Set up Git author
|
||||
shell: bash
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email noreply@github.com
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2.3.3
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$ENCRYPT_KEY"
|
||||
env:
|
||||
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
|
||||
|
||||
- name: Get current version
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew -q --no-configuration-cache clearPreRelease
|
||||
VERSION="$(tail -n1 android/version.properties | cut -d = -f 2)"
|
||||
echo VERSION="${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Update changelog
|
||||
uses: thomaseizinger/keep-a-changelog-new-release@5bc232893483441c5d6cd6c9dcb1e48abf9a2bae # 1.3.0
|
||||
with:
|
||||
tag: v${{ env.VERSION }}
|
||||
version: ${{ env.VERSION }}
|
||||
|
||||
- name: Commit changes
|
||||
shell: bash
|
||||
run: |
|
||||
git commit -am 'feat(release): bump version'
|
||||
|
||||
- name: Build release assets
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew --no-configuration-cache collectReleaseApks collectReleaseBundle
|
||||
|
||||
- name: Clean secrets
|
||||
run: scripts/signing-cleanup.sh
|
||||
|
||||
- name: Get changelog entry
|
||||
id: changelog_reader
|
||||
uses: mindsers/changelog-reader-action@b97ce03a10d9bdbb07beb491c76a5a01d78cd3ef # v2.2.2
|
||||
with:
|
||||
version: ${{ env.VERSION }}
|
||||
path: ./CHANGELOG.md
|
||||
|
||||
- name: Write release notes file
|
||||
shell: bash
|
||||
env:
|
||||
CHANGELOG: ${{ steps.changelog_reader.outputs.changes }}
|
||||
run: |
|
||||
mkdir -p distribution/whatsnew
|
||||
printf '## Changelog\n\n' > distribution/whatsnew/whatsnew-en-GB
|
||||
printf '%s' "${CHANGELOG}" >> distribution/whatsnew/whatsnew-en-GB
|
||||
|
||||
- name: Publish bundle to Google Play
|
||||
uses: r0adkll/upload-google-play@78c9e796b1035c94169c101d8e46cb880194bfc3 # tag=v1.0.19
|
||||
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
|
||||
|
||||
- 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 a GitHub release
|
||||
gh release create "v${VERSION}" --notes-file distribution/whatsnew/whatsnew-en-GB --title "v${VERSION}" ./android/apk/*.apk ./android/bundle/*.aab
|
||||
|
||||
# Start the next development iteration
|
||||
./gradlew -q --no-configuration-cache bumpSnapshot
|
||||
truncate -s 0 distribution/whatsnew/whatsnew-en-GB # Clear changelog
|
||||
git commit -am 'feat(release): start next development iteration'
|
||||
git push -u origin
|
||||
@@ -242,3 +242,6 @@ hs_err_pid*
|
||||
!/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/androidstudio,gradle,intellij+all
|
||||
|
||||
keystore.properties
|
||||
distribution/
|
||||
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
# 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.20.0] - 2023-02-09
|
||||
|
||||
## [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 newest posts tab actually showed 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 [jcs](https://lobste.rs/u/jcs)' comments
|
||||
|
||||
## [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.20.0...HEAD
|
||||
|
||||
[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,23 @@
|
||||
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.
|
||||
@@ -2,44 +2,18 @@
|
||||
|
||||
Unofficial Android app for read-only access to [lobste.rs](https://lobste.rs), built with [Jetpack Compose](https://developer.android.com/jetpack/compose).
|
||||
|
||||

|
||||
|
||||
## Download
|
||||
|
||||
<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>
|
||||
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature.webp"
|
||||
alt="Side by side screenshots of the app's main page in dark and light UI modes"
|
||||
height="550" />
|
||||
|
||||
Snapshots from the development branch can be obtained [here](https://github.com/msfjarvis/compose-lobsters/releases/tag/nightly).
|
||||
|
||||
## License
|
||||
|
||||
```
|
||||
Copyright (c) 2021 Harsh Shandilya
|
||||
|
||||
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.
|
||||
```
|
||||
See [LICENSE](LICENSE)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/apk/
|
||||
/bundle/
|
||||
@@ -0,0 +1 @@
|
||||
-dontobfuscate
|
||||
+24
-25
@@ -1,18 +1,27 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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("DSL_SCOPE_VIOLATION", "UnstableApiUsage")
|
||||
|
||||
import dagger.hilt.android.plugin.HiltExtension
|
||||
|
||||
plugins {
|
||||
id("dev.msfjarvis.claw.android-application")
|
||||
id("dev.msfjarvis.claw.rename-artifacts")
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
id("dev.msfjarvis.claw.kotlin-kapt")
|
||||
id("dev.msfjarvis.claw.versioning-plugin")
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.napt)
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.whetstone)
|
||||
}
|
||||
|
||||
// Hilt's aggregating task fails with NAPT
|
||||
extensions.getByType<HiltExtension>().enableAggregatingTask = false
|
||||
whetstone {
|
||||
addOns {
|
||||
compose.set(true)
|
||||
workManager.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.msfjarvis.claw.android"
|
||||
@@ -24,47 +33,37 @@ android {
|
||||
}
|
||||
buildTypes {
|
||||
create("benchmark") {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks += listOf("release")
|
||||
signingConfig = signingConfigs["debug"]
|
||||
matchingFallbacks += "release"
|
||||
isDebuggable = false
|
||||
proguardFile("benchmark-rules.pro")
|
||||
}
|
||||
}
|
||||
lint {
|
||||
abortOnError = false
|
||||
checkReleaseBuilds = false
|
||||
warningsAsErrors = false
|
||||
disable.add("DialogFragmentCallbacksDetector")
|
||||
baseline = file("lint-baseline.xml")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
annotationProcessor(libs.androidx.hilt.compiler)
|
||||
annotationProcessor(libs.dagger.hilt.compiler)
|
||||
kapt(libs.dagger.compiler)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.api)
|
||||
implementation(projects.common)
|
||||
implementation(projects.core)
|
||||
implementation(projects.coroutineUtils)
|
||||
implementation(projects.database)
|
||||
implementation(projects.metadataExtractor)
|
||||
implementation(projects.model)
|
||||
implementation(libs.accompanist.swiperefresh)
|
||||
implementation(libs.accompanist.sysuicontroller)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.material)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.androidx.hilt.work)
|
||||
implementation(libs.androidx.lifecycle.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.coil)
|
||||
implementation(libs.copydown)
|
||||
implementation(libs.dagger.hilt.android)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.material.motion.core)
|
||||
implementation(libs.material.motion.navigation)
|
||||
implementation(libs.okhttp.loggingInterceptor)
|
||||
implementation(libs.retrofit.kotlinxSerializationConverter)
|
||||
implementation(libs.sqldelight.extensions.coroutines)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
public static final ** CREATOR;
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
-keep class androidx.annotation.Keep
|
||||
|
||||
Vendored
-27
@@ -1,29 +1,2 @@
|
||||
# Keep `Companion` object fields of serializable classes.
|
||||
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
|
||||
-if @kotlinx.serialization.Serializable class **
|
||||
-keepclassmembers class <1> {
|
||||
static <1>$Companion Companion;
|
||||
}
|
||||
|
||||
# Keep `serializer()` on companion objects (both default and named) of serializable classes.
|
||||
-if @kotlinx.serialization.Serializable class ** {
|
||||
static **$* *;
|
||||
}
|
||||
-keepclassmembers class <2>$<3> {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
# Keep `INSTANCE.serializer()` of serializable objects.
|
||||
-if @kotlinx.serialization.Serializable class ** {
|
||||
public static ** INSTANCE;
|
||||
}
|
||||
-keepclassmembers class <1> {
|
||||
public static <1> INSTANCE;
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||
|
||||
-dontobfuscate
|
||||
-keepattributes SourceFile, LineNumberTable
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Claw.SplashScreen"
|
||||
tools:targetApi="s">
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
tools:targetApi="tiramisu">
|
||||
<profileable
|
||||
android:shell="true"
|
||||
tools:targetApi="29" />
|
||||
@@ -148,6 +149,19 @@
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
<meta-data
|
||||
android:name="com.deliveryhero.whetstone.worker.WhetstoneWorkerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
<!-- https://issuetracker.google.com/issues/258619948 -->
|
||||
<receiver
|
||||
android:name="androidx.profileinstaller.ProfileInstallReceiver"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
Generated
+19135
-4848
File diff suppressed because it is too large
Load Diff
@@ -1,48 +1,57 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.StrictMode
|
||||
import android.util.Log
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.work.Configuration
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.disk.DiskCache
|
||||
import coil.memory.MemoryCache
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import io.github.aakira.napier.DebugAntilog
|
||||
import io.github.aakira.napier.Napier
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.app.ApplicationComponentOwner
|
||||
import com.deliveryhero.whetstone.app.ContributesAppInjector
|
||||
import dev.msfjarvis.claw.android.work.SavedPostUpdaterWorker
|
||||
import dev.msfjarvis.claw.core.injection.AppPlugin
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class ClawApplication : Application(), Configuration.Provider, ImageLoaderFactory {
|
||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||
@ContributesAppInjector(generateAppComponent = true)
|
||||
class ClawApplication : Application(), ApplicationComponentOwner {
|
||||
|
||||
override val applicationComponent by
|
||||
lazy(LazyThreadSafetyMode.NONE) { GeneratedApplicationComponent.create(this) }
|
||||
|
||||
@Inject lateinit var plugins: Set<@JvmSuppressWildcards AppPlugin>
|
||||
|
||||
override fun onCreate() {
|
||||
Whetstone.inject(this)
|
||||
super.onCreate()
|
||||
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build())
|
||||
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build())
|
||||
Napier.base(DebugAntilog())
|
||||
plugins.forEach { plugin -> plugin.apply(this) }
|
||||
val postUpdateWorkRequest =
|
||||
PeriodicWorkRequestBuilder<SavedPostUpdaterWorker>(POST_REFRESH_PERIOD, TimeUnit.HOURS)
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
.setRequiresCharging(false)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
WorkManager.getInstance(this)
|
||||
.enqueueUniquePeriodicWork(
|
||||
"updateSavedPosts",
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
postUpdateWorkRequest,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getWorkManagerConfiguration(): Configuration {
|
||||
return Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this)
|
||||
.memoryCache { MemoryCache.Builder(this).maxSizePercent(0.25).build() }
|
||||
.diskCache {
|
||||
DiskCache.Builder()
|
||||
.directory(cacheDir.resolve("image_cache"))
|
||||
.maxSizeBytes(25L * 1024 * 1024) // 25 MB
|
||||
.build()
|
||||
}
|
||||
// Show a short crossfade when loading images asynchronously.
|
||||
.crossfade(true)
|
||||
.build()
|
||||
private companion object {
|
||||
private const val POST_REFRESH_PERIOD = 24L // 24 hours
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.assist.AssistContent
|
||||
@@ -6,20 +12,14 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
|
||||
import dev.msfjarvis.claw.android.ui.LobstersApp
|
||||
import dev.msfjarvis.claw.android.work.SavedPostUpdaterWorker
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@ContributesActivityInjector
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var urlLauncher: UrlLauncher
|
||||
@@ -29,26 +29,14 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
installSplashScreen()
|
||||
Whetstone.inject(this)
|
||||
setContent {
|
||||
LobstersApp(urlLauncher = urlLauncher, htmlConverter = htmlConverter) { url -> webUri = url }
|
||||
}
|
||||
val postUpdateWorkRequest =
|
||||
PeriodicWorkRequestBuilder<SavedPostUpdaterWorker>(24, TimeUnit.HOURS)
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
.setRequiresCharging(false)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
WorkManager.getInstance(this@MainActivity)
|
||||
.enqueueUniquePeriodicWork(
|
||||
"updateSavedPosts",
|
||||
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
|
||||
postUpdateWorkRequest,
|
||||
LobstersApp(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
setWebUri = { url -> webUri = url },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent?) {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ApiModule {
|
||||
@Provides
|
||||
fun provideRetrofit(
|
||||
client: OkHttpClient,
|
||||
json: Json,
|
||||
): Retrofit {
|
||||
val contentType = "application/json".toMediaType()
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
.baseUrl(LobstersApi.BASE_URL)
|
||||
.addConverterFactory(json.asConverterFactory(contentType))
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideApi(retrofit: Retrofit): LobstersApi {
|
||||
return retrofit.create()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideJsonSerializer(): Json {
|
||||
return Json { ignoreUnknownKeys = true }
|
||||
}
|
||||
}
|
||||
+11
-12
@@ -1,20 +1,25 @@
|
||||
/*
|
||||
* Copyright © 2022 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 com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.msfjarvis.claw.util.coroutines.DefaultDispatcherProvider
|
||||
import dev.msfjarvis.claw.util.coroutines.DispatcherProvider
|
||||
import javax.inject.Qualifier
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class CoroutineDispatcherModule {
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
interface CoroutineDispatcherModule {
|
||||
|
||||
@Binds abstract fun DefaultDispatcherProvider.bind(): DispatcherProvider
|
||||
@Binds fun DefaultDispatcherProvider.bind(): DispatcherProvider
|
||||
|
||||
companion object {
|
||||
@[Provides IODispatcher]
|
||||
@@ -33,9 +38,3 @@ abstract class CoroutineDispatcherModule {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class DatabaseDispatcher
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class MainDispatcher
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class IODispatcher
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.msfjarvis.claw.database.LobstersDatabase
|
||||
import dev.msfjarvis.claw.database.local.createDatabase
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
@Provides
|
||||
fun provideDatabase(@ApplicationContext context: Context): LobstersDatabase {
|
||||
return createDatabase(context)
|
||||
}
|
||||
}
|
||||
+13
-16
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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 com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.msfjarvis.claw.android.ui.util.HTMLConverterImpl
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import io.github.furstenheim.CopyDown
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object HTMLConverterModule {
|
||||
|
||||
@Provides
|
||||
fun provideHTMLConverter() =
|
||||
object : HTMLConverter {
|
||||
private val copydown = CopyDown()
|
||||
|
||||
override fun convertHTMLToMarkdown(html: String): String {
|
||||
return copydown.convert(html)
|
||||
}
|
||||
}
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
interface HTMLConverterModule {
|
||||
@Binds fun HTMLConverterImpl.bind(): HTMLConverter
|
||||
}
|
||||
|
||||
+9
-3
@@ -1,14 +1,20 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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 com.chimbori.crux.Crux
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
object MetadataExtractorModule {
|
||||
@Provides
|
||||
fun provideCrux(
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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 com.deliveryhero.whetstone.ForScope
|
||||
import com.deliveryhero.whetstone.activity.ActivityScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.qualifiers.ActivityContext
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
@ContributesTo(ActivityScope::class)
|
||||
object UrlLauncherModule {
|
||||
@Provides
|
||||
fun provideUrlLauncher(@ActivityContext context: Context): UrlLauncher {
|
||||
fun provideUrlLauncher(@ForScope(ActivityScope::class) context: Context): UrlLauncher {
|
||||
return UrlLauncher(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright © 2022 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 com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesMultibinding
|
||||
import dev.msfjarvis.claw.core.injection.AppPlugin
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesMultibinding(ApplicationScope::class)
|
||||
class WorkManagerPlugin
|
||||
@Inject
|
||||
constructor(
|
||||
private val workerFactory: WorkerFactory,
|
||||
) : AppPlugin {
|
||||
override fun apply(application: Application) {
|
||||
WorkManager.initialize(
|
||||
application,
|
||||
Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright © 2022 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 javax.inject.Qualifier
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class DatabaseDispatcher
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class MainDispatcher
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class IODispatcher
|
||||
@@ -1,19 +0,0 @@
|
||||
package dev.msfjarvis.claw.android.network
|
||||
|
||||
import dev.msfjarvis.claw.android.BuildConfig
|
||||
import javax.inject.Inject
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
/** An OkHttp [Interceptor] that adds a recognizable User-Agent header to all network requests. */
|
||||
class UserAgentInterceptor @Inject constructor() : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
return chain.proceed(
|
||||
chain
|
||||
.request()
|
||||
.newBuilder()
|
||||
.header("User-Agent", "Claw-Android/${BuildConfig.VERSION_NAME}/msfjarvis")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,21 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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 dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dev.msfjarvis.claw.android.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -18,26 +27,35 @@ constructor(
|
||||
) : PagingSource<Int, LobstersPost>() {
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
|
||||
return try {
|
||||
val page = params.key ?: 1
|
||||
val posts = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }
|
||||
|
||||
LoadResult.Page(
|
||||
data = posts,
|
||||
prevKey = if (page == 1) null else page - 1,
|
||||
nextKey = page.plus(1)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
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,
|
||||
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,
|
||||
is Failure.ApiFailure -> LoadResult.Error(IOException("API returned an invalid response"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int {
|
||||
return state.pages.size + 1
|
||||
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): 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,6 +1,14 @@
|
||||
/*
|
||||
* Copyright © 2022 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): List<T>
|
||||
suspend fun getItemsAtPage(page: Int): ApiResult<List<T>, Unit>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 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 androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -15,18 +20,24 @@ import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
||||
@@ -38,7 +49,6 @@ import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.comments.LocalHTMLConverter
|
||||
import dev.msfjarvis.claw.common.res.ClawIcons
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.decorations.ClawAppBar
|
||||
@@ -46,23 +56,24 @@ import dev.msfjarvis.claw.common.ui.surfaceColorAtNavigationBarElevation
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import dev.msfjarvis.claw.common.user.UserProfile
|
||||
import kotlinx.coroutines.launch
|
||||
import soup.compose.material.motion.navigation.MaterialMotionNavHost
|
||||
import soup.compose.material.motion.navigation.composable
|
||||
import soup.compose.material.motion.navigation.rememberMaterialMotionNavController
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
||||
@OptIn(
|
||||
ExperimentalComposeUiApi::class,
|
||||
ExperimentalMaterial3Api::class,
|
||||
)
|
||||
@Composable
|
||||
fun LobstersApp(
|
||||
viewModel: ClawViewModel = viewModel(),
|
||||
urlLauncher: UrlLauncher,
|
||||
htmlConverter: HTMLConverter,
|
||||
setWebUri: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
) {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val hottestListState = rememberLazyListState()
|
||||
val newestListState = rememberLazyListState()
|
||||
val savedListState = rememberLazyListState()
|
||||
val navController = rememberMaterialMotionNavController()
|
||||
val navController = rememberNavController()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val postActions = rememberPostActions(urlLauncher, navController, viewModel)
|
||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||
@@ -75,11 +86,7 @@ fun LobstersApp(
|
||||
|
||||
LobstersTheme(
|
||||
dynamicColor = true,
|
||||
providedValues =
|
||||
arrayOf(
|
||||
LocalUriHandler provides urlLauncher,
|
||||
LocalHTMLConverter provides htmlConverter,
|
||||
),
|
||||
providedValues = arrayOf(LocalUriHandler provides urlLauncher),
|
||||
) {
|
||||
val currentUiMode = LocalConfiguration.current.uiMode
|
||||
val systemBarsColor = MaterialTheme.colorScheme.surfaceColorAtNavigationBarElevation()
|
||||
@@ -88,7 +95,7 @@ fun LobstersApp(
|
||||
listOf(
|
||||
NavigationItem(
|
||||
label = "Hottest",
|
||||
route = Destinations.Hottest.getRoute(),
|
||||
route = Destinations.Hottest.route,
|
||||
icon = ClawIcons.Flame,
|
||||
selectedIcon = ClawIcons.FlameFilled,
|
||||
) {
|
||||
@@ -96,7 +103,7 @@ fun LobstersApp(
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Newest",
|
||||
route = Destinations.Newest.getRoute(),
|
||||
route = Destinations.Newest.route,
|
||||
icon = ClawIcons.New,
|
||||
selectedIcon = ClawIcons.NewFilled,
|
||||
) {
|
||||
@@ -104,7 +111,7 @@ fun LobstersApp(
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Saved",
|
||||
route = Destinations.Saved.getRoute(),
|
||||
route = Destinations.Saved.route,
|
||||
icon = ClawIcons.HeartBorder,
|
||||
selectedIcon = ClawIcons.Heart,
|
||||
) {
|
||||
@@ -155,40 +162,39 @@ fun LobstersApp(
|
||||
isVisible = navItems.any { it.route == currentDestination },
|
||||
)
|
||||
},
|
||||
modifier = modifier.semantics { testTagsAsResourceId = true },
|
||||
) { paddingValues ->
|
||||
MaterialMotionNavHost(
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Destinations.startDestination.getRoute(),
|
||||
startDestination = Destinations.startDestination.route,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
) {
|
||||
val uri = LobstersApi.BASE_URL
|
||||
composable(
|
||||
route = Destinations.Hottest.getRoute(),
|
||||
route = Destinations.Hottest.route,
|
||||
deepLinks =
|
||||
listOf(navDeepLink { uriPattern = uri }, navDeepLink { uriPattern = "$uri/" }),
|
||||
) {
|
||||
setWebUri("https://lobste.rs/")
|
||||
NetworkPosts(
|
||||
items = hottestPosts,
|
||||
lazyPagingItems = hottestPosts,
|
||||
listState = hottestListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
reloadPosts = viewModel::refreshHottestPosts,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Destinations.Newest.getRoute(),
|
||||
route = Destinations.Newest.route,
|
||||
) {
|
||||
setWebUri("https://lobste.rs/")
|
||||
NetworkPosts(
|
||||
items = newestPosts,
|
||||
lazyPagingItems = newestPosts,
|
||||
listState = newestListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
reloadPosts = viewModel::refreshNewestPosts,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
composable(Destinations.Saved.getRoute()) {
|
||||
composable(Destinations.Saved.route) {
|
||||
setWebUri(null)
|
||||
DatabasePosts(
|
||||
items = savedPosts,
|
||||
@@ -197,26 +203,29 @@ fun LobstersApp(
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Destinations.Comments.getRoute("{postId}"),
|
||||
route = Destinations.Comments.route,
|
||||
arguments = listOf(navArgument("postId") { type = NavType.StringType }),
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink { uriPattern = "$uri/s/{postId}/.*" },
|
||||
navDeepLink { uriPattern = "$uri/s/{postId}" },
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.placeholder}/.*" },
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.placeholder}" },
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val postId = requireNotNull(backStackEntry.arguments?.getString("postId"))
|
||||
setWebUri("https://lobste.rs/s/$postId")
|
||||
CommentsPage(
|
||||
postId = postId,
|
||||
getDetails = viewModel::getPostComments,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Destinations.User.getRoute("{username}"),
|
||||
route = Destinations.User.route,
|
||||
arguments = listOf(navArgument("username") { type = NavType.StringType }),
|
||||
deepLinks = listOf(navDeepLink { uriPattern = "$uri/u/{username}" }),
|
||||
deepLinks =
|
||||
listOf(navDeepLink { uriPattern = "$uri/u/${Destinations.User.placeholder}" }),
|
||||
) { backStackEntry ->
|
||||
val username = requireNotNull(backStackEntry.arguments?.getString("username"))
|
||||
UserProfile(
|
||||
|
||||
+13
-3
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2022 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
|
||||
@@ -14,6 +20,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.navigation.NavController
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
|
||||
@@ -61,11 +68,14 @@ fun ClawNavigationBar(
|
||||
navItem.listStateResetCallback()
|
||||
return@NavigationBarItem
|
||||
}
|
||||
navController.popBackStack(navController.graph.startDestinationRoute!!, false)
|
||||
if (navItem.route != Destinations.startDestination.getRoute()) {
|
||||
navController.graph.startDestinationRoute?.let { startDestination ->
|
||||
navController.popBackStack(startDestination, false)
|
||||
}
|
||||
if (navItem.route != Destinations.startDestination.route) {
|
||||
navController.navigate(navItem.route)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.testTag(navItem.label.uppercase()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2022 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
|
||||
@@ -11,6 +17,8 @@ import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@@ -43,7 +51,9 @@ fun rememberPostActions(
|
||||
}
|
||||
|
||||
override fun viewComments(postId: String) {
|
||||
navController.navigate(Destinations.Comments.getRoute(postId))
|
||||
navController.navigate(
|
||||
Destinations.Comments.route.replace(Destinations.Comments.placeholder, postId)
|
||||
)
|
||||
}
|
||||
|
||||
override fun viewCommentsPage(commentsUrl: String) {
|
||||
@@ -59,6 +69,14 @@ fun rememberPostActions(
|
||||
override fun toggleSave(post: SavedPost) {
|
||||
viewModel.toggleSave(post)
|
||||
}
|
||||
|
||||
override suspend fun getComments(postId: String): LobstersPostDetails {
|
||||
return viewModel.getPostComments(postId)
|
||||
}
|
||||
|
||||
override suspend fun getLinkMetadata(url: String): LinkMetadata {
|
||||
return viewModel.getLinkMetadata(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.ExperimentalFoundationApi
|
||||
@@ -26,7 +32,7 @@ fun DatabasePosts(
|
||||
) {
|
||||
items.forEach { (month, posts) ->
|
||||
stickyHeader { MonthHeader(month = month) }
|
||||
items(posts) { item ->
|
||||
items(posts, { it.shortId }) { item ->
|
||||
ListItem(
|
||||
item = item,
|
||||
isSaved = { true },
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.runtime.Composable
|
||||
|
||||
@@ -1,67 +1,63 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.Box
|
||||
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.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.SwipeRefreshIndicator
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.posts.toDbModel
|
||||
import dev.msfjarvis.claw.common.ui.NetworkError
|
||||
import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun NetworkPosts(
|
||||
items: LazyPagingItems<LobstersPost>,
|
||||
lazyPagingItems: LazyPagingItems<LobstersPost>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: suspend (SavedPost) -> Boolean,
|
||||
reloadPosts: () -> Unit,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val loadState = items.loadState.refresh
|
||||
val isRefreshing = loadState == LoadState.Loading
|
||||
SwipeRefresh(
|
||||
state = rememberSwipeRefreshState(isRefreshing),
|
||||
onRefresh = reloadPosts,
|
||||
indicator = { state, trigger ->
|
||||
val backgroundColor = MaterialTheme.colorScheme.surface
|
||||
SwipeRefreshIndicator(
|
||||
state = state,
|
||||
refreshTriggerDistance = trigger,
|
||||
backgroundColor = backgroundColor,
|
||||
contentColor = contentColorFor(backgroundColor)
|
||||
val refreshLoadState = lazyPagingItems.loadState.refresh
|
||||
val isRefreshing = refreshLoadState == LoadState.Loading
|
||||
val pullRefreshState = rememberPullRefreshState(isRefreshing, lazyPagingItems::refresh)
|
||||
Box(modifier = modifier.fillMaxSize().pullRefresh(pullRefreshState)) {
|
||||
if (lazyPagingItems.itemCount == 0 && refreshLoadState is LoadState.Error) {
|
||||
NetworkError(
|
||||
label = "Failed to load posts",
|
||||
error = refreshLoadState.error,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
) {
|
||||
if (items.itemCount == 0) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (loadState is LoadState.Error) {
|
||||
NetworkError(
|
||||
label = "Failed to load posts",
|
||||
error = loadState.error,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(items) { item ->
|
||||
items(items = lazyPagingItems, key = { it.shortId }) { item ->
|
||||
if (item != null) {
|
||||
val dbModel = item.toDbModel()
|
||||
ListItem(
|
||||
@@ -73,7 +69,24 @@ fun NetworkPosts(
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
if (lazyPagingItems.loadState.append == LoadState.Loading) {
|
||||
item(key = "progressbar") {
|
||||
ProgressBar(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
.padding(vertical = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(
|
||||
refreshing = isRefreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
backgroundColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = contentColorFor(MaterialTheme.colorScheme.surface),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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
|
||||
|
||||
sealed class Destinations {
|
||||
abstract val route: String
|
||||
|
||||
object Hottest : Destinations() {
|
||||
fun getRoute() = "hottest"
|
||||
override val route = "hottest"
|
||||
}
|
||||
|
||||
object Newest : Destinations() {
|
||||
fun getRoute() = "newest"
|
||||
override val route = "newest"
|
||||
}
|
||||
|
||||
object Saved : Destinations() {
|
||||
fun getRoute() = "saved"
|
||||
override val route = "saved"
|
||||
}
|
||||
|
||||
object Comments : Destinations() {
|
||||
fun getRoute(postId: String) = "comments/$postId"
|
||||
const val placeholder = "{postId}"
|
||||
override val route = "comments/$placeholder"
|
||||
}
|
||||
|
||||
object User : Destinations() {
|
||||
fun getRoute(username: String) = "user/$username"
|
||||
const val placeholder = "{username}"
|
||||
override val route = "user/$placeholder"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright © 2022 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.util
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import io.github.furstenheim.CopyDown
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
class HTMLConverterImpl @Inject constructor() : HTMLConverter {
|
||||
private val copydown = CopyDown()
|
||||
|
||||
override fun convertHTMLToMarkdown(html: String): String {
|
||||
return copydown.convert(html)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,28 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 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 androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
|
||||
import com.slack.eithernet.ApiResult.Failure
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import dev.msfjarvis.claw.android.injection.IODispatcher
|
||||
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.ui.toLocalDateTime
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.time.Month
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -18,25 +31,23 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@HiltViewModel
|
||||
@ContributesViewModel
|
||||
class ClawViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val api: LobstersApi,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val postDetailsRepository: PostDetailsRepository,
|
||||
private val linkMetadataRepository: LinkMetadataRepository,
|
||||
private val pagingSourceFactory: LobstersPagingSource.Factory,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
private var hottestPostsPagingSource: LobstersPagingSource? = null
|
||||
private var newestPostsPagingSource: LobstersPagingSource? = null
|
||||
private val hottestPostsPager =
|
||||
Pager(PagingConfig(20)) {
|
||||
pagingSourceFactory.create(api::getHottestPosts).also { hottestPostsPagingSource = it }
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
pagingSourceFactory.create(api::getHottestPosts)
|
||||
}
|
||||
private val newestPostsPager =
|
||||
Pager(PagingConfig(20)) {
|
||||
pagingSourceFactory.create(api::getHottestPosts).also { newestPostsPagingSource = it }
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
pagingSourceFactory.create(api::getNewestPosts)
|
||||
}
|
||||
|
||||
val hottestPosts
|
||||
@@ -73,18 +84,38 @@ constructor(
|
||||
|
||||
suspend fun getPostComments(postId: String) =
|
||||
withContext(ioDispatcher) {
|
||||
val details = api.getPostDetails(postId)
|
||||
val extendedDetails = postDetailsRepository.getExtendedDetails(details)
|
||||
extendedDetails
|
||||
when (val result = api.getPostDetails(postId)) {
|
||||
is Success -> result.value
|
||||
is Failure.NetworkFailure -> throw result.error
|
||||
is Failure.UnknownFailure -> throw result.error
|
||||
is Failure.HttpFailure -> {
|
||||
if (result.code == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||
throw IOException("Story was removed by moderator")
|
||||
} else {
|
||||
throw IOException("API returned an invalid response")
|
||||
}
|
||||
}
|
||||
is Failure.ApiFailure -> throw IOException("API returned an invalid response")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUserProfile(username: String) = withContext(ioDispatcher) { api.getUser(username) }
|
||||
suspend fun getSeenComments(postId: String) = savedPostsRepository.getSeenComments(postId)
|
||||
|
||||
fun refreshHottestPosts() {
|
||||
hottestPostsPagingSource?.invalidate()
|
||||
fun markSeenComments(postId: String, comments: List<Comment>) {
|
||||
viewModelScope.launch { savedPostsRepository.markSeenComments(postId, comments) }
|
||||
}
|
||||
|
||||
fun refreshNewestPosts() {
|
||||
newestPostsPagingSource?.invalidate()
|
||||
}
|
||||
suspend fun getLinkMetadata(url: String) =
|
||||
withContext(ioDispatcher) { linkMetadataRepository.getLinkMetadata(url) }
|
||||
|
||||
suspend fun getUserProfile(username: String) =
|
||||
withContext(ioDispatcher) {
|
||||
when (val result = api.getUser(username)) {
|
||||
is Success -> result.value
|
||||
is Failure.NetworkFailure -> throw result.error
|
||||
is Failure.UnknownFailure -> throw result.error
|
||||
is Failure.HttpFailure,
|
||||
is Failure.ApiFailure -> throw IOException("API returned an invalid response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright © 2022 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.metadata.MetadataExtractor
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import javax.inject.Inject
|
||||
|
||||
class LinkMetadataRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val metadataExtractor: MetadataExtractor,
|
||||
) {
|
||||
suspend fun getLinkMetadata(url: String): LinkMetadata {
|
||||
return metadataExtractor.getExtractedMetadata(url)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.metadata.MetadataExtractor
|
||||
import dev.msfjarvis.claw.model.ExtendedPostDetails
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import javax.inject.Inject
|
||||
|
||||
class PostDetailsRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val metadataExtractor: MetadataExtractor,
|
||||
) {
|
||||
suspend fun getExtendedDetails(details: LobstersPostDetails): ExtendedPostDetails {
|
||||
val metadata = metadataExtractor.getExtractedMetadata(details.url)
|
||||
return ExtendedPostDetails(
|
||||
title = details.title,
|
||||
linkMetadata = metadata,
|
||||
description = details.description,
|
||||
submitter = details.submitter,
|
||||
tags = details.tags,
|
||||
comments = details.comments,
|
||||
commentsUrl = details.commentsUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 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.android.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.database.LobstersDatabase
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import io.github.aakira.napier.Napier
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -17,6 +25,7 @@ constructor(
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val savedPostQueries = database.savedPostQueries
|
||||
private val postCommentsQueries = database.postCommentsQueries
|
||||
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
|
||||
|
||||
suspend fun savePost(post: SavedPost) {
|
||||
@@ -24,11 +33,29 @@ constructor(
|
||||
withContext(dbDispatcher) { savedPostQueries.insertOrReplacePost(post) }
|
||||
}
|
||||
|
||||
suspend fun savePosts(posts: List<SavedPost>) {
|
||||
Napier.d(tag = TAG) { "Saving posts: ${posts.joinToString(",") { it.shortId }}" }
|
||||
withContext(dbDispatcher) {
|
||||
savedPostQueries.transaction {
|
||||
posts.forEach { post -> savedPostQueries.insertOrReplacePost(post) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removePost(post: SavedPost) {
|
||||
Napier.d(tag = TAG) { "Removing post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
|
||||
}
|
||||
|
||||
suspend fun getSeenComments(postId: String) =
|
||||
withContext(dbDispatcher) { postCommentsQueries.getCommentIds(postId).executeAsOneOrNull() }
|
||||
|
||||
suspend fun markSeenComments(postId: String, comments: List<Comment>) {
|
||||
withContext(dbDispatcher) {
|
||||
postCommentsQueries.rememberComments(PostComments(postId, comments.map { it.shortId }))
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "SavedPostsRepository"
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
/*
|
||||
* Copyright © 2022 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.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import com.deliveryhero.whetstone.ForScope
|
||||
import com.deliveryhero.whetstone.worker.ContributesWorker
|
||||
import com.deliveryhero.whetstone.worker.WorkerScope
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.common.posts.toDbModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/**
|
||||
@@ -22,29 +26,23 @@ import kotlinx.coroutines.flow.first
|
||||
* saved posts that were saved before comment counts were added to be able to show a comment count
|
||||
* and for new-enough posts that are still getting comments to have an accurate one.
|
||||
*/
|
||||
@Suppress("DEPRECATION") // We're being nasty
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@HiltWorker
|
||||
@ContributesWorker
|
||||
class SavedPostUpdaterWorker
|
||||
@AssistedInject
|
||||
@Inject
|
||||
constructor(
|
||||
@ForScope(WorkerScope::class) appContext: Context,
|
||||
workerParams: WorkerParameters,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val lobstersApi: LobstersApi,
|
||||
@Assisted appContext: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
) : CoroutineWorker(appContext, workerParams) {
|
||||
override suspend fun doWork(): Result {
|
||||
val posts = savedPostsRepository.savedPosts.first()
|
||||
posts
|
||||
.map { post ->
|
||||
CoroutineScope(coroutineContext + Job()).async {
|
||||
val details = runCatching { lobstersApi.getPostDetails(post.shortId) }.getOrNull()
|
||||
if (details != null) {
|
||||
savedPostsRepository.savePost(details.toDbModel())
|
||||
}
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
savedPostsRepository.savedPosts
|
||||
.first()
|
||||
.map { post -> lobstersApi.getPostDetails(post.shortId) }
|
||||
.filterIsInstance<Success<LobstersPostDetails>>()
|
||||
.map { result -> result.value.toDbModel() }
|
||||
.toList()
|
||||
.let { savedPostsRepository.savePosts(it) }
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
|
||||
#
|
||||
versioning-plugin.versionCode=10100
|
||||
versioning-plugin.versionName=1.1.0
|
||||
versioning-plugin.versionCode=12000
|
||||
versioning-plugin.versionName=1.20.0
|
||||
|
||||
+21
-7
@@ -1,17 +1,31 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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("DSL_SCOPE_VIOLATION", "UnstableApiUsage")
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
id("dev.msfjarvis.claw.kotlin-library")
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
id("dev.msfjarvis.claw.android-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.whetstone)
|
||||
}
|
||||
|
||||
android { namespace = "dev.msfjarvis.claw.api" }
|
||||
|
||||
anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
dependencies {
|
||||
api(projects.model)
|
||||
api(libs.retrofit.lib)
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
testImplementation(libs.kotlinx.coroutines.core)
|
||||
testImplementation(kotlin("test-junit"))
|
||||
api(libs.retrofit)
|
||||
api(libs.eithernet)
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.javax.inject)
|
||||
testImplementation(testFixtures(libs.eithernet))
|
||||
testImplementation(libs.kotest.assertions.core)
|
||||
testImplementation(libs.kotest.runner.junit5)
|
||||
testImplementation(libs.kotlinx.serialization.json)
|
||||
testImplementation(libs.retrofit.kotlinxSerializationConverter)
|
||||
testImplementation(libs.retrofit.mock)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.0.0-alpha08" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-alpha08)" variant="all" version="8.0.0-alpha08">
|
||||
|
||||
</issues>
|
||||
@@ -1,5 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.api
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.User
|
||||
@@ -10,14 +17,17 @@ import retrofit2.http.Query
|
||||
/** Simple interface defining an API for lobste.rs */
|
||||
interface LobstersApi {
|
||||
|
||||
@GET("hottest.json") suspend fun getHottestPosts(@Query("page") page: Int): List<LobstersPost>
|
||||
@GET("hottest.json")
|
||||
suspend fun getHottestPosts(@Query("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
|
||||
@GET("newest.json") suspend fun getNewestPosts(@Query("page") page: Int): List<LobstersPost>
|
||||
@GET("newest.json")
|
||||
suspend fun getNewestPosts(@Query("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
|
||||
@GET("s/{postId}.json")
|
||||
suspend fun getPostDetails(@Path("postId") postId: String): LobstersPostDetails
|
||||
suspend fun getPostDetails(@Path("postId") postId: String): ApiResult<LobstersPostDetails, Unit>
|
||||
|
||||
@GET("u/{username}.json") suspend fun getUser(@Path("username") username: String): User
|
||||
@GET("u/{username}.json")
|
||||
suspend fun getUser(@Path("username") username: String): ApiResult<User, Unit>
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://lobste.rs"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.api.injection
|
||||
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.slack.eithernet.ApiResultCallAdapterFactory
|
||||
import com.slack.eithernet.ApiResultConverterFactory
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
object ApiModule {
|
||||
@Provides
|
||||
fun provideRetrofit(
|
||||
client: OkHttpClient,
|
||||
converterFactories: Set<@JvmSuppressWildcards Converter.Factory>,
|
||||
): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
.baseUrl(LobstersApi.BASE_URL)
|
||||
.addConverterFactory(ApiResultConverterFactory)
|
||||
.addCallAdapterFactory(ApiResultCallAdapterFactory)
|
||||
.apply { converterFactories.forEach(this::addConverterFactory) }
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideApi(retrofit: Retrofit): LobstersApi {
|
||||
return retrofit.create()
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,50 @@
|
||||
/*
|
||||
* Copyright © 2022 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.api
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.mock.MockRetrofit
|
||||
import retrofit2.mock.NetworkBehavior
|
||||
import retrofit2.mock.create
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import com.slack.eithernet.test.newEitherNetController
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.collections.shouldHaveSize
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.types.shouldBeTypeOf
|
||||
|
||||
class ApiTest {
|
||||
private val retrofit = Retrofit.Builder().baseUrl(LobstersApi.BASE_URL).build()
|
||||
private val networkBehaviour = createNetworkBehaviour()
|
||||
private val api =
|
||||
MockRetrofit.Builder(retrofit)
|
||||
.networkBehavior(networkBehaviour)
|
||||
.build()
|
||||
.create<LobstersApi>()
|
||||
.let(::FakeApi)
|
||||
class ApiTest : FunSpec() {
|
||||
private val wrapper = ApiWrapper(newEitherNetController())
|
||||
private val api
|
||||
get() = wrapper.api
|
||||
|
||||
private fun createNetworkBehaviour(): NetworkBehavior {
|
||||
val behaviour = NetworkBehavior.create()
|
||||
behaviour.setFailurePercent(0)
|
||||
behaviour.setErrorPercent(0)
|
||||
return behaviour
|
||||
}
|
||||
init {
|
||||
test("api gets correct number of items") {
|
||||
val posts = api.getHottestPosts(1)
|
||||
posts.shouldBeTypeOf<Success<List<LobstersPost>>>()
|
||||
posts.value shouldHaveSize 25
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `api gets correct number of items`() = runBlocking {
|
||||
val posts = api.getHottestPosts(1)
|
||||
assertEquals(25, posts.size)
|
||||
}
|
||||
test("posts with no urls") {
|
||||
val posts = api.getHottestPosts(1)
|
||||
posts.shouldBeTypeOf<Success<List<LobstersPost>>>()
|
||||
val commentsOnlyPosts = posts.value.asSequence().filter { it.url.isEmpty() }.toSet()
|
||||
commentsOnlyPosts shouldHaveSize 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `posts with no urls`() = runBlocking {
|
||||
val posts = api.getHottestPosts(1)
|
||||
val commentsOnlyPosts = posts.asSequence().filter { it.url.isEmpty() }.toSet()
|
||||
assertEquals(2, commentsOnlyPosts.size)
|
||||
}
|
||||
test("post details with comments") {
|
||||
val postDetails = api.getPostDetails("tdfoqh")
|
||||
postDetails.shouldBeTypeOf<Success<LobstersPostDetails>>()
|
||||
postDetails.value.comments shouldHaveSize 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `post details with comments`() = runBlocking {
|
||||
val postDetails = api.getPostDetails("tdfoqh")
|
||||
assertEquals(7, postDetails.comments.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get user details`() = runBlocking {
|
||||
val user = api.getUser("msfjarvis")
|
||||
assertEquals("msfjarvis", user.username)
|
||||
test("get user details") {
|
||||
val user = api.getUser("msfjarvis")
|
||||
user.shouldBeTypeOf<Success<User>>()
|
||||
user.value.username shouldBe "msfjarvis"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright © 2022 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.api
|
||||
|
||||
import com.slack.eithernet.ApiResult.Companion.success
|
||||
import com.slack.eithernet.test.EitherNetController
|
||||
import com.slack.eithernet.test.enqueue
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import dev.msfjarvis.claw.util.TestUtils.getJson
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
class ApiWrapper(controller: EitherNetController<LobstersApi>) {
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||
}
|
||||
private val hottest: List<LobstersPost> = json.decodeFromString(getJson("hottest.json"))
|
||||
private val postDetails: LobstersPostDetails =
|
||||
json.decodeFromString(getJson("post_details_tdfoqh.json"))
|
||||
private val user: User = json.decodeFromString(getJson("msfjarvis.json"))
|
||||
|
||||
val api = controller.api
|
||||
|
||||
init {
|
||||
controller.enqueue(LobstersApi::getHottestPosts) { success(hottest) }
|
||||
controller.enqueue(LobstersApi::getHottestPosts) { success(hottest) }
|
||||
controller.enqueue(LobstersApi::getPostDetails) { success(postDetails) }
|
||||
controller.enqueue(LobstersApi::getUser) { success(user) }
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package dev.msfjarvis.claw.api
|
||||
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import dev.msfjarvis.claw.util.TestUtils.getJson
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import retrofit2.mock.BehaviorDelegate
|
||||
|
||||
class FakeApi(private val delegate: BehaviorDelegate<LobstersApi>) : LobstersApi {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
private val hottest: List<LobstersPost> = json.decodeFromString(getJson("hottest.json"))
|
||||
private val postDetails: LobstersPostDetails =
|
||||
json.decodeFromString(getJson("post_details_tdfoqh.json"))
|
||||
private val user: User = json.decodeFromString(getJson("msfjarvis.json"))
|
||||
|
||||
override suspend fun getHottestPosts(page: Int): List<LobstersPost> {
|
||||
return delegate.returningResponse(hottest).getHottestPosts(page)
|
||||
}
|
||||
|
||||
override suspend fun getNewestPosts(page: Int): List<LobstersPost> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getPostDetails(postId: String): LobstersPostDetails {
|
||||
return delegate.returningResponse(postDetails).getPostDetails(postId)
|
||||
}
|
||||
|
||||
override suspend fun getUser(username: String): User {
|
||||
return delegate.returningResponse(user).getUser(username)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.util
|
||||
|
||||
import java.io.File
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2022 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("DSL_SCOPE_VIOLATION", "UnstableApiUsage")
|
||||
|
||||
plugins {
|
||||
@@ -14,8 +20,8 @@ android {
|
||||
buildTypes {
|
||||
create("benchmark") {
|
||||
isDebuggable = true
|
||||
signingConfig = getByName("debug").signingConfig
|
||||
matchingFallbacks += listOf("release")
|
||||
signingConfig = signingConfigs["debug"]
|
||||
matchingFallbacks += "release"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2022 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.benchmark
|
||||
|
||||
import androidx.benchmark.macro.BaselineProfileMode
|
||||
@@ -5,14 +11,12 @@ import androidx.benchmark.macro.CompilationMode
|
||||
import androidx.benchmark.macro.StartupMode
|
||||
import androidx.benchmark.macro.StartupTimingMetric
|
||||
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@LargeTest
|
||||
class BaselineProfileBenchmark {
|
||||
@get:Rule val benchmarkRule = MacrobenchmarkRule()
|
||||
private lateinit var device: UiDevice
|
||||
@@ -46,7 +50,7 @@ class BaselineProfileBenchmark {
|
||||
startupMode = StartupMode.COLD,
|
||||
compilationMode = compilationMode
|
||||
) {
|
||||
exploreUI(device)
|
||||
startActivityAndWait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
/*
|
||||
* Copyright © 2022 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.benchmark
|
||||
|
||||
import androidx.benchmark.macro.ExperimentalBaselineProfilesApi
|
||||
import androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi
|
||||
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||
import androidx.test.filters.MediumTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@ExperimentalBaselineProfilesApi
|
||||
@MediumTest
|
||||
@OptIn(ExperimentalStableBaselineProfilesApi::class)
|
||||
class BaselineProfileGenerator {
|
||||
@get:Rule val baselineProfileRule = BaselineProfileRule()
|
||||
private lateinit var device: UiDevice
|
||||
@@ -23,5 +27,10 @@ class BaselineProfileGenerator {
|
||||
|
||||
@Test
|
||||
fun startup() =
|
||||
baselineProfileRule.collectBaselineProfile(packageName = PACKAGE_NAME) { exploreUI(device) }
|
||||
baselineProfileRule.collectStableBaselineProfile(
|
||||
packageName = PACKAGE_NAME,
|
||||
maxIterations = 15,
|
||||
) {
|
||||
startActivityAndWait()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,60 @@
|
||||
/*
|
||||
* Copyright © 2022 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.benchmark
|
||||
|
||||
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import androidx.test.uiautomator.Until
|
||||
|
||||
const val PACKAGE_NAME = "dev.msfjarvis.claw.android"
|
||||
private const val AWAIT_TIMEOUT = 10_000L
|
||||
private const val SAVE_BUTTON_DESC = "Add to saved posts"
|
||||
private const val NAV_ID_HOTTEST = "HOTTEST"
|
||||
private const val NAV_ID_NEWEST = "NEWEST"
|
||||
private const val NAV_ID_SAVED = "SAVED"
|
||||
private const val COMMENT_BUTTON_DESC = "Open comments"
|
||||
|
||||
@Suppress("Unused")
|
||||
fun MacrobenchmarkScope.exploreUI(device: UiDevice) {
|
||||
startActivityAndWait()
|
||||
device.run {
|
||||
listOf("HOTTEST", "NEWEST", "SAVED").forEach { desc ->
|
||||
findObject(By.desc(desc)).click()
|
||||
waitForIdle()
|
||||
}
|
||||
findObject(By.desc("HOTTEST")).click()
|
||||
waitForIdle()
|
||||
findObjects(By.desc("Open comments")).first().click()
|
||||
waitForIdle()
|
||||
savePosts()
|
||||
|
||||
exploreScreens()
|
||||
|
||||
returnToHottestScreen()
|
||||
|
||||
openCommentsScreen()
|
||||
}
|
||||
}
|
||||
|
||||
private fun UiDevice.waitForSubmitterName() {
|
||||
wait(Until.hasObject(By.textContains("Submitted by")), AWAIT_TIMEOUT)
|
||||
}
|
||||
|
||||
private fun UiDevice.savePosts() {
|
||||
waitForSubmitterName()
|
||||
findObjects(By.desc(SAVE_BUTTON_DESC)).forEach { btn -> btn.click() }
|
||||
}
|
||||
|
||||
private fun UiDevice.exploreScreens() {
|
||||
listOf(NAV_ID_HOTTEST, NAV_ID_NEWEST, NAV_ID_SAVED).forEach { tag ->
|
||||
findObject(By.res(tag)).click()
|
||||
waitForSubmitterName()
|
||||
}
|
||||
}
|
||||
|
||||
private fun UiDevice.returnToHottestScreen() {
|
||||
findObject(By.res(NAV_ID_HOTTEST)).click()
|
||||
waitForSubmitterName()
|
||||
}
|
||||
|
||||
private fun UiDevice.openCommentsScreen() {
|
||||
waitForSubmitterName()
|
||||
findObjects(By.desc(COMMENT_BUTTON_DESC)).first().click()
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins { `kotlin-dsl` }
|
||||
|
||||
afterEvaluate {
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
sourceCompatibility = JavaVersion.VERSION_11.toString()
|
||||
targetCompatibility = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
freeCompilerArgs = freeCompilerArgs + "-Xsam-conversions=class"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
register("android-application") {
|
||||
id = "dev.msfjarvis.claw.android-application"
|
||||
implementationClass = "dev.msfjarvis.aps.gradle.ApplicationPlugin"
|
||||
}
|
||||
register("android-common") {
|
||||
id = "dev.msfjarvis.claw.android-common"
|
||||
implementationClass = "dev.msfjarvis.aps.gradle.AndroidCommonPlugin"
|
||||
}
|
||||
register("android-library") {
|
||||
id = "dev.msfjarvis.claw.android-library"
|
||||
implementationClass = "dev.msfjarvis.aps.gradle.LibraryPlugin"
|
||||
}
|
||||
register("rename-artifacts") {
|
||||
id = "dev.msfjarvis.claw.rename-artifacts"
|
||||
implementationClass = "dev.msfjarvis.aps.gradle.RenameArtifactsPlugin"
|
||||
}
|
||||
register("versioning") {
|
||||
id = "dev.msfjarvis.claw.versioning-plugin"
|
||||
implementationClass = "dev.msfjarvis.aps.gradle.versioning.VersioningPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.build.agp)
|
||||
implementation(libs.build.cachefix)
|
||||
implementation(libs.build.semver)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright © 2022 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.
|
||||
*/
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins { `kotlin-dsl` }
|
||||
|
||||
afterEvaluate {
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
sourceCompatibility = JavaVersion.VERSION_11.toString()
|
||||
targetCompatibility = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
freeCompilerArgs = freeCompilerArgs + "-Xsam-conversions=class"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
register("android-application") {
|
||||
id = "dev.msfjarvis.claw.android-application"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.ApplicationPlugin"
|
||||
}
|
||||
register("android-common") {
|
||||
id = "dev.msfjarvis.claw.android-common"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.AndroidCommonPlugin"
|
||||
}
|
||||
register("android-library") {
|
||||
id = "dev.msfjarvis.claw.android-library"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.LibraryPlugin"
|
||||
}
|
||||
register("kotlin-android") {
|
||||
id = "dev.msfjarvis.claw.kotlin-android"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.KotlinAndroidPlugin"
|
||||
}
|
||||
register("kotlin-common") {
|
||||
id = "dev.msfjarvis.claw.kotlin-common"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.KotlinCommonPlugin"
|
||||
}
|
||||
register("kotlin-jvm") {
|
||||
id = "dev.msfjarvis.claw.kotlin-jvm"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.KotlinJvmPlugin"
|
||||
}
|
||||
register("kotlin-kapt") {
|
||||
id = "dev.msfjarvis.claw.kotlin-kapt"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.KotlinKaptPlugin"
|
||||
}
|
||||
register("rename-artifacts") {
|
||||
id = "dev.msfjarvis.claw.rename-artifacts"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.RenameArtifactsPlugin"
|
||||
}
|
||||
register("spotless") {
|
||||
id = "dev.msfjarvis.claw.spotless"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.SpotlessPlugin"
|
||||
}
|
||||
register("versioning") {
|
||||
id = "dev.msfjarvis.claw.versioning-plugin"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.versioning.VersioningPlugin"
|
||||
}
|
||||
register("versions") {
|
||||
id = "dev.msfjarvis.claw.versions"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.DependencyUpdatesPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.build.agp)
|
||||
implementation(libs.build.cachefix)
|
||||
implementation(libs.build.detekt)
|
||||
implementation(libs.build.kotlin.gradle)
|
||||
implementation(libs.build.semver)
|
||||
implementation(libs.build.spotless)
|
||||
implementation(libs.build.vcu)
|
||||
implementation(libs.build.versions)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins { `kotlin-dsl` }
|
||||
|
||||
afterEvaluate {
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
sourceCompatibility = JavaVersion.VERSION_11.toString()
|
||||
targetCompatibility = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
freeCompilerArgs = freeCompilerArgs + "-Xsam-conversions=class"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
register("kotlin-android") {
|
||||
id = "dev.msfjarvis.claw.kotlin-android"
|
||||
implementationClass = "dev.msfjarvis.aps.gradle.KotlinAndroidPlugin"
|
||||
}
|
||||
register("kotlin-common") {
|
||||
id = "dev.msfjarvis.claw.kotlin-common"
|
||||
implementationClass = "dev.msfjarvis.aps.gradle.KotlinCommonPlugin"
|
||||
}
|
||||
register("kotlin-library") {
|
||||
id = "dev.msfjarvis.claw.kotlin-library"
|
||||
implementationClass = "dev.msfjarvis.aps.gradle.KotlinLibraryPlugin"
|
||||
}
|
||||
register("spotless") {
|
||||
id = "dev.msfjarvis.claw.spotless"
|
||||
implementationClass = "dev.msfjarvis.aps.gradle.SpotlessPlugin"
|
||||
}
|
||||
register("versions") {
|
||||
id = "dev.msfjarvis.claw.versions"
|
||||
implementationClass = "dev.msfjarvis.aps.gradle.DependencyUpdatesPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.build.agp)
|
||||
implementation(libs.build.kotlin.gradle)
|
||||
implementation(libs.build.kotlin.serialization)
|
||||
implementation(libs.build.spotless)
|
||||
implementation(libs.build.vcu)
|
||||
implementation(libs.build.versions)
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package dev.msfjarvis.aps.gradle
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
|
||||
@Suppress("Unused")
|
||||
class KotlinLibraryPlugin : Plugin<Project> {
|
||||
|
||||
override fun apply(project: Project) {
|
||||
project.pluginManager.apply(KotlinCommonPlugin::class)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
* Copyright © 2022 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")
|
||||
|
||||
@@ -37,7 +39,3 @@ dependencyResolutionManagement {
|
||||
}
|
||||
versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } }
|
||||
}
|
||||
|
||||
include("android-plugins")
|
||||
|
||||
include("kotlin-plugins")
|
||||
|
||||
+27
-28
@@ -1,19 +1,29 @@
|
||||
package dev.msfjarvis.aps.gradle
|
||||
/*
|
||||
* Copyright © 2022 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 com.android.build.api.dsl.TestExtension
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.android.build.api.dsl.LibraryExtension
|
||||
import com.android.build.api.dsl.Lint
|
||||
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
|
||||
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
||||
import com.android.build.gradle.TestedExtension
|
||||
import com.android.build.gradle.BaseExtension
|
||||
import org.gradle.android.AndroidCacheFixPlugin
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.findByType
|
||||
|
||||
private const val SLIM_TESTS_PROPERTY = "slimTests"
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
class AndroidCommonPlugin : Plugin<Project> {
|
||||
|
||||
private companion object {
|
||||
@@ -21,22 +31,17 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
const val MIN_SDK = 26
|
||||
const val TARGET_SDK = 33
|
||||
}
|
||||
|
||||
override fun apply(project: Project) {
|
||||
project.configureSlimTests()
|
||||
project.pluginManager.apply(AndroidCacheFixPlugin::class)
|
||||
project.extensions.findByType<TestedExtension>()?.run {
|
||||
project.extensions.configure<BaseExtension> {
|
||||
compileSdkVersion(COMPILE_SDK)
|
||||
defaultConfig {
|
||||
minSdk = MIN_SDK
|
||||
targetSdk = TARGET_SDK
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
named("main") { java.srcDirs("src/main/kotlin") }
|
||||
named("test") { java.srcDirs("src/test/kotlin") }
|
||||
named("androidTest") { java.srcDirs("src/androidTest/kotlin") }
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources.excludes.add("**/*.version")
|
||||
resources.excludes.add("**/*.txt")
|
||||
@@ -56,25 +61,19 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
unitTests.isReturnDefaultValues = true
|
||||
}
|
||||
}
|
||||
project.extensions.findByType<TestExtension>()?.run {
|
||||
compileSdk = COMPILE_SDK
|
||||
defaultConfig {
|
||||
minSdk = MIN_SDK
|
||||
targetSdk = TARGET_SDK
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
testOptions {
|
||||
animationsDisabled = true
|
||||
unitTests.isReturnDefaultValues = true
|
||||
}
|
||||
}
|
||||
project.extensions.findByType<ApplicationExtension>()?.run { lint.configureLint(project) }
|
||||
project.extensions.findByType<LibraryExtension>()?.run { lint.configureLint(project) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Lint.configureLint(project: Project) {
|
||||
abortOnError = false
|
||||
checkReleaseBuilds = false
|
||||
warningsAsErrors = false
|
||||
disable.add("DialogFragmentCallbacksDetector")
|
||||
baseline = project.file("lint-baseline.xml")
|
||||
}
|
||||
|
||||
/**
|
||||
* When the "slimTests" project property is provided, disable the unit test tasks on `release` build
|
||||
* type and `nonFree` product flavor to avoid running the same tests repeatedly in different build
|
||||
@@ -83,7 +82,7 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
* Examples: `./gradlew test -PslimTests` will run unit tests for `nonFreeDebug` and `debug` build
|
||||
* variants in Android App and Library projects, and all tests in JVM projects.
|
||||
*/
|
||||
internal fun Project.configureSlimTests() {
|
||||
private fun Project.configureSlimTests() {
|
||||
if (providers.gradleProperty(SLIM_TESTS_PROPERTY).isPresent) {
|
||||
// Disable unit test tasks on the release build type for Android Library projects
|
||||
extensions.findByType<LibraryAndroidComponentsExtension>()?.run {
|
||||
+11
-15
@@ -1,14 +1,20 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 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")
|
||||
|
||||
package dev.msfjarvis.aps.gradle
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.android.build.gradle.AppPlugin
|
||||
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
|
||||
import dev.msfjarvis.aps.gradle.signing.configureBuildSigning
|
||||
import dev.msfjarvis.claw.gradle.signing.configureBuildSigning
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
@Suppress("Unused")
|
||||
class ApplicationPlugin : Plugin<Project> {
|
||||
@@ -16,9 +22,7 @@ class ApplicationPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
project.pluginManager.apply(AppPlugin::class)
|
||||
project.pluginManager.apply(AndroidCommonPlugin::class)
|
||||
project.extensions.getByType<BaseAppModuleExtension>().run {
|
||||
adbOptions.installOptions("--user 0")
|
||||
|
||||
project.extensions.configure<ApplicationExtension> {
|
||||
dependenciesInfo {
|
||||
includeInBundle = false
|
||||
includeInApk = false
|
||||
@@ -50,12 +54,4 @@ class ApplicationPlugin : Plugin<Project> {
|
||||
project.configureBuildSigning()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Project.isSnapshot(): Boolean {
|
||||
with(project.providers) {
|
||||
val workflow = environmentVariable("GITHUB_WORKFLOW")
|
||||
val snapshot = environmentVariable("SNAPSHOT")
|
||||
return workflow.isPresent || snapshot.isPresent
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-4
@@ -1,4 +1,10 @@
|
||||
package dev.msfjarvis.aps.gradle
|
||||
/*
|
||||
* Copyright © 2022 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.gradle
|
||||
|
||||
import com.github.benmanes.gradle.versions.VersionsPlugin
|
||||
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
|
||||
@@ -7,7 +13,7 @@ import nl.littlerobots.vcu.plugin.VersionCatalogUpdatePlugin
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
|
||||
@Suppress("Unused")
|
||||
@@ -20,13 +26,22 @@ class DependencyUpdatesPlugin : Plugin<Project> {
|
||||
when (candidate.group) {
|
||||
"com.squareup.okhttp3",
|
||||
"org.jetbrains.kotlin" -> true
|
||||
else -> false
|
||||
else -> isNonStable(candidate.version) && !isNonStable(currentVersion)
|
||||
}
|
||||
}
|
||||
checkConstraints = true
|
||||
checkBuildEnvironmentConstraints = true
|
||||
checkForGradleUpdate = true
|
||||
}
|
||||
project.extensions.getByType<VersionCatalogUpdateExtension>().run {
|
||||
project.extensions.configure<VersionCatalogUpdateExtension> {
|
||||
keep.keepUnusedLibraries.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNonStable(version: String): Boolean {
|
||||
val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) }
|
||||
val regex = "^[0-9,.v-]+(-r)?$".toRegex()
|
||||
val isStable = stableKeyword || regex.matches(version)
|
||||
return isStable.not()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright © 2022 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.gradle
|
||||
|
||||
import io.gitlab.arturbosch.detekt.DetektPlugin
|
||||
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
object Detekt {
|
||||
private const val TWITTER_RULES_VERSION = "0.0.26"
|
||||
|
||||
fun apply(project: Project) {
|
||||
project.pluginManager.apply(DetektPlugin::class.java)
|
||||
project.extensions.configure<DetektExtension> {
|
||||
debug = project.providers.gradleProperty("debugDetekt").isPresent
|
||||
parallel = true
|
||||
ignoredBuildTypes = listOf("benchmark", "release")
|
||||
basePath = project.layout.projectDirectory.toString()
|
||||
baseline =
|
||||
project.rootProject.layout.projectDirectory
|
||||
.dir("detekt-baselines")
|
||||
.file("${project.name}.xml")
|
||||
.asFile
|
||||
}
|
||||
project.dependencies.add(
|
||||
"detektPlugins",
|
||||
"com.twitter.compose.rules:detekt:$TWITTER_RULES_VERSION",
|
||||
)
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -1,9 +1,10 @@
|
||||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
* Copyright © 2022 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.aps.gradle
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
+10
-9
@@ -1,9 +1,10 @@
|
||||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
* Copyright © 2022 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.aps.gradle
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Plugin
|
||||
@@ -18,6 +19,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
class KotlinCommonPlugin : Plugin<Project> {
|
||||
|
||||
override fun apply(project: Project) {
|
||||
Detekt.apply(project)
|
||||
project.tasks.run {
|
||||
withType<JavaCompile>().configureEach {
|
||||
sourceCompatibility = JavaVersion.VERSION_11.toString()
|
||||
@@ -25,16 +27,17 @@ class KotlinCommonPlugin : Plugin<Project> {
|
||||
}
|
||||
withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
allWarningsAsErrors = false
|
||||
allWarningsAsErrors =
|
||||
false // project.providers.environmentVariable("GITHUB_WORKFLOW").isPresent
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
freeCompilerArgs = freeCompilerArgs + ADDITIONAL_COMPILER_ARGS
|
||||
languageVersion = "1.5"
|
||||
languageVersion = "1.7"
|
||||
}
|
||||
}
|
||||
withType<Test>().configureEach {
|
||||
maxParallelForks = Runtime.getRuntime().availableProcessors() * 2
|
||||
testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) }
|
||||
doNotTrackState("We want tests to always run even if Gradle thinks otherwise")
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,8 +46,6 @@ class KotlinCommonPlugin : Plugin<Project> {
|
||||
private val ADDITIONAL_COMPILER_ARGS =
|
||||
listOf(
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=1.7.20",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright © 2022 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.gradle
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper
|
||||
|
||||
@Suppress("Unused")
|
||||
class KotlinJvmPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
project.pluginManager.apply(KotlinPluginWrapper::class)
|
||||
project.pluginManager.apply(KotlinCommonPlugin::class)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright © 2022 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.gradle
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.jetbrains.kotlin.gradle.internal.Kapt3GradleSubplugin
|
||||
import org.jetbrains.kotlin.gradle.plugin.KaptExtension
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper
|
||||
|
||||
@Suppress("Unused")
|
||||
class KotlinKaptPlugin : Plugin<Project> {
|
||||
|
||||
override fun apply(project: Project) {
|
||||
project.pluginManager.run {
|
||||
apply(KotlinAndroidPluginWrapper::class)
|
||||
apply(Kapt3GradleSubplugin::class)
|
||||
}
|
||||
project.afterEvaluate {
|
||||
project.extensions.configure<KaptExtension> {
|
||||
javacOptions {
|
||||
if (hasDaggerCompilerDependency()) {
|
||||
// https://dagger.dev/dev-guide/compiler-options#fastinit-mode
|
||||
option("-Adagger.fastInit=enabled")
|
||||
// Enable the better, experimental error messages
|
||||
// https://github.com/google/dagger/commit/0d2505a727b54f47b8677f42dd4fc5c1924e37f5
|
||||
option("-Adagger.experimentalDaggerErrorMessages=enabled")
|
||||
// KAPT nests errors causing real issues to be suppressed in CI logs
|
||||
option("-Xmaxerrs", 500)
|
||||
// Enables per-module validation for faster error detection
|
||||
// https://github.com/google/dagger/commit/325b516ac6a53d3fc973d247b5231fafda9870a2
|
||||
option("-Adagger.moduleBindingValidation=ERROR")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
project.tasks
|
||||
.matching { it.name.startsWith("kapt") && it.name.endsWith("UnitTestKotlin") }
|
||||
.configureEach { enabled = false }
|
||||
}
|
||||
|
||||
private fun Project.hasDaggerCompilerDependency(): Boolean {
|
||||
return configurations.any {
|
||||
it.dependencies.any { dependency -> dependency.name == "dagger-compiler" }
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -1,4 +1,10 @@
|
||||
package dev.msfjarvis.aps.gradle
|
||||
/*
|
||||
* Copyright © 2022 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.gradle
|
||||
|
||||
import com.android.build.gradle.LibraryPlugin
|
||||
import org.gradle.api.Plugin
|
||||
+18
-9
@@ -1,13 +1,19 @@
|
||||
package dev.msfjarvis.aps.gradle
|
||||
/*
|
||||
* Copyright © 2022 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.gradle
|
||||
|
||||
import com.android.build.api.artifact.SingleArtifact
|
||||
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
|
||||
import com.android.build.api.variant.VariantOutputConfiguration
|
||||
import dev.msfjarvis.aps.gradle.artifacts.CollectApksTask
|
||||
import dev.msfjarvis.aps.gradle.artifacts.CollectBundleTask
|
||||
import dev.msfjarvis.claw.gradle.artifacts.CollectApksTask
|
||||
import dev.msfjarvis.claw.gradle.artifacts.CollectBundleTask
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.register
|
||||
|
||||
@Suppress("Unused")
|
||||
@@ -15,23 +21,26 @@ class RenameArtifactsPlugin : Plugin<Project> {
|
||||
|
||||
override fun apply(project: Project) {
|
||||
project.pluginManager.withPlugin("com.android.application") {
|
||||
project.extensions.getByType<ApplicationAndroidComponentsExtension>().run {
|
||||
project.extensions.configure<ApplicationAndroidComponentsExtension> {
|
||||
onVariants { variant ->
|
||||
project.tasks.register<CollectApksTask>("collect${variant.name.capitalize()}Apks") {
|
||||
val taskPrefix = "collect${variant.name.replaceFirstChar { it.uppercase() }}"
|
||||
project.tasks.register<CollectApksTask>("${taskPrefix}Apks") {
|
||||
variantName.set(variant.name)
|
||||
apkFolder.set(variant.artifacts.get(SingleArtifact.APK))
|
||||
mappingFile.set(variant.artifacts.get(SingleArtifact.OBFUSCATION_MAPPING_FILE))
|
||||
builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader())
|
||||
outputDirectory.set(project.layout.projectDirectory.dir("outputs"))
|
||||
outputDirectory.set(project.layout.projectDirectory.dir("apk"))
|
||||
}
|
||||
project.tasks.register<CollectBundleTask>("collect${variant.name.capitalize()}Bundle") {
|
||||
project.tasks.register<CollectBundleTask>("${taskPrefix}Bundle") {
|
||||
val mainOutput =
|
||||
variant.outputs.single {
|
||||
it.outputType == VariantOutputConfiguration.OutputType.SINGLE
|
||||
}
|
||||
variantName.set(variant.name)
|
||||
versionName.set(mainOutput.versionName)
|
||||
mappingFile.set(variant.artifacts.get(SingleArtifact.OBFUSCATION_MAPPING_FILE))
|
||||
bundleFile.set(variant.artifacts.get(SingleArtifact.BUNDLE))
|
||||
outputDirectory.set(project.layout.projectDirectory.dir("outputs"))
|
||||
outputDirectory.set(project.layout.projectDirectory.dir("bundle"))
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-8
@@ -1,32 +1,39 @@
|
||||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
* Copyright © 2022 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.aps.gradle
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import com.diffplug.gradle.spotless.SpotlessExtension
|
||||
import com.diffplug.gradle.spotless.SpotlessPlugin
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
@Suppress("Unused")
|
||||
class SpotlessPlugin : Plugin<Project> {
|
||||
|
||||
override fun apply(project: Project) {
|
||||
if (project.rootProject != project) {
|
||||
throw GradleException("Spotless plugin must only be applied to the root project.")
|
||||
}
|
||||
project.pluginManager.apply(SpotlessPlugin::class)
|
||||
project.extensions.getByType<SpotlessExtension>().run {
|
||||
project.extensions.configure<SpotlessExtension> {
|
||||
kotlin {
|
||||
ktfmt(KTFMT_VERSION).googleStyle()
|
||||
target("**/*.kt")
|
||||
targetExclude("**/build/", "**/gen/")
|
||||
targetExclude("**/build/", "/spotless/")
|
||||
licenseHeaderFile(project.file("spotless/license.kt"))
|
||||
}
|
||||
kotlinGradle {
|
||||
ktfmt(KTFMT_VERSION).googleStyle()
|
||||
target("**/*.kts")
|
||||
targetExclude("**/build/")
|
||||
licenseHeaderFile(project.file("spotless/license.kt"), "import|plugins|@file")
|
||||
}
|
||||
format("xml") {
|
||||
target("**/*.xml")
|
||||
@@ -39,6 +46,6 @@ class SpotlessPlugin : Plugin<Project> {
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val KTFMT_VERSION = "0.41"
|
||||
private const val KTFMT_VERSION = "0.43"
|
||||
}
|
||||
}
|
||||
+25
-2
@@ -1,14 +1,24 @@
|
||||
package dev.msfjarvis.aps.gradle.artifacts
|
||||
/*
|
||||
* Copyright © 2022 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.gradle.artifacts
|
||||
|
||||
import com.android.build.api.variant.BuiltArtifactsLoader
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.CacheableTask
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
@@ -21,6 +31,10 @@ import org.gradle.api.tasks.TaskAction
|
||||
abstract class CollectApksTask : DefaultTask() {
|
||||
@get:InputFiles @get:PathSensitive(PathSensitivity.NONE) abstract val apkFolder: DirectoryProperty
|
||||
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.NONE)
|
||||
abstract val mappingFile: RegularFileProperty
|
||||
|
||||
@get:Input abstract val variantName: Property<String>
|
||||
|
||||
@get:Internal abstract val builtArtifactsLoader: Property<BuiltArtifactsLoader>
|
||||
@@ -30,9 +44,13 @@ abstract class CollectApksTask : DefaultTask() {
|
||||
@TaskAction
|
||||
fun run() {
|
||||
val outputDir = outputDirectory.asFile.get()
|
||||
val outputDirStream =
|
||||
Files.walk(outputDir.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile)
|
||||
outputDirStream.forEach(File::delete)
|
||||
outputDirStream.close()
|
||||
outputDir.mkdirs()
|
||||
val builtArtifacts =
|
||||
builtArtifactsLoader.get().load(apkFolder.get()) ?: throw RuntimeException("Cannot load APKs")
|
||||
builtArtifactsLoader.get().load(apkFolder.get()) ?: error("Cannot load APKs")
|
||||
builtArtifacts.elements.forEach { artifact ->
|
||||
Files.copy(
|
||||
Paths.get(artifact.outputFile),
|
||||
@@ -40,5 +58,10 @@ abstract class CollectApksTask : DefaultTask() {
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
Files.copy(
|
||||
mappingFile.get().asFile.toPath(),
|
||||
outputDir.resolve("mapping.txt").toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
}
|
||||
+20
-1
@@ -1,6 +1,14 @@
|
||||
package dev.msfjarvis.aps.gradle.artifacts
|
||||
/*
|
||||
* Copyright © 2022 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.gradle.artifacts
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
@@ -14,6 +22,8 @@ import org.gradle.api.tasks.TaskAction
|
||||
abstract class CollectBundleTask : DefaultTask() {
|
||||
@get:InputFile abstract val bundleFile: RegularFileProperty
|
||||
|
||||
@get:InputFile abstract val mappingFile: RegularFileProperty
|
||||
|
||||
@get:Input abstract val variantName: Property<String>
|
||||
|
||||
@get:Input abstract val versionName: Property<String>
|
||||
@@ -23,11 +33,20 @@ abstract class CollectBundleTask : DefaultTask() {
|
||||
@TaskAction
|
||||
fun taskAction() {
|
||||
val outputDir = outputDirectory.asFile.get()
|
||||
val outputDirStream =
|
||||
Files.walk(outputDir.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile)
|
||||
outputDirStream.forEach(File::delete)
|
||||
outputDirStream.close()
|
||||
outputDir.mkdirs()
|
||||
Files.copy(
|
||||
bundleFile.get().asFile.toPath(),
|
||||
outputDir.resolve("Claw-${variantName.get()}-${versionName.get()}.aab").toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
Files.copy(
|
||||
mappingFile.get().asFile.toPath(),
|
||||
outputDir.resolve("mapping.txt").toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -1,9 +1,10 @@
|
||||
/*
|
||||
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
* Copyright © 2022 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.aps.gradle.signing
|
||||
package dev.msfjarvis.claw.gradle.signing
|
||||
|
||||
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
|
||||
import java.util.Properties
|
||||
+7
-1
@@ -1,4 +1,10 @@
|
||||
package dev.msfjarvis.aps.gradle.versioning
|
||||
/*
|
||||
* Copyright © 2022 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.gradle.versioning
|
||||
|
||||
const val VERSIONING_PROP_FILE = "version.properties"
|
||||
const val VERSIONING_PROP_VERSION_NAME = "versioning-plugin.versionName"
|
||||
+17
-7
@@ -1,4 +1,10 @@
|
||||
package dev.msfjarvis.aps.gradle.versioning
|
||||
/*
|
||||
* Copyright © 2022 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.gradle.versioning
|
||||
|
||||
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
|
||||
import com.android.build.api.variant.VariantOutputConfiguration
|
||||
@@ -8,7 +14,7 @@ import java.util.Properties
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.register
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
|
||||
@@ -40,11 +46,15 @@ class VersioningPlugin : Plugin<Project> {
|
||||
}
|
||||
project.plugins.withType<AppPlugin> {
|
||||
androidAppPluginApplied.set(true)
|
||||
extensions.getByType<ApplicationAndroidComponentsExtension>().onVariants { variant ->
|
||||
val mainOutput =
|
||||
variant.outputs.single { it.outputType == VariantOutputConfiguration.OutputType.SINGLE }
|
||||
mainOutput.versionName.set(versionName)
|
||||
mainOutput.versionCode.set(versionCode)
|
||||
extensions.configure<ApplicationAndroidComponentsExtension> {
|
||||
onVariants { variant ->
|
||||
val mainOutput =
|
||||
variant.outputs.single {
|
||||
it.outputType == VariantOutputConfiguration.OutputType.SINGLE
|
||||
}
|
||||
mainOutput.versionName.set(versionName)
|
||||
mainOutput.versionCode.set(versionCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
val version = Semver(versionName)
|
||||
+7
-1
@@ -1,4 +1,10 @@
|
||||
package dev.msfjarvis.aps.gradle.versioning
|
||||
/*
|
||||
* Copyright © 2022 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.gradle.versioning
|
||||
|
||||
import com.vdurmont.semver4j.Semver
|
||||
import org.gradle.api.DefaultTask
|
||||
+6
-1
@@ -1,9 +1,14 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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("DSL_SCOPE_VIOLATION")
|
||||
|
||||
plugins {
|
||||
id("dev.msfjarvis.claw.spotless")
|
||||
id("dev.msfjarvis.claw.versions")
|
||||
id("dev.msfjarvis.claw.kotlin-common")
|
||||
alias(libs.plugins.hilt) apply false
|
||||
alias(libs.plugins.android.test) apply false
|
||||
}
|
||||
|
||||
+13
-3
@@ -1,22 +1,32 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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("DSL_SCOPE_VIOLATION", "UnstableApiUsage")
|
||||
|
||||
plugins {
|
||||
kotlin("android")
|
||||
id("dev.msfjarvis.claw.kotlin-common")
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
id("dev.msfjarvis.claw.android-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.whetstone)
|
||||
}
|
||||
|
||||
anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
androidComponents { beforeVariants { it.enableUnitTest = false } }
|
||||
|
||||
dependencies {
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
api(libs.napier)
|
||||
implementation(projects.core)
|
||||
implementation(projects.database)
|
||||
implementation(projects.model)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(libs.androidx.compose.animation)
|
||||
implementation(libs.androidx.compose.foundation)
|
||||
implementation(libs.androidx.compose.material)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.0.0-alpha08" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-alpha08)" variant="all" version="8.0.0-alpha08">
|
||||
|
||||
</issues>
|
||||
@@ -1,6 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.common
|
||||
|
||||
sealed class NetworkState {
|
||||
internal sealed class NetworkState {
|
||||
class Success<T>(val data: T) : NetworkState()
|
||||
class Error(val error: Throwable, val description: String) : NetworkState()
|
||||
object Loading : NetworkState()
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 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.common.comments
|
||||
|
||||
import android.text.format.DateUtils
|
||||
@@ -14,23 +20,22 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.posts.PostTitle
|
||||
@@ -39,21 +44,28 @@ import dev.msfjarvis.claw.common.posts.TagRow
|
||||
import dev.msfjarvis.claw.common.res.ClawIcons
|
||||
import dev.msfjarvis.claw.common.ui.NetworkImage
|
||||
import dev.msfjarvis.claw.common.ui.ThemedRichText
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import dev.msfjarvis.claw.model.ExtendedPostDetails
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import java.time.Instant
|
||||
import java.time.temporal.TemporalAccessor
|
||||
|
||||
@Composable
|
||||
fun CommentsHeader(
|
||||
postDetails: ExtendedPostDetails,
|
||||
internal fun CommentsHeader(
|
||||
postDetails: LobstersPostDetails,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val htmlConverter = LocalHTMLConverter.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val linkMetadata by
|
||||
produceState(
|
||||
initialValue = LinkMetadata(postDetails.url, null, null),
|
||||
) {
|
||||
runCatching { postActions.getLinkMetadata(postDetails.url) }
|
||||
.onSuccess { metadata -> value = metadata }
|
||||
}
|
||||
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Surface(color = MaterialTheme.colorScheme.background, modifier = modifier) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
@@ -62,13 +74,11 @@ fun CommentsHeader(
|
||||
TagRow(tags = postDetails.tags)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
if (postDetails.linkMetadata.url.isNotBlank()) {
|
||||
if (linkMetadata.url.isNotBlank()) {
|
||||
PostLink(
|
||||
linkMetadata = postDetails.linkMetadata,
|
||||
linkMetadata = linkMetadata,
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
postActions.viewPost(postDetails.linkMetadata.url, postDetails.commentsUrl)
|
||||
},
|
||||
Modifier.clickable { postActions.viewPost(linkMetadata.url, postDetails.commentsUrl) },
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
@@ -91,7 +101,7 @@ fun CommentsHeader(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PostLink(
|
||||
private fun PostLink(
|
||||
linkMetadata: LinkMetadata,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -101,22 +111,13 @@ fun PostLink(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
) {
|
||||
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
if (linkMetadata.faviconUrl != null) {
|
||||
NetworkImage(
|
||||
url = linkMetadata.faviconUrl!!,
|
||||
placeholder = ClawIcons.Web,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = ClawIcons.Web,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSecondary,
|
||||
modifier = Modifier.clip(CircleShape),
|
||||
)
|
||||
}
|
||||
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
NetworkImage(
|
||||
url = linkMetadata.faviconUrl,
|
||||
placeholder = ClawIcons.Web,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
Text(
|
||||
text = linkMetadata.url,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -128,20 +129,30 @@ fun PostLink(
|
||||
}
|
||||
}
|
||||
|
||||
private val CommentEntryPadding = 16f.dp
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun CommentEntry(
|
||||
comment: Comment,
|
||||
internal fun CommentEntry(
|
||||
commentNode: CommentNode,
|
||||
htmlConverter: HTMLConverter,
|
||||
toggleExpanded: (CommentNode) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember(comment) { mutableStateOf(true) }
|
||||
val htmlConverter = LocalHTMLConverter.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val comment = commentNode.comment
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable { expanded = !expanded }
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { toggleExpanded(commentNode) }
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(start = (comment.indentLevel * 16).dp, end = 16.dp, top = 16.dp, bottom = 16.dp),
|
||||
.padding(
|
||||
start = CommentEntryPadding * comment.indentLevel,
|
||||
end = CommentEntryPadding,
|
||||
top = CommentEntryPadding,
|
||||
bottom = CommentEntryPadding,
|
||||
),
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Submitter(
|
||||
@@ -149,6 +160,7 @@ fun CommentEntry(
|
||||
buildCommenterString(
|
||||
commenterName = comment.user.username,
|
||||
score = comment.score,
|
||||
isUnread = commentNode.isUnread,
|
||||
createdAt = comment.createdAt,
|
||||
updatedAt = comment.updatedAt,
|
||||
),
|
||||
@@ -157,7 +169,7 @@ fun CommentEntry(
|
||||
modifier =
|
||||
Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${comment.user.username}") },
|
||||
)
|
||||
AnimatedContent(targetState = expanded) { expandedState ->
|
||||
AnimatedContent(targetState = commentNode.isExpanded) { expandedState ->
|
||||
if (expandedState) {
|
||||
ThemedRichText(
|
||||
text = htmlConverter.convertHTMLToMarkdown(comment.comment),
|
||||
@@ -173,6 +185,7 @@ fun CommentEntry(
|
||||
fun buildCommenterString(
|
||||
commenterName: String,
|
||||
score: Int,
|
||||
isUnread: Boolean,
|
||||
createdAt: TemporalAccessor,
|
||||
updatedAt: TemporalAccessor,
|
||||
): AnnotatedString {
|
||||
@@ -211,5 +224,17 @@ fun buildCommenterString(
|
||||
append(updatedRelative.toString())
|
||||
append(')')
|
||||
}
|
||||
if (isUnread) {
|
||||
append(' ')
|
||||
withStyle(
|
||||
style =
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
) {
|
||||
append("(unread)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 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.common.comments
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.Divider
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
|
||||
internal data class CommentNode(
|
||||
val comment: Comment,
|
||||
var parent: CommentNode? = null,
|
||||
val children: MutableList<CommentNode> = mutableListOf(),
|
||||
val isUnread: Boolean = false,
|
||||
var isExpanded: Boolean = true,
|
||||
) {
|
||||
fun addChild(child: CommentNode) {
|
||||
if (comment.indentLevel + 1 == child.comment.indentLevel) {
|
||||
children.add(child)
|
||||
child.parent = this
|
||||
} else {
|
||||
children.last().addChild(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createListNode(
|
||||
comments: List<Comment>,
|
||||
commentState: PostComments?
|
||||
): MutableList<CommentNode> {
|
||||
val commentNodes = mutableListOf<CommentNode>()
|
||||
val isUnread = { id: String -> commentState?.commentIds?.contains(id) == false }
|
||||
|
||||
for (i in comments.indices) {
|
||||
if (comments[i].indentLevel == 1) {
|
||||
commentNodes.add(CommentNode(comment = comments[i], isUnread = isUnread(comments[i].shortId)))
|
||||
} else {
|
||||
commentNodes
|
||||
.last()
|
||||
.addChild(CommentNode(comment = comments[i], isUnread = isUnread(comments[i].shortId)))
|
||||
}
|
||||
}
|
||||
|
||||
return commentNodes
|
||||
}
|
||||
|
||||
internal fun setExpanded(commentNode: CommentNode, expanded: Boolean): CommentNode {
|
||||
commentNode.isExpanded = expanded
|
||||
|
||||
if (commentNode.children.isNotEmpty()) {
|
||||
commentNode.children.forEach { setExpanded(it, expanded) }
|
||||
}
|
||||
return commentNode
|
||||
}
|
||||
|
||||
internal fun findTopMostParent(node: CommentNode): CommentNode {
|
||||
val parent = node.parent
|
||||
return if (parent != null) {
|
||||
findTopMostParent(parent)
|
||||
} else {
|
||||
node
|
||||
}
|
||||
}
|
||||
|
||||
internal fun LazyListScope.nodes(
|
||||
nodes: List<CommentNode>,
|
||||
htmlConverter: HTMLConverter,
|
||||
toggleExpanded: (CommentNode) -> Unit,
|
||||
) {
|
||||
nodes.forEach { node ->
|
||||
node(
|
||||
node = node,
|
||||
htmlConverter = htmlConverter,
|
||||
toggleExpanded = toggleExpanded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.node(
|
||||
node: CommentNode,
|
||||
htmlConverter: HTMLConverter,
|
||||
toggleExpanded: (CommentNode) -> Unit,
|
||||
) {
|
||||
// Skip the node if neither the node nor its parent is expanded
|
||||
if (!node.isExpanded && node.parent?.isExpanded == false) {
|
||||
return
|
||||
}
|
||||
item {
|
||||
CommentEntry(
|
||||
commentNode = node,
|
||||
htmlConverter = htmlConverter,
|
||||
toggleExpanded = toggleExpanded,
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
if (node.children.isNotEmpty()) {
|
||||
nodes(
|
||||
node.children,
|
||||
htmlConverter = htmlConverter,
|
||||
toggleExpanded = toggleExpanded,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,26 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 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.common.comments
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -24,19 +32,34 @@ import dev.msfjarvis.claw.common.NetworkState.Success
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.ui.NetworkError
|
||||
import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||
import dev.msfjarvis.claw.model.ExtendedPostDetails
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun CommentsPageInternal(
|
||||
details: ExtendedPostDetails,
|
||||
details: LobstersPostDetails,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
commentState: PostComments?,
|
||||
markSeenComments: (String, List<Comment>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val commentNodes = createListNode(details.comments, commentState).toMutableStateList()
|
||||
LaunchedEffect(key1 = commentNodes) { markSeenComments(details.shortId, details.comments) }
|
||||
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
|
||||
LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) {
|
||||
item { CommentsHeader(postDetails = details, postActions = postActions) }
|
||||
item {
|
||||
CommentsHeader(
|
||||
postDetails = details,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
)
|
||||
}
|
||||
|
||||
if (details.comments.isNotEmpty()) {
|
||||
if (commentNodes.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "Comments",
|
||||
@@ -45,12 +68,20 @@ private fun CommentsPageInternal(
|
||||
)
|
||||
}
|
||||
|
||||
itemsIndexed(details.comments) { index, item ->
|
||||
if (index != 0) {
|
||||
Divider()
|
||||
}
|
||||
CommentEntry(item)
|
||||
}
|
||||
nodes(
|
||||
nodes = commentNodes,
|
||||
htmlConverter = htmlConverter,
|
||||
toggleExpanded = { node ->
|
||||
val newNode = setExpanded(node, !node.isExpanded)
|
||||
val parent = findTopMostParent(newNode)
|
||||
val index =
|
||||
commentNodes.indexOf(commentNodes.find { it.comment.url == parent.comment.url })
|
||||
if (index != -1) {
|
||||
commentNodes.removeAt(index)
|
||||
commentNodes.add(index, parent)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
item {
|
||||
Text(
|
||||
@@ -66,35 +97,52 @@ private fun CommentsPageInternal(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Suppress("UNCHECKED_CAST", "LongParameterList")
|
||||
@Composable
|
||||
fun CommentsPage(
|
||||
postId: String,
|
||||
getDetails: suspend (String) -> ExtendedPostDetails,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
getSeenComments: suspend (String) -> PostComments?,
|
||||
markSeenComments: (String, List<Comment>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val postDetails by
|
||||
produceState<NetworkState>(Loading) {
|
||||
runCatching { getDetails(postId) }
|
||||
runCatching { postActions.getComments(postId) }
|
||||
.fold(
|
||||
onSuccess = { details -> value = Success(details) },
|
||||
onFailure = { value = Error(error = it, description = "Failed to load comments") }
|
||||
)
|
||||
}
|
||||
val commentState by
|
||||
produceState<PostComments?>(initialValue = null) { value = getSeenComments(postId) }
|
||||
|
||||
when (postDetails) {
|
||||
is Success<*> -> {
|
||||
CommentsPageInternal(
|
||||
(postDetails as Success<ExtendedPostDetails>).data,
|
||||
postActions,
|
||||
modifier.fillMaxSize(),
|
||||
details = (postDetails as Success<LobstersPostDetails>).data,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
commentState = commentState,
|
||||
markSeenComments = markSeenComments,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
is Error -> {
|
||||
val error = postDetails as Error
|
||||
NetworkError(label = error.description, error = error.error)
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
NetworkError(
|
||||
label = error.description,
|
||||
error = error.error,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading -> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ProgressBar(modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
}
|
||||
Loading -> ProgressBar()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.common.comments
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
/** Defines a contract to convert strings of HTML to Markdown. */
|
||||
@Stable
|
||||
fun interface HTMLConverter {
|
||||
fun convertHTMLToMarkdown(html: String): String
|
||||
}
|
||||
|
||||
val LocalHTMLConverter = staticCompositionLocalOf<HTMLConverter> { error("To be provided") }
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright © 2022 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.common.injection
|
||||
|
||||
import android.app.Application
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import coil.disk.DiskCache
|
||||
import coil.memory.MemoryCache
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesMultibinding
|
||||
import dev.msfjarvis.claw.core.injection.AppPlugin
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesMultibinding(ApplicationScope::class)
|
||||
class CoilAppPlugin @Inject constructor() : AppPlugin {
|
||||
override fun apply(application: Application) {
|
||||
Coil.setImageLoader {
|
||||
ImageLoader.Builder(application)
|
||||
.memoryCache { MemoryCache.Builder(application).maxSizePercent(MEMORY_CACHE_RATIO).build() }
|
||||
.diskCache {
|
||||
DiskCache.Builder()
|
||||
.directory(application.cacheDir.resolve("image_cache"))
|
||||
.maxSizeBytes(DISK_CACHE_MAX_SIZE)
|
||||
.build()
|
||||
}
|
||||
// Show a short crossfade when loading images asynchronously.
|
||||
.crossfade(true)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val MEMORY_CACHE_RATIO = 0.25
|
||||
private const val DISK_CACHE_MAX_SIZE = 25L * 1024 * 1024 // 25 MB
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.common.posts
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
@@ -5,7 +11,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -19,8 +24,6 @@ import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.Divider
|
||||
@@ -77,11 +80,7 @@ fun LobstersCard(
|
||||
SaveButton(
|
||||
isSaved = localSavedState,
|
||||
modifier =
|
||||
Modifier.clickable(
|
||||
role = Role.Button,
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
Modifier.clickable(role = Role.Button) {
|
||||
localSavedState = !localSavedState
|
||||
postActions.toggleSave(post)
|
||||
},
|
||||
@@ -92,8 +91,6 @@ fun LobstersCard(
|
||||
modifier =
|
||||
Modifier.combinedClickable(
|
||||
role = Role.Button,
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { postActions.viewComments(post.shortId) },
|
||||
onLongClick = { postActions.viewCommentsPage(post.commentsUrl) },
|
||||
),
|
||||
@@ -118,7 +115,7 @@ fun PostDetails(post: SavedPost, modifier: Modifier = Modifier) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PostTitle(
|
||||
internal fun PostTitle(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -131,7 +128,7 @@ fun PostTitle(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Submitter(
|
||||
internal fun Submitter(
|
||||
text: AnnotatedString,
|
||||
avatarUrl: String,
|
||||
contentDescription: String,
|
||||
@@ -146,15 +143,15 @@ fun Submitter(
|
||||
url = avatarUrl,
|
||||
placeholder = ClawIcons.Account,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier.requiredSize(24.dp).clip(CircleShape),
|
||||
modifier = Modifier.requiredSize(24.dp).clip(CircleShape),
|
||||
)
|
||||
|
||||
Text(text = text, modifier = modifier, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(text = text, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SaveButton(
|
||||
private fun SaveButton(
|
||||
isSaved: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -172,7 +169,7 @@ fun SaveButton(
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun CommentsButton(
|
||||
private fun CommentsButton(
|
||||
commentCount: Int?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -181,7 +178,7 @@ fun CommentsButton(
|
||||
badge = {
|
||||
if (commentCount != null) {
|
||||
Badge(
|
||||
modifier = Modifier.absoluteOffset(x = (-8).dp),
|
||||
modifier = Modifier.absoluteOffset(x = -(8.dp)),
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
) {
|
||||
Text(
|
||||
@@ -203,7 +200,7 @@ fun CommentsButton(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TagRow(
|
||||
internal fun TagRow(
|
||||
tags: List<String>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -217,16 +214,16 @@ fun TagRow(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TagText(
|
||||
private fun TagText(
|
||||
tag: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = tag,
|
||||
modifier =
|
||||
Modifier.background(MaterialTheme.colorScheme.tertiaryContainer, RoundedCornerShape(50))
|
||||
.padding(vertical = 4.dp, horizontal = 12.dp)
|
||||
.then(modifier),
|
||||
modifier
|
||||
.background(MaterialTheme.colorScheme.tertiaryContainer, CircleShape)
|
||||
.padding(vertical = 4.dp, horizontal = 12.dp),
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.common.posts
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
|
||||
@Stable
|
||||
interface PostActions {
|
||||
fun viewPost(postUrl: String, commentsUrl: String)
|
||||
fun viewComments(postId: String)
|
||||
fun viewCommentsPage(commentsUrl: String)
|
||||
fun toggleSave(post: SavedPost)
|
||||
suspend fun getComments(postId: String): LobstersPostDetails
|
||||
suspend fun getLinkMetadata(url: String): LinkMetadata
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 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.common.posts
|
||||
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
@@ -15,6 +21,7 @@ fun LobstersPost.toDbModel(): SavedPost {
|
||||
submitterName = submitter.username,
|
||||
submitterAvatarUrl = submitter.avatarUrl,
|
||||
tags = tags,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,5 +36,6 @@ fun LobstersPostDetails.toDbModel(): SavedPost {
|
||||
submitterName = submitter.username,
|
||||
submitterAvatarUrl = submitter.avatarUrl,
|
||||
tags = tags,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2022 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.common.res
|
||||
|
||||
import dev.msfjarvis.claw.common.res.clawicons.account_circle_black_24dp
|
||||
|
||||
Generated
+15
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
* Copyright © 2022 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(
|
||||
"ClassNaming",
|
||||
"FunctionNaming",
|
||||
"FunctionOnlyReturningConstant",
|
||||
"LongMethod",
|
||||
"MagicNumber",
|
||||
"WildcardImport",
|
||||
)
|
||||
|
||||
package dev.msfjarvis.claw.common.res.clawicons
|
||||
|
||||
import androidx.compose.ui.geometry.*
|
||||
|
||||
+15
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
* Copyright © 2022 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(
|
||||
"ClassNaming",
|
||||
"FunctionNaming",
|
||||
"FunctionOnlyReturningConstant",
|
||||
"LongMethod",
|
||||
"MagicNumber",
|
||||
"WildcardImport",
|
||||
)
|
||||
|
||||
package dev.msfjarvis.claw.common.res.clawicons
|
||||
|
||||
import androidx.compose.ui.geometry.*
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user