Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f00f185f6d | ||
|
|
a08ffb903c | ||
|
|
4eaf25e189 | ||
|
|
109176768c | ||
|
|
cf0ef021d8 | ||
|
|
59ba98174c | ||
|
|
afe78aa82f | ||
|
|
0a70924cab | ||
|
|
ae362c2e03 | ||
|
|
83ca0e6167 | ||
|
|
71977c5b2c | ||
|
|
047d6badb0 | ||
|
|
5c0b5bb4f7 | ||
|
|
7762733da2 | ||
|
|
9f705ca02a | ||
|
|
6c5b54587e | ||
|
|
e318ea3a35 | ||
|
|
5ba0c9a544 | ||
|
|
85fc5cac21 | ||
|
|
86a40d2e84 | ||
|
|
557407347b | ||
|
|
b0467aa36c | ||
|
|
d29b4e2ee2 | ||
|
|
64fb1235d5 | ||
|
|
d46993aaf6 | ||
|
|
310db93ff0 | ||
|
|
f07a5682a1 | ||
|
|
bcde4b9719 | ||
|
|
e2e2f4f574 | ||
|
|
a42139ae1e | ||
|
|
5d0e56f115 | ||
|
|
fa31e300e9 | ||
|
|
f54d790da4 | ||
|
|
96c8a0450e | ||
|
|
886cd65a32 | ||
|
|
8c8dd93136 | ||
|
|
9110244236 | ||
|
|
56f0ceddb6 | ||
|
|
c07752df4a | ||
|
|
3f18160dd0 | ||
|
|
0c3dc00137 | ||
|
|
4fb66975fb | ||
|
|
dc584a300d | ||
|
|
30121d6d88 | ||
|
|
2a97576e37 | ||
|
|
32a59d0104 | ||
|
|
a161769cbd | ||
|
|
9a6d7d267a | ||
|
|
9298fa9ea0 | ||
|
|
f0bd943607 | ||
|
|
30474377fc | ||
|
|
6d04230dc6 | ||
|
|
19f408b6cd | ||
|
|
ad528792fa | ||
|
|
78e34a0042 | ||
|
|
83ba17dea3 | ||
|
|
fcca85c51a | ||
|
|
17c7313f41 | ||
|
|
97126da808 | ||
|
|
fdf891c8eb | ||
|
|
af7c54203f | ||
|
|
334cf11b82 | ||
|
|
a9ba948d12 | ||
|
|
1e29a866a0 | ||
|
|
693713eb8d | ||
|
|
1b6c6af3c4 | ||
|
|
61b0ac83f0 | ||
|
|
bce435e3d2 | ||
|
|
833b3cd927 | ||
|
|
a6a3b2a0e4 | ||
|
|
73e0b4927f | ||
|
|
b68d58c19d | ||
|
|
3bccc8748d | ||
|
|
f115fd8dda | ||
|
|
954ef90d99 | ||
|
|
65f7013e78 | ||
|
|
af799e0165 | ||
|
|
c4fb53e09d | ||
|
|
982497a83f | ||
|
|
165c0eaaed | ||
|
|
2c62515746 | ||
|
|
376c2f138c | ||
|
|
d271dd3733 | ||
|
|
0c36875502 | ||
|
|
b31521affa | ||
|
|
488880ee35 | ||
|
|
f2065d897b | ||
|
|
e83b87be6c | ||
|
|
993b45034e | ||
|
|
9d6d497185 | ||
|
|
719d317263 | ||
|
|
8d78e1910a | ||
|
|
62f643135b | ||
|
|
595a8f7dfe | ||
|
|
1d7be62857 | ||
|
|
f86cdb0e62 | ||
|
|
ffc525a1ad | ||
|
|
de6b13fbfc | ||
|
|
e949ee0bb6 | ||
|
|
9f4caa7f62 | ||
|
|
a334eebe74 | ||
|
|
fd30320ed6 | ||
|
|
c293fced8a | ||
|
|
fa57bc9695 | ||
|
|
1d1c7a36b6 | ||
|
|
682377f6c5 | ||
|
|
a8d637e589 | ||
|
|
9e2564f352 | ||
|
|
eb17c20be8 | ||
|
|
e878699e2a | ||
|
|
9dc0b5cd13 | ||
|
|
875453cce5 | ||
|
|
82a1ba9ac3 | ||
|
|
ff4a09e41b | ||
|
|
6b670ad3e5 | ||
|
|
2bf7ccc952 | ||
|
|
d25476be11 | ||
|
|
6d7e2bc7aa | ||
|
|
b1148679e3 | ||
|
|
6a9cd40eaf | ||
|
|
bd16709a81 | ||
|
|
cbd7f2fca4 | ||
|
|
89b821ebf1 | ||
|
|
3fc0f891d4 | ||
|
|
74a7835a53 | ||
|
|
051b7ab2bb | ||
|
|
38f71460f0 | ||
|
|
1ab1520c60 | ||
|
|
0e5caf591a | ||
|
|
ad2582a28e | ||
|
|
c9d89f493d | ||
|
|
8669e3f780 | ||
|
|
e7d5676cb5 | ||
|
|
800bffbf24 | ||
|
|
17c02c2a25 | ||
|
|
89db5e3b3f | ||
|
|
1524eb23d4 | ||
|
|
d7dd2eb611 | ||
|
|
a3e150fdd9 | ||
|
|
fa2868edd6 | ||
|
|
e3e62b62ad | ||
|
|
a071a65ecc | ||
|
|
65e033cc80 | ||
|
|
ffe63bbce0 | ||
|
|
75d8a2dd2b | ||
|
|
2c0c97d6cc | ||
|
|
61307bdf03 | ||
|
|
2ec8fb03aa | ||
|
|
f8611efdc7 | ||
|
|
ae06a768fe | ||
|
|
feed18c33e | ||
|
|
8340228637 | ||
|
|
c5117e8669 | ||
|
|
9fb8e17cf6 | ||
|
|
d5a1413799 | ||
|
|
a7f6658b85 | ||
|
|
1885859d2f | ||
|
|
988cf117c2 | ||
|
|
d4a6009b92 | ||
|
|
a187752659 | ||
|
|
050007ff1b | ||
|
|
1441804e70 | ||
|
|
990c293ac2 | ||
|
|
de1bda0034 | ||
|
|
949df2c71f | ||
|
|
012376d2aa | ||
|
|
88224b765a | ||
|
|
3cfa48835c | ||
|
|
54a6c9767c | ||
|
|
05c7a32532 | ||
|
|
c5a3562608 | ||
|
|
a23037ce70 | ||
|
|
f4469c8393 | ||
|
|
d27474e910 | ||
|
|
7399dc9939 | ||
|
|
352f028659 | ||
|
|
966749d172 | ||
|
|
653dbf736e | ||
|
|
80be2aac8d | ||
|
|
6c3d21a5f4 | ||
|
|
c8f5ec5120 | ||
|
|
e171154a79 | ||
|
|
5217eb411d | ||
|
|
3112dc7c95 | ||
|
|
3e23ef40aa | ||
|
|
1b1aaf1032 | ||
|
|
39707842b1 | ||
|
|
2a0ec3d47e | ||
|
|
3c2022315e | ||
|
|
9a63ff2809 | ||
|
|
ee3b0edcf5 | ||
|
|
940ff167bf | ||
|
|
9a300aaf21 | ||
|
|
4924e3eebe | ||
|
|
a49c7136cd | ||
|
|
bc433bde83 | ||
|
|
bc19205e0a | ||
|
|
10f802c4aa | ||
|
|
b5b8a864cd | ||
|
|
11be868d43 | ||
|
|
0c63920cdf | ||
|
|
882c2a36b3 | ||
|
|
8270d5c05d | ||
|
|
9f6c7a3e7b | ||
|
|
55231eaf08 | ||
|
|
1af4c237ad | ||
|
|
b5d2636cbf | ||
|
|
a4d9bf87ef | ||
|
|
697eb1f639 | ||
|
|
0c9b823273 | ||
|
|
06d45d6b29 | ||
|
|
37869e52b6 | ||
|
|
6169311561 | ||
|
|
3e58d46c98 | ||
|
|
48601a6508 | ||
|
|
5fc4d8495e | ||
|
|
3f45d588e3 | ||
|
|
7bfc16ae22 | ||
|
|
1d31c10625 | ||
|
|
00af2995e0 | ||
|
|
fd042460ee | ||
|
|
c3810de709 | ||
|
|
b39d705ea5 | ||
|
|
91a13a400c | ||
|
|
b9003f8280 | ||
|
|
c91315ea01 | ||
|
|
03f8f7d64f | ||
|
|
3a43996939 | ||
|
|
2b12ab1501 | ||
|
|
6221419af6 | ||
|
|
05735ef769 | ||
|
|
8e47906bbe | ||
|
|
53f0656353 | ||
|
|
8e50cb3e77 | ||
|
|
35fc839573 | ||
|
|
9039a2de58 | ||
|
|
8263d1c34e | ||
|
|
338ad07db7 | ||
|
|
311f7c0574 | ||
|
|
ca91918707 | ||
|
|
5bcfe0a85c | ||
|
|
6084e5bb30 | ||
|
|
1d5302f26c | ||
|
|
1221daa780 | ||
|
|
b7d79fcb5f | ||
|
|
b466579c22 | ||
|
|
ed8353664b | ||
|
|
25609d9fd4 | ||
|
|
cf3859b591 | ||
|
|
8be80fde71 | ||
|
|
336de41849 | ||
|
|
306236b6e3 | ||
|
|
416ef47fb1 | ||
|
|
c9ff083c0a | ||
|
|
66c22aa3b8 | ||
|
|
b2a942c179 | ||
|
|
e9cbe3cdcb | ||
|
|
0000db67c4 | ||
|
|
89827405d7 | ||
|
|
e6914ac018 | ||
|
|
e162120943 | ||
|
|
333fb2aa7e | ||
|
|
84ee08e75c | ||
|
|
f682d12bd0 | ||
|
|
10adef15a8 | ||
|
|
fc769349d5 | ||
|
|
5152a9054b | ||
|
|
783d830c68 | ||
|
|
c25dfabdbf | ||
|
|
c7e94db3d2 | ||
|
|
2f0be7a8a6 | ||
|
|
22e55b142c | ||
|
|
6dda2f5917 | ||
|
|
838c86d1f8 | ||
|
|
a1a9e712b8 | ||
|
|
35469825c7 | ||
|
|
f565bc41d7 | ||
|
|
9446f09f22 | ||
|
|
925158b149 | ||
|
|
6f1c1b3629 | ||
|
|
cb6e71c8bf | ||
|
|
17258ea4d7 | ||
|
|
035d33fcea | ||
|
|
de46f1b159 | ||
|
|
4b4d133e8b | ||
|
|
5a1cdc73c5 | ||
|
|
c5fe4f5325 | ||
|
|
e2fa605db6 | ||
|
|
55580bc51c | ||
|
|
fe78f97f6a | ||
|
|
9d46e6f460 | ||
|
|
21f9f6f476 | ||
|
|
2ba8eb9430 | ||
|
|
6ea194dd0e | ||
|
|
1d3d4f7455 | ||
|
|
fdb79d4a6d | ||
|
|
89fce2010c | ||
|
|
fb878824e8 | ||
|
|
aff5af4771 | ||
|
|
dd06c93935 | ||
|
|
d9e664c440 | ||
|
|
0a492a169e | ||
|
|
6ae71adbca | ||
|
|
cbaa098005 | ||
|
|
741e82a425 | ||
|
|
c694b57e08 | ||
|
|
2c7bc2d668 | ||
|
|
765f83a2ae | ||
|
|
58f6607404 | ||
|
|
9b27783886 | ||
|
|
c6de087615 | ||
|
|
86f7b54b87 | ||
|
|
defe305ad7 | ||
|
|
cc210a3597 | ||
|
|
f7c7bb293b | ||
|
|
ab1db27dc7 | ||
|
|
71a86fa9aa | ||
|
|
fcc20d2f1e | ||
|
|
8922142ade | ||
|
|
eb28fe0120 | ||
|
|
d73a755cf2 | ||
|
|
7a648a77b0 | ||
|
|
c721b8b1da | ||
|
|
ce62671276 | ||
|
|
ba4cd1b0c6 | ||
|
|
a18ee3d6e5 | ||
|
|
15fa833d8e | ||
|
|
06edbeb903 | ||
|
|
082dc9cc82 | ||
|
|
0b3286cd1e | ||
|
|
f17fb4487e | ||
|
|
339162d595 | ||
|
|
da6fefb1b0 | ||
|
|
8d3ed24d65 | ||
|
|
ba219af1ac | ||
|
|
4218beac9d | ||
|
|
d5d71c1c82 | ||
|
|
348b3a7c65 | ||
|
|
f636b56ee3 | ||
|
|
ed81ceaa94 | ||
|
|
7a78c77b6a | ||
|
|
337cd259a6 | ||
|
|
e7781517aa | ||
|
|
ce3411e956 | ||
|
|
a58ca2c419 | ||
|
|
608d855cae | ||
|
|
eb529be286 | ||
|
|
68dbe97442 | ||
|
|
5374410c67 | ||
|
|
cbb697216c | ||
|
|
25a11bde59 | ||
|
|
62bffc33fd | ||
|
|
45f384d048 | ||
|
|
b9d92ecefa | ||
|
|
01fd8bee0c | ||
|
|
9048d253e6 | ||
|
|
16b660ad0e | ||
|
|
2273768556 | ||
|
|
ab01554b82 | ||
|
|
cce12fb138 | ||
|
|
6f4c1a0fbd | ||
|
|
3663ca1ec1 | ||
|
|
f39bf0b043 | ||
|
|
f55d89b17e | ||
|
|
f6185a3dc5 | ||
|
|
be7bd38341 | ||
|
|
b3277e2367 | ||
|
|
f560bb6fbf | ||
|
|
013138afb8 | ||
|
|
d6d639d059 | ||
|
|
31b2d66b9b | ||
|
|
4b278d68e6 | ||
|
|
e81687bf53 | ||
|
|
a0ea6d6e1c | ||
|
|
17eb9a4c45 | ||
|
|
2e8ad58979 | ||
|
|
680d43f78c | ||
|
|
0d1fb26b52 | ||
|
|
7540430e1d | ||
|
|
095ec1ed04 | ||
|
|
ce62a7b1fc | ||
|
|
4b89775f8a | ||
|
|
a5af23e330 | ||
|
|
46cc0910bf | ||
|
|
37e0fdb0a7 | ||
|
|
a3e7e76931 | ||
|
|
cfdab8ddcd | ||
|
|
389924a544 | ||
|
|
b3634a0762 | ||
|
|
985bc1361a | ||
|
|
d3d3a6af09 | ||
|
|
3883160234 | ||
|
|
d04ad16aee | ||
|
|
0067418ffb | ||
|
|
deb900f8f8 | ||
|
|
e6f6ed75f1 | ||
|
|
6226c3b0c8 | ||
|
|
1a525d6e5d | ||
|
|
bc213fe3c9 | ||
|
|
731666936c | ||
|
|
67ec7e92b5 | ||
|
|
40bfcb07b6 | ||
|
|
e5f09a7aef | ||
|
|
341bdb0eda | ||
|
|
27d68f2774 | ||
|
|
d259cd8eb5 | ||
|
|
c9e5ded43a | ||
|
|
a8e03e22bf | ||
|
|
7094016e7e | ||
|
|
1bfa1f3981 | ||
|
|
655745e049 | ||
|
|
f83a22b310 | ||
|
|
025c24a9d9 | ||
|
|
c8c3e83e7b | ||
|
|
7d8327af18 | ||
|
|
58bab2f538 | ||
|
|
cf93eda6c1 | ||
|
|
a3b7af0f79 | ||
|
|
91bf5e5555 | ||
|
|
61a72b330d | ||
|
|
6a1164363c | ||
|
|
df9a82482c | ||
|
|
778d98c7f2 | ||
|
|
cae759a09e | ||
|
|
52126b2248 | ||
|
|
5782521824 | ||
|
|
3522dc529d | ||
|
|
8602e68b60 | ||
|
|
fb4514d70e | ||
|
|
35d86bff11 | ||
|
|
193730e16c | ||
|
|
ed32186984 | ||
|
|
eb449740eb | ||
|
|
327fd43dd6 | ||
|
|
bc2345f415 | ||
|
|
fd1bdc79f8 | ||
|
|
22a2419185 | ||
|
|
de921bf786 | ||
|
|
3cf6b22d95 | ||
|
|
82ce45b8e4 | ||
|
|
dfe95a646e | ||
|
|
7b0b206905 | ||
|
|
1b1984064c | ||
|
|
7e03e2571f | ||
|
|
23f0b32634 | ||
|
|
f844062980 | ||
|
|
84f982b20e | ||
|
|
c6739487a4 | ||
|
|
be0f46371e | ||
|
|
85351d5b1c | ||
|
|
23366e4536 | ||
|
|
0d1f69d36e | ||
|
|
d77db45f37 | ||
|
|
20c5b6c4a5 | ||
|
|
62f512e319 | ||
|
|
e740ddb7cc | ||
|
|
68924d6f35 | ||
|
|
94db512518 | ||
|
|
f39e9889f3 | ||
|
|
b0ffbcd07d | ||
|
|
19412cc963 | ||
|
|
45c7dbb3f7 | ||
|
|
3b0d0e55dc | ||
|
|
ae21ebe80d | ||
|
|
cca14765ce | ||
|
|
725718d18c | ||
|
|
61b000dc48 | ||
|
|
cf850071e5 | ||
|
|
fe473e70a3 | ||
|
|
ce6540a6f3 | ||
|
|
ac3e60053a |
@@ -6,3 +6,6 @@ cbdfa67d075ec1f8ceec0f3559498cbca01783a0
|
||||
|
||||
# Renamed Renovate config file
|
||||
f304304bc5b64edf189aaa473b05df912067a8bd
|
||||
|
||||
# Refactored database module into core and impl
|
||||
7b0b206905b8dc1f824a2d2a10e3d94c6b9dd11c
|
||||
|
||||
|
Before Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 261 KiB |
@@ -22,6 +22,7 @@
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"androidx.compose.compiler:compiler",
|
||||
"com.google.devtools.ksp",
|
||||
"dev.drewhamilton.poko",
|
||||
"org.jetbrains.kotlin:kotlin-gradle-plugin",
|
||||
"org.jetbrains.kotlin.plugin.serialization",
|
||||
@@ -32,21 +33,26 @@
|
||||
"matchPackagePatterns": [
|
||||
"^androidx.*",
|
||||
],
|
||||
"groupName": "androidx"
|
||||
"groupName": "AndroidX"
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^io.sentry",
|
||||
"^sentry",
|
||||
],
|
||||
"groupName": "sentry"
|
||||
"groupName": "Sentry"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"com.android.tools:r8",
|
||||
"com.squareup.anvil",
|
||||
"matchPackagePatterns": [
|
||||
"^com.squareup.anvil"
|
||||
],
|
||||
"enabled": false,
|
||||
"allowedVersions": "!/.*-1-8$/",
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^androidx.glance",
|
||||
],
|
||||
"allowedVersions": "<=1.0.0",
|
||||
},
|
||||
],
|
||||
"regexManagers": [
|
||||
|
||||
@@ -12,87 +12,58 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Run unit tests
|
||||
uses: gradle/gradle-build-action@ef76a971e2fa3f867b617efd72f2fbd72cf6f8bc # v2.8.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||
with:
|
||||
arguments: --no-configuration-cache --stacktrace check -PslimTests
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
gradle-home-cache-cleanup: true
|
||||
add-job-summary: always
|
||||
dependency-graph: generate-and-submit
|
||||
|
||||
- name: Check changelog format
|
||||
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2.2.3
|
||||
with:
|
||||
path: ./CHANGELOG.md
|
||||
validation_level: error
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew --no-configuration-cache --stacktrace check -PslimTests
|
||||
|
||||
- name: (Fail-only) Upload test report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: Test report
|
||||
path: android/build/reports
|
||||
path: '**/build/reports/tests/**'
|
||||
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-14
|
||||
if: "github.event_name == 'push' && github.event.ref != 'refs/heads/main'"
|
||||
needs:
|
||||
- check
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 # v3
|
||||
uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # v4
|
||||
with:
|
||||
base-ref: refs/heads/main
|
||||
head-ref: ${{ github.ref }}
|
||||
|
||||
deploy-release-snapshot:
|
||||
runs-on: ubuntu-latest
|
||||
if: "github.event_name == 'push' && github.event.ref == 'refs/heads/main'"
|
||||
needs:
|
||||
- check
|
||||
- dependency-review
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
env:
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
|
||||
- name: Build release app
|
||||
uses: gradle/gradle-build-action@ef76a971e2fa3f867b617efd72f2fbd72cf6f8bc # v2.8.0
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
with:
|
||||
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
|
||||
|
||||
- name: Deploy snapshot
|
||||
run: scripts/deploy-snapshot.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Code quality analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- renovate/**
|
||||
schedule:
|
||||
- cron: '31 7 * * 3'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
name: CodeQL
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||
with:
|
||||
cache-read-only: true
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@05963f47d870e2cb19a537396c1f668a348c7d8f # v3.24.8
|
||||
with:
|
||||
languages: java
|
||||
tools: latest
|
||||
queries: +security-extended
|
||||
|
||||
- name: Build project
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew assembleDebug assembleInternal
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@05963f47d870e2cb19a537396c1f668a348c7d8f # v3.24.8
|
||||
with:
|
||||
category: "/language:java"
|
||||
mobsfscan:
|
||||
name: MobSF
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Run mobsfscan
|
||||
uses: MobSF/mobsfscan@e29e85c36fa1aa950d7f1b1064345d94a70b2a28
|
||||
with:
|
||||
args: . --sarif --output results.sarif || true
|
||||
|
||||
- name: Upload mobsfscan report
|
||||
uses: github/codeql-action/upload-sarif@05963f47d870e2cb19a537396c1f668a348c7d8f # v3.24.8
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -1,53 +0,0 @@
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- renovate/**
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '31 7 * * 3'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@ef76a971e2fa3f867b617efd72f2fbd72cf6f8bc # v2.8.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
|
||||
with:
|
||||
languages: java
|
||||
tools: latest
|
||||
queries: +security-extended
|
||||
|
||||
- name: Build project
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew assemble -PslimTests
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
|
||||
with:
|
||||
category: "/language:java"
|
||||
@@ -13,32 +13,32 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
publish-google-play-release:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- 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@ef76a971e2fa3f867b617efd72f2fbd72cf6f8bc # v2.8.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
echo VERSION="${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Update changelog
|
||||
uses: msfjarvis/keep-a-changelog-new-release@bf43ae42f4e42f137572950af426efcf6e777207 # 2.1.0
|
||||
uses: msfjarvis/keep-a-changelog-new-release@78cddbe3549f1c9263a2d754446d3765e865c518 # v2.1.1
|
||||
with:
|
||||
tag: v${{ env.VERSION }}
|
||||
version: ${{ env.VERSION }}
|
||||
@@ -70,14 +70,14 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: |
|
||||
./gradlew --no-configuration-cache collectReleaseApks collectReleaseBundle -PsentryUploadMappings
|
||||
./gradlew --no-configuration-cache collectReleaseApks collectReleaseBundle -PenableSentry
|
||||
|
||||
- name: Clean secrets
|
||||
run: scripts/signing-cleanup.sh
|
||||
|
||||
- name: Get changelog entry
|
||||
id: changelog_reader
|
||||
uses: mindsers/changelog-reader-action@b97ce03a10d9bdbb07beb491c76a5a01d78cd3ef # v2.2.2
|
||||
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2.2.3
|
||||
with:
|
||||
version: ${{ env.VERSION }}
|
||||
path: ./CHANGELOG.md
|
||||
@@ -88,11 +88,10 @@ jobs:
|
||||
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@9cf8fd30c9e616ec761db3b9abb2b0f15842ce58 # v1.1.2
|
||||
uses: r0adkll/upload-google-play@935ef9c68bb393a8e6116b1575626a7f5be3a7fb # v1.1.3
|
||||
with:
|
||||
mappingFile: android/bundle/mapping.txt
|
||||
packageName: dev.msfjarvis.claw.android
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Android release build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
versionName:
|
||||
description: 'User-facing release version name'
|
||||
required: true
|
||||
default: ""
|
||||
versionCode:
|
||||
description: 'versionCode or build number'
|
||||
required: true
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
signed-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||
with:
|
||||
cache-read-only: true
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- name: Build release app
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
./gradlew --no-configuration-cache --stacktrace -PenableSentry collectReleaseBundle
|
||||
./scripts/signing-cleanup.sh
|
||||
env:
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
ORG_GRADLE_PROJECT_VERSION_NAME: ${{ github.event.inputs.versionName }}
|
||||
ORG_GRADLE_PROJECT_VERSION_CODE: ${{ github.event.inputs.versionCode }}
|
||||
|
||||
- name: Upload app bundle
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: release-aab
|
||||
path: android/build/outputs/bundle/release/android-release.aab
|
||||
@@ -1,3 +1,4 @@
|
||||
*.json
|
||||
baseline-prof.txt
|
||||
startup-prof.txt
|
||||
**/test/resources/**
|
||||
@@ -7,6 +7,87 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.44.0] - 2024-03-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed a bug in the database code that was triggering crashes for
|
||||
a subset of upgrading users
|
||||
|
||||
## [1.43.0] - 2024-03-17
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix crash when trying to open a comments-only post from
|
||||
the home screen widget
|
||||
|
||||
## [1.42.0] - 2024-03-16
|
||||
|
||||
### Fixed
|
||||
|
||||
* Adapt to changes in lobste.rs API
|
||||
|
||||
## [1.41.0] - 2024-03-07
|
||||
|
||||
### Fixed
|
||||
|
||||
* Downgrade dependency that was pulling in extraneous storage
|
||||
permissions which the app does not require.
|
||||
|
||||
## [1.40.0] - 2024-03-07
|
||||
|
||||
### Changed
|
||||
|
||||
* Update to Compose March releases
|
||||
* Improve performance of frequently invoked database queries
|
||||
* Refactor UI data model to improve state handling
|
||||
* Make saved posts update in the background more often
|
||||
* Consolidate widget display logic
|
||||
* Rework some theming logic to align with new Material guidelines
|
||||
|
||||
## [1.39.0] - 2024-01-23
|
||||
|
||||
### Changed
|
||||
|
||||
* Add HTML bookmarks as an export format
|
||||
* Rework widget item layout for consistent touch targets
|
||||
* Add attribution for libraries used in the app
|
||||
* Redesign settings screen
|
||||
* Update to Compose December releases
|
||||
|
||||
## [1.38.0] - 2023-11-20
|
||||
|
||||
### Changed
|
||||
|
||||
* Add brand new icon by dzuk
|
||||
* Fix a bug that caused the app to fetch the same 20 posts over and over
|
||||
* Significantly improve scrolling performance when there are a lot of saved posts
|
||||
* Move search feature to its own screen
|
||||
* Make top app and bottom system bars use the same color
|
||||
* Reduce unnecessary API calls in search screen
|
||||
* Upgrade to Compose November releases
|
||||
* Enable logging of SQLite queries
|
||||
|
||||
## [1.37.0] - 2023-10-06
|
||||
|
||||
### Changed
|
||||
|
||||
* Fixed a crash that infrequently happened in the comments page
|
||||
* Fixed a crash when trying to list saved posts
|
||||
* Fixed a crash in home screen widget when user had less than 50 saved posts
|
||||
* Upgraded to Compose 1.6.0-alpha07
|
||||
|
||||
## [1.36.0] - 2023-10-04
|
||||
|
||||
### Added
|
||||
|
||||
* Introduce an initial attempt at a home screen widget
|
||||
|
||||
### Changed
|
||||
|
||||
* Fixed a crash triggered when swiping items
|
||||
* Adjusted pull to refresh component to match Material You theme
|
||||
|
||||
## [1.35.0] - 2023-09-19
|
||||
|
||||
### Changed
|
||||
@@ -301,7 +382,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
* Initial Play Store release
|
||||
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.35.0...HEAD
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.44.0...HEAD
|
||||
|
||||
[1.44.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.43.0...v1.44.0
|
||||
|
||||
[1.43.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.42.0...v1.43.0
|
||||
|
||||
[1.42.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.41.0...v1.42.0
|
||||
|
||||
[1.41.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.40.0...v1.41.0
|
||||
|
||||
[1.40.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.39.0...v1.40.0
|
||||
|
||||
[1.39.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.38.0...v1.39.0
|
||||
|
||||
[1.38.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.37.0...v1.38.0
|
||||
|
||||
[1.37.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.36.0...v1.37.0
|
||||
|
||||
[1.36.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.35.0...v1.36.0
|
||||
|
||||
[1.35.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.34.0...v1.35.0
|
||||
|
||||
|
||||
@@ -8,36 +8,17 @@ Unofficial Android app for read-only access to [lobste.rs](https://lobste.rs), b
|
||||
height="80" />
|
||||
</a>
|
||||
|
||||
> Snapshots from the development branch can be obtained [here](https://github.com/msfjarvis/compose-lobsters/releases/tag/nightly).
|
||||
> [!NOTE]
|
||||
> Daily builds are published to the Google Play 'Open Testing' channel (if there are any changes), which can be opted into via this link: [[Android]](https://play.google.com/store/apps/details?id=dev.msfjarvis.claw.android) [[Web]](https://play.google.com/apps/testing/dev.msfjarvis.claw.android)
|
||||
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature.webp"
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature_light.webp#gh-light-mode-only"
|
||||
alt="A grid of screenshots from the app, in clockwise order: the main screen, the comments page, the search page and the saved posts page"
|
||||
width="550" />
|
||||
|
||||
## Dependency Diagram
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature_dark.webp#gh-dark-mode-only"
|
||||
alt="A grid of screenshots from the app, in clockwise order: the main screen, the comments page, the search page and the saved posts page"
|
||||
width="550" />
|
||||
|
||||
```mermaid
|
||||
%%{
|
||||
init: {
|
||||
'theme': 'dark'
|
||||
}
|
||||
}%%
|
||||
|
||||
graph TB
|
||||
|
||||
benchmark --> android
|
||||
android --> api
|
||||
android --> common
|
||||
android --> core
|
||||
android --> database
|
||||
android --> model
|
||||
android --> benchmark
|
||||
api --> model
|
||||
common --> core
|
||||
common --> database
|
||||
common --> model
|
||||
|
||||
```
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -15,6 +15,7 @@ plugins {
|
||||
id("dev.msfjarvis.claw.kotlin-kapt")
|
||||
id("dev.msfjarvis.claw.sentry")
|
||||
id("dev.msfjarvis.claw.versioning-plugin")
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.modulegraphassert)
|
||||
alias(libs.plugins.whetstone)
|
||||
@@ -31,8 +32,11 @@ android {
|
||||
useLiveLiterals = false
|
||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
}
|
||||
// kotlinx.serialization 1.6.0-RC regression
|
||||
packagingOptions.resources.pickFirsts += "**/*.bin"
|
||||
buildTypes.create("internal") {
|
||||
matchingFallbacks += "release"
|
||||
signingConfig = signingConfigs["debug"]
|
||||
applicationIdSuffix = ".internal"
|
||||
}
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
@@ -44,6 +48,7 @@ baselineProfile {
|
||||
licensee {
|
||||
allow("Apache-2.0")
|
||||
allow("MIT")
|
||||
ignoreDependencies("com.michael-bull.kotlin-result") { because("kotlin-result is ISC licensed") }
|
||||
ignoreDependencies("org.commonmark") { because("Commonmark is BSD licensed") }
|
||||
allowUrl("https://jsoup.org/license") { because("Jsoup is MIT licensed") }
|
||||
}
|
||||
@@ -64,13 +69,14 @@ whetstone {
|
||||
dependencies {
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.aboutLibraries.m3)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.material)
|
||||
implementation(libs.androidx.compose.glance)
|
||||
implementation(libs.androidx.compose.glance.m3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material3.window.size)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.lifecycle.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
@@ -82,6 +88,7 @@ dependencies {
|
||||
implementation(libs.jsoup)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.material3.pulltorefresh)
|
||||
implementation(libs.napier)
|
||||
implementation(libs.sqldelight.extensions.coroutines)
|
||||
implementation(libs.swipe)
|
||||
@@ -89,8 +96,9 @@ dependencies {
|
||||
implementation(projects.api)
|
||||
implementation(projects.common)
|
||||
implementation(projects.core)
|
||||
implementation(projects.database)
|
||||
implementation(projects.database.impl)
|
||||
implementation(projects.model)
|
||||
implementation(projects.web)
|
||||
|
||||
kapt(libs.dagger.compiler)
|
||||
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
<issue
|
||||
id="VectorPath"
|
||||
message="Very long vector path (918 characters), which is bad for performance. Considering reducing precision, removing minor details or rasterizing vector."
|
||||
errorLine1=" android:pathData="M431.3,341.2C435.9,337 580.3,236 710.5,223.8C747.3,220.3 790.7,254.7 833.2,312.5C875.9,370.7 892.9,433.4 875.9,443C863.2,450.2 838.6,407.4 758.8,396.3C743.4,394.2 730.4,393.4 719.4,393.5C738.5,423.4 745.6,451.1 737.2,458C733.1,461.4 673.6,438.7 612.9,436.5C570.1,435 535.2,440.9 506.7,451.3C496.9,454.9 486,453.8 477.2,448.3C444.5,427.9 424.2,406.2 418.4,376.2C415.8,362.8 420.9,349.4 431.3,341.2ZM274.7,394.8C285.8,370.1 312.2,325.9 346.3,305.4C383.6,282.9 335,390.3 427.3,489.8C519.5,589.3 586.5,623.8 655.1,581.5C723.7,539.2 746.5,518.1 753,518.7C764,519.6 772.9,536.1 764.4,589.5C763,598.2 761.3,606.5 759.3,614.2C828.7,575 871.7,516.3 889,520C912,524.8 918.3,617.8 852.1,721C811.8,784 768.8,830.7 741.4,857.5C724.3,874 699.7,880 677,873.1C512,822.6 315.5,733.8 202.9,652C193.8,645.2 186.8,635.9 183,625.1C171.8,593.7 161.3,548.1 168.5,495.3C179.5,414.2 222.7,352.9 245.2,335.9C261.9,323.2 267.1,362.4 274.7,394.8Z""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_launcher_foreground.xml"
|
||||
line="17"
|
||||
column="27"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -1,22 +1,2 @@
|
||||
-dontobfuscate
|
||||
-keepattributes SourceFile, LineNumberTable
|
||||
|
||||
# From https://github.com/square/retrofit/blob/master/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro
|
||||
|
||||
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
|
||||
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
|
||||
-if interface * { @retrofit2.http.* <methods>; }
|
||||
-keep,allowobfuscation interface <1>
|
||||
|
||||
# Keep inherited services.
|
||||
-if interface * { @retrofit2.http.* <methods>; }
|
||||
-keep,allowobfuscation interface * extends <1>
|
||||
|
||||
# With R8 full mode generic signatures are stripped for classes that are not
|
||||
# kept. Suspend functions are wrapped in continuations where the type argument
|
||||
# is used.
|
||||
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
||||
|
||||
# R8 full mode strips generic signatures from return types if not kept.
|
||||
-if interface * { @retrofit2.http.* public *** *(...); }
|
||||
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<!--
|
||||
~ Copyright © 2021-2024 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">Claw 🤫</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Copyright © 2021-2024 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.
|
||||
@@ -25,6 +25,8 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="colorMode|density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -132,6 +134,19 @@
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".SearchActivity"
|
||||
android:configChanges="colorMode|density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
|
||||
android:exported="true"
|
||||
android:label="Claw Search"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/Theme.Claw">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
@@ -146,23 +161,36 @@
|
||||
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:name=".glance.WidgetReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION" />
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/saved_posts_widget_info" />
|
||||
</receiver>
|
||||
<!-- Required: set your sentry.io project identifier (DSN) -->
|
||||
<meta-data android:name="io.sentry.dsn" android:value="${sentryDsn}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.dsn"
|
||||
android:value="${sentryDsn}" />
|
||||
<!-- enable automatic breadcrumbs for user interactions (clicks, swipes, scrolls) -->
|
||||
<meta-data android:name="io.sentry.traces.user-interaction.enable" android:value="${enableSentry}" />
|
||||
<meta-data android:name="io.sentry.breadcrumbs.user-interaction" android:value="${enableSentry}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.traces.user-interaction.enable"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="io.sentry.breadcrumbs.user-interaction"
|
||||
android:value="true" />
|
||||
<!-- enable screenshot for crashes -->
|
||||
<meta-data android:name="io.sentry.attach-screenshot" android:value="${enableSentry}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-screenshot"
|
||||
android:value="true" />
|
||||
<!-- enable view hierarchy for crashes -->
|
||||
<meta-data android:name="io.sentry.attach-view-hierarchy" android:value="${enableSentry}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-view-hierarchy"
|
||||
android:value="true" />
|
||||
<!-- set the environment for this build -->
|
||||
<meta-data android:name="io.sentry.environment" android:value="${sentryEnvironment}" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright © 2021-2024 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
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import javax.inject.Inject
|
||||
|
||||
/** A base class that encapsulates all activities used by Claw. */
|
||||
@Stable
|
||||
abstract class BaseActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var urlLauncher: UrlLauncher
|
||||
@Inject lateinit var htmlConverter: HTMLConverter
|
||||
@Inject lateinit var viewModel: ClawViewModel
|
||||
var webUri: String? = null
|
||||
|
||||
/** Entrypoint to show a [Composable] as this [ComponentActivity]'s view. */
|
||||
@Composable abstract fun Content()
|
||||
|
||||
/** Overrideable method to call stuff in [onCreate]. */
|
||||
open fun preLaunch() {}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
preLaunch()
|
||||
Whetstone.inject(this)
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||
navigationBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||
)
|
||||
setContent {
|
||||
LobstersTheme(
|
||||
dynamicColor = true,
|
||||
providedValues = arrayOf(LocalUriHandler provides urlLauncher),
|
||||
) {
|
||||
Content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent?) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
if (outContent != null) {
|
||||
if (webUri != null) {
|
||||
outContent.webUri = Uri.parse(webUri)
|
||||
} else {
|
||||
outContent.webUri = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -34,19 +34,12 @@ class ClawApplication : Application(), ApplicationComponentOwner {
|
||||
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()
|
||||
)
|
||||
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
.build()
|
||||
WorkManager.getInstance(this)
|
||||
.enqueueUniquePeriodicWork(
|
||||
"updateSavedPosts",
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
postUpdateWorkRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,66 +1,39 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
|
||||
import dev.msfjarvis.claw.android.ui.LobstersApp
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import javax.inject.Inject
|
||||
import dev.msfjarvis.claw.android.ui.screens.LobstersPostsScreen
|
||||
|
||||
@ContributesActivityInjector
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var urlLauncher: UrlLauncher
|
||||
@Inject lateinit var htmlConverter: HTMLConverter
|
||||
private var webUri: String? = null
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
installSplashScreen()
|
||||
Whetstone.inject(this)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
val windowSizeClass = calculateWindowSizeClass(this)
|
||||
LobstersTheme(
|
||||
dynamicColor = true,
|
||||
providedValues = arrayOf(LocalUriHandler provides urlLauncher),
|
||||
) {
|
||||
LobstersApp(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
windowSizeClass = windowSizeClass,
|
||||
setWebUri = { url -> webUri = url },
|
||||
)
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val windowSizeClass = calculateWindowSizeClass(this)
|
||||
LobstersPostsScreen(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
windowSizeClass = windowSizeClass,
|
||||
setWebUri = { url -> webUri = url },
|
||||
)
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent?) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
if (outContent != null) {
|
||||
if (webUri != null) {
|
||||
outContent.webUri = Uri.parse(webUri)
|
||||
} else {
|
||||
outContent.webUri = null
|
||||
}
|
||||
}
|
||||
override fun preLaunch() {
|
||||
super.preLaunch()
|
||||
installSplashScreen()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NAVIGATION_KEY = "postId"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright © 2021-2024 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 androidx.compose.runtime.Composable
|
||||
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
|
||||
import dev.msfjarvis.claw.android.ui.screens.SearchScreen
|
||||
|
||||
@ContributesActivityInjector
|
||||
class SearchActivity : BaseActivity() {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
SearchScreen(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
setWebUri = { webUri = it },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.appWidgetBackground
|
||||
import androidx.glance.appwidget.lazy.LazyColumn
|
||||
import androidx.glance.appwidget.lazy.items
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.background
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.material3.ColorProviders
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import dev.msfjarvis.claw.common.theme.DarkThemeColors
|
||||
import dev.msfjarvis.claw.common.theme.LightThemeColors
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class SavedPostsWidget(private val posts: Flow<List<UIPost>>) : GlanceAppWidget() {
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val postWindow = posts.first().take(50).toImmutableList()
|
||||
provideContent {
|
||||
GlanceTheme(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) GlanceTheme.colors
|
||||
else ColorProviders(light = LightThemeColors, dark = DarkThemeColors)
|
||||
) {
|
||||
WidgetHost(postWindow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WidgetHost(posts: ImmutableList<UIPost>, modifier: GlanceModifier = GlanceModifier) {
|
||||
LazyColumn(
|
||||
modifier =
|
||||
modifier.fillMaxSize().background(GlanceTheme.colors.background).appWidgetBackground(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
item {
|
||||
val style = MaterialTheme.typography.titleLarge
|
||||
Text(
|
||||
text = "Saved posts",
|
||||
style =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onBackground,
|
||||
fontSize = style.fontSize,
|
||||
fontWeight = style.fontWeight.toGlance(),
|
||||
fontStyle = style.fontStyle.toGlance(),
|
||||
),
|
||||
modifier = GlanceModifier.padding(vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
items(posts) { post -> WidgetListEntry(post = post) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceComposable
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.action.ActionParameters.Key
|
||||
import androidx.glance.action.actionParametersOf
|
||||
import androidx.glance.action.actionStartActivity
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.action.actionStartActivity
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.background
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.MainActivity.Companion.NAVIGATION_KEY
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
private val destinationKey = Key<String>(NAVIGATION_KEY)
|
||||
|
||||
@Composable
|
||||
@GlanceComposable
|
||||
fun WidgetListEntry(post: UIPost, modifier: GlanceModifier = GlanceModifier) {
|
||||
val titleStyle = MaterialTheme.typography.titleMedium
|
||||
val commentsAction =
|
||||
actionStartActivity<MainActivity>(actionParametersOf(destinationKey to post.shortId))
|
||||
// If the URL starts with a /, it's a relative URL and we should open the comments page directly.
|
||||
val postAction =
|
||||
if (post.url.startsWith('/')) commentsAction
|
||||
else actionStartActivity(Intent(Intent.ACTION_VIEW, Uri.parse(post.url)))
|
||||
Box(modifier.padding(8.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
GlanceModifier.fillMaxWidth()
|
||||
.background(GlanceTheme.colors.surfaceVariant)
|
||||
.cornerRadius(4.dp)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = post.title,
|
||||
modifier = GlanceModifier.defaultWeight().padding(start = 4.dp).clickable(postAction),
|
||||
style =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onSurfaceVariant,
|
||||
fontSize = titleStyle.fontSize,
|
||||
fontWeight = titleStyle.fontWeight.toGlance(),
|
||||
fontStyle = titleStyle.fontStyle.toGlance(),
|
||||
),
|
||||
)
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.ic_comment),
|
||||
contentDescription = "${post.commentCount} comments",
|
||||
modifier = GlanceModifier.padding(end = 4.dp).clickable(commentsAction),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.broadcastreceiver.ContributesBroadcastReceiverInjector
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBroadcastReceiverInjector
|
||||
class WidgetReceiver : GlanceAppWidgetReceiver() {
|
||||
|
||||
@Inject lateinit var viewModel: ClawViewModel
|
||||
|
||||
override val glanceAppWidget: GlanceAppWidget
|
||||
get() = SavedPostsWidget(viewModel.savedPosts)
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Whetstone.inject(this, context)
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright © 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.glance
|
||||
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
fun FontWeight?.toGlance(): androidx.glance.text.FontWeight? {
|
||||
return when (this) {
|
||||
FontWeight.Normal -> androidx.glance.text.FontWeight.Normal
|
||||
FontWeight.Medium -> androidx.glance.text.FontWeight.Medium
|
||||
FontWeight.Bold -> androidx.glance.text.FontWeight.Bold
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun FontStyle?.toGlance(): androidx.glance.text.FontStyle? {
|
||||
return when (this) {
|
||||
FontStyle.Normal -> androidx.glance.text.FontStyle.Normal
|
||||
FontStyle.Italic -> androidx.glance.text.FontStyle.Italic
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright © 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.injection
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import com.squareup.anvil.annotations.optional.ForScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dev.msfjarvis.claw.core.coroutines.DispatcherProvider
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
class DataStoreModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideLoginDatastore(
|
||||
@ForScope(ApplicationScope::class) context: Context,
|
||||
dispatcherProvider: DispatcherProvider,
|
||||
): DataStore<Preferences> {
|
||||
return PreferenceDataStoreFactory.create(
|
||||
corruptionHandler = null,
|
||||
migrations = emptyList(),
|
||||
scope = CoroutineScope(dispatcherProvider.io() + SupervisorJob()),
|
||||
) {
|
||||
context.preferencesDataStoreFile("login")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -32,13 +32,7 @@ object MetadataExtractorModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideUnfurler(
|
||||
okHttpClient: OkHttpClient,
|
||||
logger: UnfurlLogger,
|
||||
): Unfurler {
|
||||
return Unfurler(
|
||||
httpClient = okHttpClient,
|
||||
logger = logger,
|
||||
)
|
||||
fun provideUnfurler(okHttpClient: OkHttpClient, logger: UnfurlLogger): Unfurler {
|
||||
return Unfurler(httpClient = okHttpClient, logger = logger)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -17,18 +17,14 @@ import dev.msfjarvis.claw.core.injection.AppPlugin
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesMultibinding(ApplicationScope::class)
|
||||
class WorkManagerPlugin
|
||||
@Inject
|
||||
constructor(
|
||||
private val workerFactory: WorkerFactory,
|
||||
) : AppPlugin {
|
||||
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()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -13,8 +13,12 @@ import com.slack.eithernet.ApiResult.Success
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dev.msfjarvis.claw.android.viewmodel.ReadPostsRepository
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.toUIPost
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -24,15 +28,25 @@ class LobstersPagingSource
|
||||
constructor(
|
||||
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : PagingSource<Int, LobstersPost>() {
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val readPostsRepository: ReadPostsRepository,
|
||||
) : PagingSource<Int, UIPost>() {
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UIPost> {
|
||||
val page = params.key ?: STARTING_PAGE_INDEX
|
||||
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
|
||||
is Success ->
|
||||
LoadResult.Page(
|
||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||
data = result.value,
|
||||
data =
|
||||
result.value.map {
|
||||
it
|
||||
.toUIPost()
|
||||
.copy(
|
||||
isSaved = savedPostsRepository.isPostSaved(it.shortId),
|
||||
isRead = readPostsRepository.isPostRead(it.shortId),
|
||||
)
|
||||
},
|
||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||
nextKey = page + 1,
|
||||
)
|
||||
@@ -43,7 +57,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int? {
|
||||
override fun getRefreshKey(state: PagingState<Int, UIPost>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -14,40 +14,61 @@ import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
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.viewmodel.ReadPostsRepository
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.toUIPost
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Duplicate of [LobstersPagingSource] with an additional optimization to not continue loading
|
||||
* further pages if nothing is found in the current one. This is required to prevent the app from
|
||||
* hammering the search endpoint with up to 70 consecutive requests for the search results of a
|
||||
* blank query.
|
||||
* Specialized variant of [LobstersPagingSource] to prevent hammering the Lobsters search endpoint
|
||||
* with unnecessary requests. Rather than abstract out the API call this paging source takes in the
|
||||
* relevant parameters to short-circuit when there is no query specified in order to avoid calling
|
||||
* the API at all.
|
||||
*/
|
||||
class SearchPagingSource
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
|
||||
private val searchApi: LobstersSearchApi,
|
||||
@Assisted private val queryProvider: () -> String,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : PagingSource<Int, LobstersPost>() {
|
||||
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int? {
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val readPostsRepository: ReadPostsRepository,
|
||||
) : PagingSource<Int, UIPost>() {
|
||||
override fun getRefreshKey(state: PagingState<Int, UIPost>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UIPost> {
|
||||
val searchQuery = queryProvider()
|
||||
// If there is no query, we don't need to call the API at all.
|
||||
if (searchQuery.isEmpty()) {
|
||||
return LoadResult.Page(itemsBefore = 0, data = emptyList(), prevKey = null, nextKey = null)
|
||||
}
|
||||
val page = params.key ?: STARTING_PAGE_INDEX
|
||||
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
|
||||
val result = withContext(ioDispatcher) { searchApi.searchPosts(searchQuery, page) }
|
||||
return when (result) {
|
||||
is ApiResult.Success -> {
|
||||
// Optimization: prevent fetching more pages if we found no items, this means
|
||||
// there is no active search query.
|
||||
val nextKey = if (result.value.isEmpty()) null else page + 1
|
||||
LoadResult.Page(
|
||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||
data = result.value,
|
||||
data =
|
||||
result.value.map {
|
||||
it
|
||||
.toUIPost()
|
||||
.copy(
|
||||
isSaved = savedPostsRepository.isPostSaved(it.shortId),
|
||||
isRead = readPostsRepository.isPostRead(it.shortId),
|
||||
)
|
||||
},
|
||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||
nextKey = nextKey,
|
||||
)
|
||||
@@ -62,6 +83,6 @@ constructor(
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(remoteFetcher: RemoteFetcher<LobstersPost>): SearchPagingSource
|
||||
fun create(queryProvider: () -> String): SearchPagingSource
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright © 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.settings
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import javax.inject.Inject
|
||||
|
||||
class LoginSettings
|
||||
@Inject
|
||||
constructor(
|
||||
val dataStore: DataStore<Preferences>,
|
||||
) {
|
||||
companion object {
|
||||
val CSRF_TOKEN = stringPreferencesKey("csrf_token")
|
||||
val LOGIN_COOKIE = stringPreferencesKey("login_cookie")
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Copyright © 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.datatransfer
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.activity.result.contract.ActivityResultContracts.GetContent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.Upload
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val MIME_TYPE = "application/json"
|
||||
|
||||
@Composable
|
||||
fun DataTransferScreen(
|
||||
context: Context,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
exportPosts: suspend (OutputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Column(modifier = modifier) {
|
||||
ImportOption(context, coroutineScope, importPosts, snackbarHostState)
|
||||
ExportOption(context, coroutineScope, exportPosts, snackbarHostState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImportOption(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val importAction =
|
||||
rememberLauncherForActivityResult(GetContent()) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
importPosts(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully imported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsActionItem(
|
||||
title = "Import saved posts",
|
||||
description = "Import saved posts from a previously generated export",
|
||||
icon = Icons.Filled.Download,
|
||||
) {
|
||||
importAction.launch(MIME_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExportOption(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
exportPosts: suspend (OutputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val exportAction =
|
||||
rememberLauncherForActivityResult(CreateDocument(MIME_TYPE)) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openOutputStream(uri)?.use { stream ->
|
||||
exportPosts(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsActionItem(
|
||||
title = "Export posts to file",
|
||||
description = "Write all saved posts into a JSON file that can be imported at a later date",
|
||||
icon = Icons.Filled.Upload,
|
||||
) {
|
||||
exportAction.launch("claw-export.json")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsActionItem(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
description: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(title) },
|
||||
supportingContent = { description?.let { Text(it) } },
|
||||
leadingContent = {
|
||||
icon?.let {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = modifier.clickable { onClick?.invoke() },
|
||||
)
|
||||
}
|
||||
|
||||
/** Shows a Snackbar but dismisses any existing ones first. */
|
||||
private suspend fun SnackbarHostState.showSnackbarDismissing(
|
||||
text: String,
|
||||
) {
|
||||
currentSnackbarData?.dismiss()
|
||||
showSnackbar(text)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -16,9 +16,8 @@ import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
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 dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
fun Context.getActivity(): ComponentActivity? {
|
||||
return when (this) {
|
||||
@@ -45,7 +44,7 @@ fun rememberPostActions(
|
||||
viewModel.markPostAsRead(postId)
|
||||
val currentRoute = navController.currentDestination?.route
|
||||
val newRoute =
|
||||
Destinations.Comments.route.replace(Destinations.Comments.placeholder, postId)
|
||||
Destinations.Comments.route.replace(Destinations.Comments.PLACEHOLDER, postId)
|
||||
if (currentRoute != Destinations.Comments.route) navController.navigate(newRoute)
|
||||
}
|
||||
|
||||
@@ -59,11 +58,11 @@ fun rememberPostActions(
|
||||
urlLauncher.openUri(commentsUrl.replaceAfterLast('/', "r"))
|
||||
}
|
||||
|
||||
override fun toggleSave(post: SavedPost) {
|
||||
override fun toggleSave(post: UIPost) {
|
||||
viewModel.toggleSave(post)
|
||||
}
|
||||
|
||||
override suspend fun getComments(postId: String): LobstersPostDetails {
|
||||
override suspend fun getComments(postId: String): UIPost {
|
||||
return viewModel.getPostComments(postId)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -27,14 +27,13 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.ui.decorations.MonthHeader
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import java.time.Month
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun DatabasePosts(
|
||||
items: ImmutableMap<Month, List<SavedPost>>,
|
||||
items: ImmutableMap<String, List<UIPost>>,
|
||||
listState: LazyListState,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -53,18 +52,9 @@ fun DatabasePosts(
|
||||
} else {
|
||||
LazyColumn(state = listState) {
|
||||
items.forEach { (month, posts) ->
|
||||
stickyHeader(contentType = "month-header") { MonthHeader(month = month) }
|
||||
items(
|
||||
items = posts,
|
||||
key = { it.shortId },
|
||||
contentType = { "LobstersItem" },
|
||||
) { item ->
|
||||
ListItem(
|
||||
item = item,
|
||||
isSaved = { true },
|
||||
isRead = { false },
|
||||
postActions = postActions,
|
||||
)
|
||||
stickyHeader(contentType = "month-header") { MonthHeader(label = month) }
|
||||
items(items = posts, key = { it.shortId }, contentType = { "LobstersItem" }) { item ->
|
||||
LobstersListItem(item = item, refresh = {}, postActions = postActions)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -11,40 +11,28 @@ import androidx.compose.material.icons.automirrored.filled.Reply
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import dev.msfjarvis.claw.common.posts.LobstersCard
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import me.saket.swipe.SwipeAction
|
||||
import me.saket.swipe.SwipeableActionsBox
|
||||
|
||||
@Composable
|
||||
fun ListItem(
|
||||
item: SavedPost,
|
||||
isSaved: suspend (SavedPost) -> Boolean,
|
||||
isRead: suspend (String) -> Boolean,
|
||||
fun LobstersListItem(
|
||||
item: UIPost,
|
||||
postActions: PostActions,
|
||||
refresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val saved by produceState(false, item) { value = isSaved(item) }
|
||||
val read by produceState(false, item.shortId) { value = isRead(item.shortId) }
|
||||
val commentsAction =
|
||||
SwipeAction(
|
||||
icon = rememberVectorPainter(Icons.AutoMirrored.Filled.Reply),
|
||||
background = MaterialTheme.colorScheme.tertiary,
|
||||
onSwipe = { postActions.viewCommentsPage(item.commentsUrl) },
|
||||
)
|
||||
SwipeableActionsBox(
|
||||
endActions = listOf(commentsAction),
|
||||
) {
|
||||
LobstersCard(
|
||||
post = item,
|
||||
isSaved = saved,
|
||||
isRead = read,
|
||||
postActions = postActions,
|
||||
modifier = modifier,
|
||||
)
|
||||
SwipeableActionsBox(endActions = listOf(commentsAction)) {
|
||||
LobstersCard(post = item, postActions = postActions, refresh = refresh, modifier = modifier)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -14,13 +14,8 @@ 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.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
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
|
||||
@@ -32,23 +27,22 @@ import androidx.paging.compose.itemKey
|
||||
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.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import eu.bambooapps.material3.pullrefresh.PullRefreshIndicator
|
||||
import eu.bambooapps.material3.pullrefresh.pullRefresh
|
||||
import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NetworkPosts(
|
||||
lazyPagingItems: LazyPagingItems<LobstersPost>,
|
||||
lazyPagingItems: LazyPagingItems<UIPost>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: suspend (SavedPost) -> Boolean,
|
||||
isPostRead: suspend (String) -> Boolean,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ReportDrawnWhen { lazyPagingItems.itemCount > 0 }
|
||||
val refreshLoadState = lazyPagingItems.loadState.refresh
|
||||
val isRefreshing = refreshLoadState == LoadState.Loading
|
||||
val isRefreshing = refreshLoadState == LoadState.Loading && lazyPagingItems.itemCount == 0
|
||||
val pullRefreshState = rememberPullRefreshState(isRefreshing, lazyPagingItems::refresh)
|
||||
Box(modifier = modifier.fillMaxSize().pullRefresh(pullRefreshState)) {
|
||||
if (lazyPagingItems.itemCount == 0 && refreshLoadState is LoadState.Error) {
|
||||
@@ -58,9 +52,7 @@ fun NetworkPosts(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
) {
|
||||
LazyColumn(state = listState) {
|
||||
items(
|
||||
count = lazyPagingItems.itemCount,
|
||||
key = lazyPagingItems.itemKey { it.shortId },
|
||||
@@ -68,14 +60,11 @@ fun NetworkPosts(
|
||||
) { index ->
|
||||
val item = lazyPagingItems[index]
|
||||
if (item != null) {
|
||||
val dbModel = item.toSavedPost()
|
||||
ListItem(
|
||||
item = dbModel,
|
||||
isSaved = isPostSaved,
|
||||
isRead = isPostRead,
|
||||
LobstersListItem(
|
||||
item = item,
|
||||
postActions = postActions,
|
||||
refresh = { lazyPagingItems.refresh() },
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
@@ -85,7 +74,7 @@ fun NetworkPosts(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
.padding(vertical = 16.dp),
|
||||
.padding(vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -95,8 +84,7 @@ fun NetworkPosts(
|
||||
refreshing = isRefreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
backgroundColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = contentColorFor(MaterialTheme.colorScheme.surface),
|
||||
shadowElevation = 6.dp, // From M2 implementation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,16 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SearchOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.isTraversalGroup
|
||||
@@ -35,15 +21,13 @@ import androidx.paging.PagingData
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.ui.SearchBar
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Composable
|
||||
fun SearchList(
|
||||
items: Flow<PagingData<LobstersPost>>,
|
||||
items: Flow<PagingData<UIPost>>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: suspend (SavedPost) -> Boolean,
|
||||
postActions: PostActions,
|
||||
searchQuery: String,
|
||||
setSearchQuery: (String) -> Unit,
|
||||
@@ -54,42 +38,17 @@ fun SearchList(
|
||||
setSearchQuery(query)
|
||||
lazyPagingItems.refresh()
|
||||
}
|
||||
DisposableEffect(true) {
|
||||
// Clear search field when navigating away
|
||||
onDispose { setSearchQuery("") }
|
||||
}
|
||||
|
||||
var searchActive by remember { mutableStateOf(false) }
|
||||
Column(modifier = Modifier.semantics { isTraversalGroup = true }.zIndex(1f).fillMaxWidth()) {
|
||||
Column(modifier = modifier.semantics { isTraversalGroup = true }.zIndex(1f).fillMaxWidth()) {
|
||||
SearchBar(
|
||||
value = searchQuery,
|
||||
onValueChange = setSearchQuery,
|
||||
onSearch = {
|
||||
triggerSearch(it)
|
||||
searchActive = true
|
||||
},
|
||||
onSearch = { triggerSearch(it) },
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp).testTag("search_bar"),
|
||||
)
|
||||
if (searchActive) {
|
||||
NetworkPosts(
|
||||
lazyPagingItems = lazyPagingItems,
|
||||
listState = listState,
|
||||
isPostSaved = isPostSaved,
|
||||
isPostRead = { false },
|
||||
postActions = postActions,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.SearchOff,
|
||||
contentDescription = "No search results",
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally).size(36.dp),
|
||||
)
|
||||
Text(text = "Nothing to see here", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
NetworkPosts(
|
||||
lazyPagingItems = lazyPagingItems,
|
||||
listState = listState,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright © 2023-2024 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.login
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.google.accompanist.web.AccompanistWebViewClient
|
||||
import com.google.accompanist.web.WebView
|
||||
import com.google.accompanist.web.rememberWebViewState
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import io.github.aakira.napier.Napier
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
|
||||
@Suppress("Unused")
|
||||
@Composable
|
||||
fun LobstersLoginWebView(modifier: Modifier = Modifier) {
|
||||
val state = rememberWebViewState(url = "https://lobste.rs/login")
|
||||
val webClient = remember {
|
||||
object : AccompanistWebViewClient() {
|
||||
override fun onPageFinished(view: WebView, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
Napier.d(tag = "WebView") { "onPageFinished(url=$url)" }
|
||||
if (url != null && url.toHttpUrl() == LobstersApi.BASE_URL.toHttpUrl()) {
|
||||
val cookies = CookieManager.getInstance().getCookie(LobstersApi.BASE_URL)
|
||||
val trap =
|
||||
cookies
|
||||
.split(";")
|
||||
.associate {
|
||||
val split = it.split("=")
|
||||
split[0] to split[1]
|
||||
}["lobsters_trap"]
|
||||
Napier.d(tag = "WebView") { "lobsters_trap=${trap}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WebView(state = state, client = webClient, modifier = modifier, captureBackPresses = false)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -9,34 +9,38 @@ package dev.msfjarvis.claw.android.ui.navigation
|
||||
sealed class Destinations {
|
||||
abstract val route: String
|
||||
|
||||
object Hottest : Destinations() {
|
||||
data object Hottest : Destinations() {
|
||||
override val route = "hottest"
|
||||
}
|
||||
|
||||
object Newest : Destinations() {
|
||||
data object Newest : Destinations() {
|
||||
override val route = "newest"
|
||||
}
|
||||
|
||||
object Saved : Destinations() {
|
||||
data object Saved : Destinations() {
|
||||
override val route = "saved"
|
||||
}
|
||||
|
||||
object Search : Destinations() {
|
||||
data object Comments : Destinations() {
|
||||
const val PLACEHOLDER = "{postId}"
|
||||
override val route = "comments/$PLACEHOLDER"
|
||||
}
|
||||
|
||||
data object User : Destinations() {
|
||||
const val PLACEHOLDER = "{username}"
|
||||
override val route = "user/$PLACEHOLDER"
|
||||
}
|
||||
|
||||
data object Search : Destinations() {
|
||||
override val route = "search"
|
||||
}
|
||||
|
||||
object Comments : Destinations() {
|
||||
const val placeholder = "{postId}"
|
||||
override val route = "comments/$placeholder"
|
||||
data object Settings : Destinations() {
|
||||
override val route = "settings"
|
||||
}
|
||||
|
||||
object User : Destinations() {
|
||||
const val placeholder = "{username}"
|
||||
override val route = "user/$placeholder"
|
||||
}
|
||||
|
||||
object DataTransfer : Destinations() {
|
||||
override val route: String = "datatransfer"
|
||||
data object AboutLibraries : Destinations() {
|
||||
override val route = "about_libraries"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,48 +1,57 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.ImportExport
|
||||
import androidx.compose.material.icons.filled.NewReleases
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Tune
|
||||
import androidx.compose.material.icons.filled.Whatshot
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material.icons.outlined.Whatshot
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.compose.ui.unit.dp
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
@@ -52,21 +61,24 @@ import androidx.navigation.navArgument
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.android.ui.datatransfer.DataTransferScreen
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawAppBar
|
||||
import dev.msfjarvis.claw.android.SearchActivity
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
|
||||
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
|
||||
import dev.msfjarvis.claw.android.ui.getActivity
|
||||
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
|
||||
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
|
||||
import dev.msfjarvis.claw.android.ui.lists.SearchList
|
||||
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import dev.msfjarvis.claw.android.ui.rememberPostActions
|
||||
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.ui.decorations.ClawAppBar
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import dev.msfjarvis.claw.common.user.UserProfile
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -75,7 +87,7 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LobstersApp(
|
||||
fun LobstersPostsScreen(
|
||||
urlLauncher: UrlLauncher,
|
||||
htmlConverter: HTMLConverter,
|
||||
windowSizeClass: WindowSizeClass,
|
||||
@@ -86,7 +98,6 @@ fun LobstersApp(
|
||||
val hottestListState = rememberLazyListState()
|
||||
val newestListState = rememberLazyListState()
|
||||
val savedListState = rememberLazyListState()
|
||||
val searchListState = rememberLazyListState()
|
||||
val navController = rememberNavController()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
@@ -97,10 +108,19 @@ fun LobstersApp(
|
||||
|
||||
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
|
||||
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
|
||||
val savedPosts by viewModel.savedPosts.collectAsState(persistentMapOf())
|
||||
val savedPosts by viewModel.savedPostsByMonth.collectAsState(persistentMapOf())
|
||||
|
||||
val navigationType = ClawNavigationType.fromSize(windowSizeClass.widthSizeClass)
|
||||
|
||||
val postIdOverride = context.getActivity()?.intent?.extras?.getString(MainActivity.NAVIGATION_KEY)
|
||||
LaunchedEffect(false) {
|
||||
if (postIdOverride != null) {
|
||||
navController.navigate(
|
||||
Destinations.Comments.route.replace(Destinations.Comments.PLACEHOLDER, postIdOverride)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val navItems =
|
||||
persistentListOf(
|
||||
NavigationItem(
|
||||
@@ -127,52 +147,52 @@ fun LobstersApp(
|
||||
) {
|
||||
coroutineScope.launch { savedListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Search",
|
||||
route = Destinations.Search.route,
|
||||
icon = Icons.Outlined.Search,
|
||||
selectedIcon = Icons.Filled.Search,
|
||||
) {
|
||||
coroutineScope.launch { searchListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (currentDestination != Destinations.Search.route) {
|
||||
ClawAppBar(
|
||||
navigationIcon = {
|
||||
if (
|
||||
navController.previousBackStackEntry != null &&
|
||||
navItems.none { it.route == currentDestination }
|
||||
ClawAppBar(
|
||||
navigationIcon = {
|
||||
if (
|
||||
navController.previousBackStackEntry != null &&
|
||||
navItems.none { it.route == currentDestination }
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() }
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Go back to previous screen",
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Go back to previous screen",
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
}
|
||||
},
|
||||
title = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
IconButton(onClick = { navController.navigate(Destinations.DataTransfer.route) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ImportExport,
|
||||
contentDescription = "Data transfer options",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
IconButton(
|
||||
onClick = { context.startActivity(Intent(context, SearchActivity::class.java)) }
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Search, contentDescription = "Search posts")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { navController.navigate(Destinations.Settings.route) }) {
|
||||
Icon(imageVector = Icons.Filled.Tune, contentDescription = "Settings")
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
|
||||
@@ -208,8 +228,6 @@ fun LobstersApp(
|
||||
NetworkPosts(
|
||||
lazyPagingItems = hottestPosts,
|
||||
listState = hottestListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
isPostRead = viewModel::isPostRead,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
@@ -218,40 +236,26 @@ fun LobstersApp(
|
||||
NetworkPosts(
|
||||
lazyPagingItems = newestPosts,
|
||||
listState = newestListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
isPostRead = viewModel::isPostRead,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.Saved.route) {
|
||||
setWebUri(null)
|
||||
DatabasePosts(
|
||||
items = savedPosts,
|
||||
listState = savedListState,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
composable(Destinations.Search.route) {
|
||||
setWebUri("https://lobste.rs/search")
|
||||
SearchList(
|
||||
items = viewModel.searchResults,
|
||||
listState = searchListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
postActions = postActions,
|
||||
searchQuery = viewModel.searchQuery,
|
||||
setSearchQuery = { query -> viewModel.searchQuery = query },
|
||||
)
|
||||
DatabasePosts(items = savedPosts, listState = savedListState, postActions = postActions)
|
||||
}
|
||||
composable(
|
||||
route = Destinations.Comments.route,
|
||||
arguments = listOf(navArgument("postId") { type = NavType.StringType }),
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.placeholder}/.*" },
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.placeholder}" },
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.PLACEHOLDER}/.*" },
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.PLACEHOLDER}" },
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val postId = requireNotNull(backStackEntry.arguments?.getString("postId"))
|
||||
val postId =
|
||||
requireNotNull(backStackEntry.arguments?.getString("postId")) {
|
||||
"Navigating to ${Destinations.Comments.route} without necessary 'postId' argument"
|
||||
}
|
||||
setWebUri("https://lobste.rs/s/$postId")
|
||||
CommentsPage(
|
||||
postId = postId,
|
||||
@@ -264,24 +268,28 @@ fun LobstersApp(
|
||||
composable(
|
||||
route = Destinations.User.route,
|
||||
arguments = listOf(navArgument("username") { type = NavType.StringType }),
|
||||
deepLinks =
|
||||
listOf(navDeepLink { uriPattern = "$uri/u/${Destinations.User.placeholder}" }),
|
||||
deepLinks = listOf(navDeepLink { uriPattern = "$uri/u/${Destinations.User.PLACEHOLDER}" }),
|
||||
) { backStackEntry ->
|
||||
val username = requireNotNull(backStackEntry.arguments?.getString("username"))
|
||||
val username =
|
||||
requireNotNull(backStackEntry.arguments?.getString("username")) {
|
||||
"Navigating to ${Destinations.User.route} without necessary 'username' argument"
|
||||
}
|
||||
setWebUri("https://lobste.rs/u/$username")
|
||||
UserProfile(
|
||||
username = username,
|
||||
getProfile = viewModel::getUserProfile,
|
||||
)
|
||||
UserProfile(username = username, getProfile = viewModel::getUserProfile)
|
||||
}
|
||||
composable(route = Destinations.DataTransfer.route) {
|
||||
DataTransferScreen(
|
||||
composable(route = Destinations.Settings.route) {
|
||||
SettingsScreen(
|
||||
context = context,
|
||||
openLibrariesScreen = { navController.navigate(Destinations.AboutLibraries.route) },
|
||||
importPosts = viewModel::importPosts,
|
||||
exportPosts = viewModel::exportPosts,
|
||||
exportPostsAsJson = viewModel::exportPostsAsJson,
|
||||
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.AboutLibraries.route) {
|
||||
LibrariesContainer(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||
import dev.msfjarvis.claw.android.ui.lists.SearchList
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import dev.msfjarvis.claw.android.ui.rememberPostActions
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
urlLauncher: UrlLauncher,
|
||||
htmlConverter: HTMLConverter,
|
||||
setWebUri: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val postActions = rememberPostActions(urlLauncher, navController, viewModel)
|
||||
val listState = rememberLazyListState()
|
||||
Scaffold(modifier = modifier) { paddingValues ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Destinations.Search.route,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
) {
|
||||
composable(route = Destinations.Search.route) {
|
||||
setWebUri("https://lobste.rs/search")
|
||||
SearchList(
|
||||
items = viewModel.searchResults,
|
||||
listState = listState,
|
||||
postActions = postActions,
|
||||
searchQuery = viewModel.searchQuery,
|
||||
setSearchQuery = { query -> viewModel.searchQuery = query },
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Destinations.Comments.route,
|
||||
arguments = listOf(navArgument("postId") { type = NavType.StringType }),
|
||||
) { backStackEntry ->
|
||||
val postId =
|
||||
requireNotNull(backStackEntry.arguments?.getString("postId")) {
|
||||
"Navigating to ${Destinations.Comments.route} without necessary 'postId' argument"
|
||||
}
|
||||
setWebUri("https://lobste.rs/s/$postId")
|
||||
CommentsPage(
|
||||
postId = postId,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright © 2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
||||
import androidx.compose.material.icons.filled.Bookmarks
|
||||
import androidx.compose.material.icons.filled.Code
|
||||
import androidx.compose.material.icons.filled.ImportExport
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val JSON_MIME_TYPE = "application/json"
|
||||
private const val HTML_MIME_TYPE = "application/html"
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
context: Context,
|
||||
openLibrariesScreen: () -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
exportPostsAsJson: suspend (OutputStream) -> Unit,
|
||||
exportPostsAsHtml: suspend (OutputStream) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
Column {
|
||||
ListItem(
|
||||
headlineContent = { Text("Libraries") },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.LibraryBooks,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = openLibrariesScreen),
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text("Data transfer") },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ImportExport,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ImportPosts(context, coroutineScope, snackbarHostState, importPosts)
|
||||
ExportPosts(
|
||||
context,
|
||||
coroutineScope,
|
||||
snackbarHostState,
|
||||
exportPostsAsJson,
|
||||
exportPostsAsHtml,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.ImportPosts(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val importAction =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
importPosts(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully imported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { importAction.launch(JSON_MIME_TYPE) },
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
modifier = modifier.fillMaxWidth(0.30f).weight(0.5f),
|
||||
) {
|
||||
Text(text = "Import")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.ExportPosts(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
exportPostsAsJson: suspend (OutputStream) -> Unit,
|
||||
exportPostsAsHtml: suspend (OutputStream) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val jsonExportAction =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(JSON_MIME_TYPE)) { uri
|
||||
->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openOutputStream(uri)?.use { stream ->
|
||||
exportPostsAsJson(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
val htmlExportAction =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(HTML_MIME_TYPE)) { uri
|
||||
->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openOutputStream(uri)?.use { stream ->
|
||||
exportPostsAsHtml(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
OutlinedButton(
|
||||
onClick = { expanded = true },
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
modifier = modifier.fillMaxWidth(0.30f).weight(0.5f),
|
||||
) {
|
||||
Text(text = "Export")
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("JSON") },
|
||||
onClick = {
|
||||
expanded = false
|
||||
jsonExportAction.launch("claw-export.json")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Filled.Code, contentDescription = "Export as JSON")
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Bookmarks") },
|
||||
onClick = {
|
||||
expanded = false
|
||||
htmlExportAction.launch("claw-export.html")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Bookmarks,
|
||||
contentDescription = "Export as browser bookmarks",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows a Snackbar but dismisses any existing ones first. */
|
||||
private suspend fun SnackbarHostState.showSnackbarDismissing(text: String) {
|
||||
currentSnackbarData?.dismiss()
|
||||
showSnackbar(text)
|
||||
}
|
||||
@@ -1,43 +1,53 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
|
||||
import com.slack.eithernet.ApiResult.Failure
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import com.squareup.anvil.annotations.optional.ForScope
|
||||
import dev.msfjarvis.claw.android.glance.SavedPostsWidget
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
|
||||
import dev.msfjarvis.claw.android.paging.SearchPagingSource
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.core.injection.MainDispatcher
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.fromSavedPost
|
||||
import dev.msfjarvis.claw.model.toUIPost
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.time.LocalDateTime
|
||||
import java.time.Month
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -47,7 +57,6 @@ class ClawViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val api: LobstersApi,
|
||||
private val searchApi: LobstersSearchApi,
|
||||
private val commentsRepository: CommentsRepository,
|
||||
private val readPostsRepository: ReadPostsRepository,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
@@ -56,61 +65,78 @@ constructor(
|
||||
private val pagingSourceFactory: LobstersPagingSource.Factory,
|
||||
private val searchPagingSourceFactory: SearchPagingSource.Factory,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
private val hottestPostsPager =
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
pagingSourceFactory.create(api::getHottestPosts)
|
||||
}
|
||||
private val newestPostsPager =
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
pagingSourceFactory.create(api::getNewestPosts)
|
||||
}
|
||||
private val searchResultsPager =
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
searchPagingSourceFactory.create { searchApi.searchPosts(searchQuery, it) }
|
||||
}
|
||||
|
||||
val hottestPosts
|
||||
get() = hottestPostsPager.flow
|
||||
|
||||
val newestPosts
|
||||
get() = newestPostsPager.flow
|
||||
|
||||
private val savedPostsFlow
|
||||
get() = savedPostsRepository.savedPosts
|
||||
|
||||
val savedPosts
|
||||
get() = savedPostsFlow.map(::mapSavedPosts)
|
||||
|
||||
val searchResults
|
||||
get() = searchResultsPager.flow
|
||||
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
|
||||
@ForScope(ApplicationScope::class) context: Context,
|
||||
) : AndroidViewModel(context as Application) {
|
||||
val hottestPosts =
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = PAGE_SIZE),
|
||||
initialKey = STARTING_PAGE_INDEX,
|
||||
pagingSourceFactory = { pagingSourceFactory.create(api::getHottestPosts) },
|
||||
)
|
||||
.flow
|
||||
.cachedIn(viewModelScope)
|
||||
val newestPosts =
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = PAGE_SIZE),
|
||||
initialKey = STARTING_PAGE_INDEX,
|
||||
pagingSourceFactory = { pagingSourceFactory.create(api::getNewestPosts) },
|
||||
)
|
||||
.flow
|
||||
.cachedIn(viewModelScope)
|
||||
val searchResults =
|
||||
Pager(
|
||||
PagingConfig(pageSize = PAGE_SIZE),
|
||||
initialKey = STARTING_PAGE_INDEX,
|
||||
pagingSourceFactory = { searchPagingSourceFactory.create { searchQuery } },
|
||||
)
|
||||
.flow
|
||||
val savedPosts = savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) }
|
||||
val savedPostsByMonth
|
||||
get() = savedPosts.map(::groupSavedPosts)
|
||||
|
||||
var searchQuery by mutableStateOf("")
|
||||
|
||||
private fun mapSavedPosts(items: List<SavedPost>): ImmutableMap<Month, List<SavedPost>> {
|
||||
val sorted = items.sortedByDescending { post -> post.createdAt.toLocalDateTime() }
|
||||
return sorted.groupBy { post -> post.createdAt.toLocalDateTime().month }.toImmutableMap()
|
||||
private var _readPosts = emptyList<String>()
|
||||
private var _savedPosts = emptyList<String>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch { savedPosts.collectLatest { _savedPosts = it.map(UIPost::shortId) } }
|
||||
viewModelScope.launch { readPostsRepository.readPosts.collectLatest { _readPosts = it } }
|
||||
}
|
||||
|
||||
suspend fun isPostSaved(post: SavedPost): Boolean {
|
||||
return savedPostsFlow.first().any { savedPost -> savedPost.shortId == post.shortId }
|
||||
}
|
||||
|
||||
fun toggleSave(post: SavedPost) {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
val saved = isPostSaved(post)
|
||||
if (saved) {
|
||||
savedPostsRepository.removePost(post)
|
||||
} else {
|
||||
savedPostsRepository.savePost(post)
|
||||
private fun groupSavedPosts(items: List<UIPost>): ImmutableMap<String, List<UIPost>> {
|
||||
val sorted =
|
||||
items.sortedWith { post1, post2 ->
|
||||
val post1Date = post1.createdAt.toLocalDateTime()
|
||||
val post2Date = post2.createdAt.toLocalDateTime()
|
||||
if (post2Date.isBefore(post1Date)) {
|
||||
-1
|
||||
} else if (post2Date.isAfter(post1Date)) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
return sorted
|
||||
.groupBy { post ->
|
||||
val time = post.createdAt.toLocalDateTime()
|
||||
"${time.month.name.lowercase().capitalize(Locale.current)} ${time.year}"
|
||||
}
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
fun toggleSave(post: UIPost) {
|
||||
viewModelScope.launch {
|
||||
savedPostsRepository.toggleSave(post)
|
||||
withContext(mainDispatcher) { SavedPostsWidget(savedPosts).updateAll(getApplication()) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getPostComments(postId: String) =
|
||||
withContext(ioDispatcher) {
|
||||
when (val result = api.getPostDetails(postId)) {
|
||||
is Success -> result.value
|
||||
is Success -> result.value.toUIPost()
|
||||
is Failure.NetworkFailure -> throw result.error
|
||||
is Failure.UnknownFailure -> throw result.error
|
||||
is Failure.HttpFailure -> {
|
||||
@@ -146,14 +172,16 @@ constructor(
|
||||
|
||||
suspend fun importPosts(input: InputStream) = dataTransferRepository.importPosts(input)
|
||||
|
||||
suspend fun exportPosts(output: OutputStream) = dataTransferRepository.exportPosts(output)
|
||||
suspend fun exportPostsAsJson(output: OutputStream) =
|
||||
dataTransferRepository.exportPostsAsJson(output)
|
||||
|
||||
suspend fun exportPostsAsHtml(output: OutputStream) =
|
||||
dataTransferRepository.exportPostsAsHTML(output)
|
||||
|
||||
fun markPostAsRead(postId: String) {
|
||||
viewModelScope.launch { readPostsRepository.markRead(postId) }
|
||||
}
|
||||
|
||||
suspend fun isPostRead(postId: String) = readPostsRepository.isRead(postId)
|
||||
|
||||
/**
|
||||
* Parses a given [String] into a [LocalDateTime]. This method is only intended to be used for
|
||||
* dates in the format returned by the Lobsters API, and is not a general purpose parsing
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.database.SavedPostSerializer
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.database.local.SavedPostQueries
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
@@ -27,27 +28,68 @@ class DataTransferRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val json: Json,
|
||||
private val savedPostQueries: SavedPostQueries,
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val serializer = ListSerializer(SavedPostSerializer)
|
||||
|
||||
suspend fun importPosts(input: InputStream) {
|
||||
val posts: List<SavedPost> =
|
||||
withContext(ioDispatcher) {
|
||||
json.decodeFromStream(
|
||||
serializer,
|
||||
input,
|
||||
)
|
||||
}
|
||||
withContext(dbDispatcher) {
|
||||
savedPostQueries.transaction { posts.forEach(savedPostQueries::insertOrReplacePost) }
|
||||
}
|
||||
withContext(ioDispatcher) { json.decodeFromStream(serializer, input) }
|
||||
savedPostsRepository.savePosts(posts)
|
||||
}
|
||||
|
||||
suspend fun exportPosts(output: OutputStream) {
|
||||
val posts = withContext(dbDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
|
||||
suspend fun exportPostsAsJson(output: OutputStream) {
|
||||
val posts = savedPostsRepository.savedPosts.first()
|
||||
withContext(ioDispatcher) { json.encodeToStream(serializer, posts, output) }
|
||||
}
|
||||
|
||||
suspend fun exportPostsAsHTML(output: OutputStream) {
|
||||
fun computeTimestamp(post: SavedPost): Long {
|
||||
val temporal = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(post.createdAt)
|
||||
val instant = Instant.from(temporal)
|
||||
return instant.toEpochMilli()
|
||||
}
|
||||
|
||||
val posts = savedPostsRepository.savedPosts.first()
|
||||
val header =
|
||||
"""
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<!-- This is an automatically generated file.
|
||||
It will be read and overwritten.
|
||||
DO NOT EDIT! -->
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
"""
|
||||
.trimIndent()
|
||||
val html = buildString {
|
||||
append(header)
|
||||
append("<DD><p>\n")
|
||||
for (post in posts) {
|
||||
append(
|
||||
"""
|
||||
<DT><A HREF="${post.url.ifEmpty { post.commentsUrl }}" ADD_DATE="${computeTimestamp(post)}" PRIVATE="0" TAGS="${post.tags.joinToString(",")}">${post.title}</A>
|
||||
<DD>${post.title}
|
||||
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
append(
|
||||
"""
|
||||
<DT><A HREF="https://example.com/" ADD_DATE="0" PRIVATE="0" TAGS="delete,me,pls">Padding post</A>
|
||||
<DD>Linkding ignores the last entry so this pads the difference for imports
|
||||
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
append("</DD></p>\n")
|
||||
}
|
||||
withContext(ioDispatcher) {
|
||||
val writer = output.bufferedWriter()
|
||||
writer.write(html)
|
||||
writer.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.database.local.ReadPostsQueries
|
||||
import javax.inject.Inject
|
||||
@@ -18,11 +20,13 @@ constructor(
|
||||
private val readPostsQueries: ReadPostsQueries,
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
val readPosts = readPostsQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
|
||||
|
||||
suspend fun markRead(postId: String) {
|
||||
withContext(dbDispatcher) { readPostsQueries.markRead(postId) }
|
||||
}
|
||||
|
||||
suspend fun isRead(postId: String): Boolean =
|
||||
withContext(dbDispatcher) { readPostsQueries.isRead(postId).executeAsOneOrNull() != null }
|
||||
suspend fun isPostRead(shortId: String): Boolean {
|
||||
return withContext(dbDispatcher) { readPostsQueries.isPostRead(shortId).executeAsOne() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -11,6 +11,8 @@ import app.cash.sqldelight.coroutines.mapToList
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.database.local.SavedPostQueries
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import io.github.aakira.napier.Napier
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -24,9 +26,14 @@ constructor(
|
||||
) {
|
||||
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
|
||||
|
||||
suspend fun savePost(post: SavedPost) {
|
||||
Napier.d(tag = TAG) { "Saving post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.insertOrReplacePost(post) }
|
||||
suspend fun toggleSave(post: UIPost) {
|
||||
if (post.isSaved) {
|
||||
Napier.d(tag = TAG) { "Removing post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
|
||||
} else {
|
||||
Napier.d(tag = TAG) { "Saving post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.insertOrReplacePost(post.toSavedPost()) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun savePosts(posts: List<SavedPost>) {
|
||||
@@ -38,9 +45,8 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removePost(post: SavedPost) {
|
||||
Napier.d(tag = TAG) { "Removing post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
|
||||
suspend fun isPostSaved(postId: String): Boolean {
|
||||
return withContext(dbDispatcher) { savedPostQueries.isPostSaved(postId).executeAsOne() }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -7,18 +7,23 @@
|
||||
package dev.msfjarvis.claw.android.work
|
||||
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.deliveryhero.whetstone.worker.ContributesWorker
|
||||
import com.deliveryhero.whetstone.worker.WorkerScope
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import com.squareup.anvil.annotations.optional.ForScope
|
||||
import dev.msfjarvis.claw.android.glance.SavedPostsWidget
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.fromSavedPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* WorkManager-backed [CoroutineWorker] that gets all the posts from [SavedPostsRepository], fetches
|
||||
@@ -42,6 +47,10 @@ constructor(
|
||||
.filterIsInstance<Success<LobstersPostDetails>>()
|
||||
.map { result -> result.value.toSavedPost() }
|
||||
.let { savedPostsRepository.savePosts(it) }
|
||||
SavedPostsWidget(
|
||||
savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) }
|
||||
)
|
||||
.updateAll(applicationContext)
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--
|
||||
~ Copyright © 2024 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:colorControlNormal"
|
||||
android:alpha="0.6"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21.99,4c0,-1.1 -0.89,-2 -1.99,-2L4,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h14l4,4 -0.01,-18zM20,4v13.17L18.83,16L4,16L4,4h16zM6,12h12v2L6,14zM6,9h12v2L6,11zM6,6h12v2L6,8z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,20 @@
|
||||
<!--
|
||||
~ Copyright © 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.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1080"
|
||||
android:viewportHeight="1080">
|
||||
<group android:scaleX="0.7"
|
||||
android:scaleY="0.7"
|
||||
android:translateX="162"
|
||||
android:translateY="162">
|
||||
<path
|
||||
android:pathData="M431.3,341.2C435.9,337 580.3,236 710.5,223.8C747.3,220.3 790.7,254.7 833.2,312.5C875.9,370.7 892.9,433.4 875.9,443C863.2,450.2 838.6,407.4 758.8,396.3C743.4,394.2 730.4,393.4 719.4,393.5C738.5,423.4 745.6,451.1 737.2,458C733.1,461.4 673.6,438.7 612.9,436.5C570.1,435 535.2,440.9 506.7,451.3C496.9,454.9 486,453.8 477.2,448.3C444.5,427.9 424.2,406.2 418.4,376.2C415.8,362.8 420.9,349.4 431.3,341.2ZM274.7,394.8C285.8,370.1 312.2,325.9 346.3,305.4C383.6,282.9 335,390.3 427.3,489.8C519.5,589.3 586.5,623.8 655.1,581.5C723.7,539.2 746.5,518.1 753,518.7C764,519.6 772.9,536.1 764.4,589.5C763,598.2 761.3,606.5 759.3,614.2C828.7,575 871.7,516.3 889,520C912,524.8 918.3,617.8 852.1,721C811.8,784 768.8,830.7 741.4,857.5C724.3,874 699.7,880 677,873.1C512,822.6 315.5,733.8 202.9,652C193.8,645.2 186.8,635.9 183,625.1C171.8,593.7 161.3,548.1 168.5,495.3C179.5,414.2 222.7,352.9 245.2,335.9C261.9,323.2 267.1,362.4 274.7,394.8Z"
|
||||
android:fillColor="#FF4529"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -5,7 +5,7 @@
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 44 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 44 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 46 B |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 52 B |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 52 B |
|
Before Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +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.
|
||||
-->
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#540C00</color>
|
||||
</resources>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Copyright © 2021-2024 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.
|
||||
@@ -7,17 +7,11 @@
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Claw.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">#FF6C0000</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_foreground</item>
|
||||
<item name="windowSplashScreenBackground">@color/ic_launcher_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
||||
<item name="windowSplashScreenAnimationDuration">1000</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.Claw</item>
|
||||
</style>
|
||||
|
||||
<style name="Platform.Theme.Claw" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
|
||||
<style name="Base.Theme.Claw" parent="Platform.Theme.Claw">
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Claw" parent="Base.Theme.Claw" />
|
||||
<style name="Theme.Claw" parent="android:Theme.Material.NoActionBar" />
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
~ Copyright © 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.
|
||||
-->
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="349dp"
|
||||
android:minHeight="110dp"
|
||||
android:minResizeWidth="349dp"
|
||||
android:minResizeHeight="110dp"
|
||||
android:resizeMode="vertical|horizontal"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen" />
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
|
||||
#
|
||||
versioning-plugin.versionCode=13500
|
||||
versioning-plugin.versionName=1.35.0
|
||||
versioning-plugin.versionCode=14400
|
||||
versioning-plugin.versionName=1.44.0
|
||||
|
||||
@@ -29,5 +29,6 @@ dependencies {
|
||||
testImplementation(testFixtures(libs.eithernet))
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.kotlinx.serialization.json)
|
||||
testImplementation(libs.retrofit.kotlinxSerializationConverter)
|
||||
addTestDependencies(project)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -12,16 +12,15 @@ import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
/** Simple interface defining an API for lobste.rs */
|
||||
interface LobstersApi {
|
||||
|
||||
@GET("hottest.json")
|
||||
suspend fun getHottestPosts(@Query("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
@GET("page/{page}.json")
|
||||
suspend fun getHottestPosts(@Path("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
|
||||
@GET("newest.json")
|
||||
suspend fun getNewestPosts(@Query("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
@GET("newest/page/{page}.json")
|
||||
suspend fun getNewestPosts(@Path("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
|
||||
@GET("s/{postId}.json")
|
||||
suspend fun getPostDetails(@Path("postId") postId: String): ApiResult<LobstersPostDetails, Unit>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -8,7 +8,6 @@ package dev.msfjarvis.claw.api.converters
|
||||
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import java.lang.reflect.Type
|
||||
import okhttp3.ResponseBody
|
||||
import org.jsoup.Jsoup
|
||||
@@ -32,8 +31,7 @@ object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
|
||||
val url = titleElement.attr("href")
|
||||
val tags = elem.select("span.tags > a").map(Element::text)
|
||||
val (commentCount, commentsUrl) = getCommentsData(elem.select("span.comments_label"))
|
||||
val submitter =
|
||||
getSubmitter(elem.select("div.byline").first() ?: error("No byline element found"))
|
||||
val submitter = elem.select("div.byline > a.u-author").text()
|
||||
return LobstersPost(
|
||||
shortId = shortId,
|
||||
title = title,
|
||||
@@ -55,29 +53,11 @@ object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
|
||||
return (countString.toIntOrNull() ?: 0) to commentsUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a bare-bones [User] object given a byline [elem]. We only need this to be usable for
|
||||
* displaying in a list.
|
||||
*/
|
||||
private fun getSubmitter(elem: Element): User {
|
||||
val userElement = elem.select("a.u-author")
|
||||
val avatarElement = elem.select("img.avatar")
|
||||
val username = userElement.text()
|
||||
val avatarUrl = avatarElement.attr("src")
|
||||
return User(
|
||||
username = username,
|
||||
about = "",
|
||||
invitedBy = null,
|
||||
avatarUrl = avatarUrl,
|
||||
createdAt = "",
|
||||
)
|
||||
}
|
||||
|
||||
object Factory : Converter.Factory() {
|
||||
override fun responseBodyConverter(
|
||||
type: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
retrofit: Retrofit,
|
||||
): Converter<ResponseBody, List<LobstersPost>> {
|
||||
return SearchConverter
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -18,6 +18,7 @@ import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.api.converters.CSRFTokenConverter
|
||||
import dev.msfjarvis.claw.api.converters.SearchConverter
|
||||
import javax.inject.Named
|
||||
import javax.inject.Qualifier
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.CallAdapter
|
||||
@@ -40,7 +41,7 @@ object RetrofitModule {
|
||||
client: OkHttpClient,
|
||||
converterFactories: Map<Int, @JvmSuppressWildcards Converter.Factory>,
|
||||
callAdapterFactories: Map<Int, @JvmSuppressWildcards CallAdapter.Factory>,
|
||||
@BaseUrl baseUrl: String,
|
||||
@Named("LobstersURL") baseUrl: String,
|
||||
): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
@@ -56,7 +57,7 @@ object RetrofitModule {
|
||||
client: OkHttpClient,
|
||||
@SearchApi converterFactories: List<@JvmSuppressWildcards Converter.Factory>,
|
||||
callAdapterFactories: Map<Int, @JvmSuppressWildcards CallAdapter.Factory>,
|
||||
@BaseUrl baseUrl: String,
|
||||
@Named("LobstersURL") baseUrl: String,
|
||||
): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
@@ -90,15 +91,10 @@ object RetrofitModule {
|
||||
@Provides
|
||||
@SearchApi
|
||||
fun provideConverters(): List<@JvmSuppressWildcards Converter.Factory> =
|
||||
listOf(
|
||||
ApiResultConverterFactory,
|
||||
SearchConverter.Factory,
|
||||
)
|
||||
listOf(ApiResultConverterFactory, SearchConverter.Factory)
|
||||
|
||||
@Provides @BaseUrl fun provideBaseUrl(): String = LobstersApi.BASE_URL
|
||||
@Provides @Named("LobstersURL") fun provideLobstersUrl(): String = LobstersApi.BASE_URL
|
||||
}
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class BaseUrl
|
||||
|
||||
/** Internal Dagger [Qualifier] to identify dependencies exclusive to [LobstersSearchApi]. */
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) private annotation class SearchApi
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -33,7 +33,7 @@ class ApiTest {
|
||||
val posts = api.getHottestPosts(1)
|
||||
assertIs<Success<List<LobstersPost>>>(posts)
|
||||
val commentsOnlyPosts = posts.value.asSequence().filter { it.url.isEmpty() }.toSet()
|
||||
assertThat(commentsOnlyPosts).hasSize(2)
|
||||
assertThat(commentsOnlyPosts).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -10,7 +10,6 @@ import com.google.common.truth.Truth.assertThat
|
||||
import com.slack.eithernet.ApiResult
|
||||
import com.slack.eithernet.test.newEitherNetController
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import dev.msfjarvis.claw.util.TestUtils.assertIs
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -38,7 +37,7 @@ class SearchApiTest {
|
||||
createdAt = "",
|
||||
commentCount = 3,
|
||||
commentsUrl = "https://lobste.rs/s/gjlsdg/chatgpt_visits_emacs_doctor",
|
||||
submitter = User("xenodium", "", null, "/avatars/xenodium-16.png", ""),
|
||||
submitter = "xenodium",
|
||||
tags = listOf("ai", "emacs"),
|
||||
description = "",
|
||||
),
|
||||
@@ -50,7 +49,7 @@ class SearchApiTest {
|
||||
createdAt = "",
|
||||
commentCount = 0,
|
||||
commentsUrl = "https://lobste.rs/s/astcqf/implementing_question_answering_system",
|
||||
submitter = User("asteroid", "", null, "/avatars/asteroid-16.png", ""),
|
||||
submitter = "asteroid",
|
||||
tags = listOf("ai"),
|
||||
description = "",
|
||||
),
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"username":"msfjarvis","created_at":"2020-04-24T11:41:56.000-05:00","is_admin":false,"about":"Android and Kotlin developer\r\n","is_moderator":false,"karma":574,"avatar_url":"/avatars/msfjarvis-100.png","invited_by_user":"Amolith","github_username":"msfjarvis","twitter_username":"msfjarvis"}
|
||||
{"username":"msfjarvis","created_at":"2020-04-24T11:41:56.000-05:00","is_admin":false,"about":"Android and Kotlin developer, currently working for [Dyte](https://dyte.io/)","is_moderator":false,"karma":1343,"avatar_url":"/avatars/msfjarvis-100.png","invited_by_user":"Amolith","github_username":"msfjarvis"}
|
||||
@@ -1,13 +1,11 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.ManagedVirtualDevice
|
||||
|
||||
plugins {
|
||||
id(libs.plugins.android.test.get().pluginId)
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
@@ -30,21 +28,11 @@ android {
|
||||
matchingFallbacks += "release"
|
||||
}
|
||||
}
|
||||
|
||||
testOptions.managedDevices.devices {
|
||||
create<ManagedVirtualDevice>("api31") {
|
||||
device = "Pixel 6"
|
||||
apiLevel = 31
|
||||
systemImageSource = "aosp"
|
||||
}
|
||||
}
|
||||
|
||||
targetProjectPath = ":android"
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
managedDevices += "api31"
|
||||
useConnectedDevices = false
|
||||
useConnectedDevices = true
|
||||
enableEmulatorDisplay = false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
*/
|
||||
plugins { `kotlin-dsl` }
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
alias(libs.plugins.android.lint)
|
||||
}
|
||||
|
||||
kotlin.jvmToolchain { languageVersion.set(JavaLanguageVersion.of(17)) }
|
||||
kotlin.jvmToolchain(17)
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
@@ -61,14 +64,19 @@ gradlePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
lint.baseline = project.file("lint-baseline.xml")
|
||||
|
||||
dependencies {
|
||||
implementation(libs.build.agp)
|
||||
implementation(libs.build.cachefix)
|
||||
implementation(libs.build.kotlin.gradle)
|
||||
implementation(libs.build.r8)
|
||||
implementation(libs.build.semver)
|
||||
implementation(libs.build.sentry)
|
||||
implementation(libs.build.spotless)
|
||||
implementation(libs.build.vcu)
|
||||
implementation(libs.build.versions)
|
||||
|
||||
// Expose the generated version catalog API to the plugin.
|
||||
implementation(files(libs::class.java.superclass.protectionDomain.codeSource.location))
|
||||
|
||||
lintChecks(libs.androidx.lint)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -8,31 +8,42 @@
|
||||
|
||||
rootProject.name = "build-logic"
|
||||
|
||||
pluginManagement {
|
||||
plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" }
|
||||
repositories {
|
||||
exclusiveContent {
|
||||
forRepository { gradlePluginPortal() }
|
||||
filter {
|
||||
includeModule(
|
||||
"org.gradle.toolchains.foojay-resolver-convention",
|
||||
"org.gradle.toolchains.foojay-resolver-convention.gradle.plugin",
|
||||
)
|
||||
includeModule("org.gradle.toolchains", "foojay-resolver")
|
||||
includeModule("org.gradle.kotlin.kotlin-dsl", "org.gradle.kotlin.kotlin-dsl.gradle.plugin")
|
||||
includeModule("org.gradle.kotlin", "gradle-kotlin-dsl-plugins")
|
||||
}
|
||||
}
|
||||
exclusiveContent {
|
||||
forRepository { google() }
|
||||
filter {
|
||||
includeGroup("androidx.databinding")
|
||||
includeGroupByRegex("com.android.*")
|
||||
includeGroup("com.google.testing.platform")
|
||||
}
|
||||
}
|
||||
mavenCentral { mavenContent { releasesOnly() } }
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
exclusiveContent {
|
||||
forRepository(::google)
|
||||
filter {
|
||||
includeGroup("androidx.databinding")
|
||||
includeGroup("com.android")
|
||||
includeGroup("com.android.tools.analytics-library")
|
||||
includeGroup("com.android.tools.build")
|
||||
includeGroup("com.android.tools.build.jetifier")
|
||||
includeGroup("com.android.databinding")
|
||||
includeGroup("com.android.tools.ddms")
|
||||
includeGroup("com.android.tools.layoutlib")
|
||||
includeGroup("com.android.tools.lint")
|
||||
includeGroup("com.android.tools.utp")
|
||||
includeGroup("androidx.lint")
|
||||
includeGroupByRegex("com.android.*")
|
||||
includeGroup("com.google.testing.platform")
|
||||
includeModule("com.android.tools", "annotations")
|
||||
includeModule("com.android.tools", "common")
|
||||
includeModule("com.android.tools", "desugar_jdk_libs")
|
||||
includeModule("com.android.tools", "desugar_jdk_libs_configuration")
|
||||
includeModule("com.android.tools", "dvlib")
|
||||
includeModule("com.android.tools", "play-sdk-proto")
|
||||
includeModule("com.android.tools", "repository")
|
||||
includeModule("com.android.tools", "sdklib")
|
||||
includeModule("com.android.tools", "sdk-common")
|
||||
}
|
||||
}
|
||||
exclusiveContent {
|
||||
@@ -43,11 +54,7 @@ dependencyResolutionManagement {
|
||||
includeModule("gradle.plugin.org.gradle.android", "android-cache-fix-gradle-plugin")
|
||||
}
|
||||
}
|
||||
exclusiveContent {
|
||||
forRepository { maven("https://storage.googleapis.com/r8-releases/raw/main") }
|
||||
filter { includeModule("com.android.tools", "r8") }
|
||||
}
|
||||
mavenCentral()
|
||||
mavenCentral { mavenContent { releasesOnly() } }
|
||||
}
|
||||
versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } }
|
||||
}
|
||||
|
||||
@@ -15,14 +15,13 @@ import com.android.build.api.variant.HasUnitTestBuilder
|
||||
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
||||
import com.android.build.gradle.BaseExtension
|
||||
import dev.msfjarvis.claw.gradle.LintConfig.configureLint
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.gradle.android.AndroidCacheFixPlugin
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.findByType
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
class AndroidCommonPlugin : Plugin<Project> {
|
||||
|
||||
@@ -59,9 +58,9 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
}
|
||||
project.extensions.findByType<ApplicationExtension>()?.lint?.configureLint(project)
|
||||
project.extensions.findByType<LibraryExtension>()?.lint?.configureLint(project)
|
||||
val catalog = project.extensions.getByType<VersionCatalogsExtension>()
|
||||
val libs = catalog.named("libs")
|
||||
project.dependencies.addProvider("lintChecks", libs.findLibrary("slack-compose-lints").get())
|
||||
val libs = project.extensions.getByName("libs") as LibrariesForLibs
|
||||
project.dependencies.addProvider("lintChecks", libs.slack.compose.lints)
|
||||
project.dependencies.addProvider("lintChecks", libs.slack.lints)
|
||||
}
|
||||
|
||||
private fun Project.configureSlimTests() {
|
||||
@@ -73,7 +72,7 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
beforeVariants(selector().all()) {
|
||||
if (it.name == "debug") return@beforeVariants
|
||||
(it as HasUnitTestBuilder).enableUnitTest = false
|
||||
it.enableAndroidTest = false
|
||||
it.androidTest.enable = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +81,7 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
beforeVariants(selector().all()) {
|
||||
if (it.name == "debug") return@beforeVariants
|
||||
(it as HasUnitTestBuilder).enableUnitTest = false
|
||||
it.enableAndroidTest = false
|
||||
it.androidTest.enable = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -33,24 +33,22 @@ class ApplicationPlugin : Plugin<Project> {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
all {
|
||||
setProguardFiles(
|
||||
listOf(
|
||||
"proguard-android-optimize.pro",
|
||||
"proguard-rules.pro",
|
||||
"proguard-rules-missing-classes.pro",
|
||||
)
|
||||
buildTypes.configureEach {
|
||||
setProguardFiles(
|
||||
listOf(
|
||||
"proguard-android-optimize.pro",
|
||||
"proguard-rules.pro",
|
||||
"proguard-rules-missing-classes.pro",
|
||||
)
|
||||
}
|
||||
named("release") { isMinifyEnabled = true }
|
||||
named("debug") {
|
||||
)
|
||||
if (name == "release") {
|
||||
isMinifyEnabled = !project.providers.environmentVariable("DISABLE_MINIFY").isPresent
|
||||
} else if (name == "debug") {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
project.configureBuildSigning()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,36 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2024 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
|
||||
import com.github.zafarkhaja.semver.Version
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import nl.littlerobots.vcu.plugin.VersionCatalogUpdateExtension
|
||||
import nl.littlerobots.vcu.plugin.VersionCatalogUpdatePlugin
|
||||
import nl.littlerobots.vcu.plugin.versionSelector
|
||||
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.withType
|
||||
|
||||
@Suppress("Unused")
|
||||
class DependencyUpdatesPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
project.pluginManager.apply(VersionsPlugin::class)
|
||||
project.pluginManager.apply(VersionCatalogUpdatePlugin::class)
|
||||
project.tasks.withType<DependencyUpdatesTask>().configureEach {
|
||||
rejectVersionIf { isNonStable(candidate.version) && !isNonStable(currentVersion) }
|
||||
checkConstraints = true
|
||||
checkBuildEnvironmentConstraints = true
|
||||
checkForGradleUpdate = true
|
||||
}
|
||||
project.extensions.configure<VersionCatalogUpdateExtension> {
|
||||
keep.keepUnusedLibraries.set(true)
|
||||
versionSelector {
|
||||
val currentVersion = Version.tryParse(it.currentVersion).getOrNull()
|
||||
val newVersion = Version.tryParse(it.candidate.version).getOrNull()
|
||||
if (currentVersion == null || newVersion == null) {
|
||||
false
|
||||
} else {
|
||||
newVersion.isHigherThan(currentVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import dev.msfjarvis.claw.gradle.KotlinCommonPlugin.Companion.JVM_TOOLCHAIN_ACTION
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
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.withType
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
@Suppress("Unused")
|
||||
class KotlinAndroidPlugin : Plugin<Project> {
|
||||
@@ -23,5 +26,25 @@ class KotlinAndroidPlugin : Plugin<Project> {
|
||||
apply(KotlinCommonPlugin::class)
|
||||
}
|
||||
project.extensions.getByType<KotlinProjectExtension>().jvmToolchain(JVM_TOOLCHAIN_ACTION)
|
||||
val libs = project.extensions.getByName("libs") as LibrariesForLibs
|
||||
val composeCompilerVersion = libs.versions.composeCompiler.get()
|
||||
val kotlinVersion = libs.versions.kotlin.get()
|
||||
val matches = COMPOSE_COMPILER_VERSION_REGEX.find(composeCompilerVersion)
|
||||
if (matches != null) {
|
||||
val (compilerKotlinVersion) = matches.destructured
|
||||
if (compilerKotlinVersion != kotlinVersion) {
|
||||
project.tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions.freeCompilerArgs.addAll(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=$kotlinVersion",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// Matches against 1.5.0-dev-k1.9.0-6a60475e07f
|
||||
val COMPOSE_COMPILER_VERSION_REGEX = "\\d.\\d.\\d-dev-k(\\d.\\d.\\d+)-[a-z0-9]+".toRegex()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -31,7 +31,7 @@ class KotlinCommonPlugin : Plugin<Project> {
|
||||
compilerOptions {
|
||||
allWarningsAsErrors.set(true)
|
||||
freeCompilerArgs.addAll(ADDITIONAL_COMPILER_ARGS)
|
||||
languageVersion.set(KotlinVersion.KOTLIN_1_8)
|
||||
languageVersion.set(KotlinVersion.KOTLIN_1_9)
|
||||
}
|
||||
}
|
||||
withType<Test>().configureEach {
|
||||
@@ -43,10 +43,7 @@ class KotlinCommonPlugin : Plugin<Project> {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val ADDITIONAL_COMPILER_ARGS =
|
||||
listOf(
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
)
|
||||
private val ADDITIONAL_COMPILER_ARGS = listOf("-opt-in=kotlin.RequiresOptIn")
|
||||
|
||||
val JVM_TOOLCHAIN_ACTION =
|
||||
Action<JavaToolchainSpec> { languageVersion.set(JavaLanguageVersion.of(17)) }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -41,7 +41,7 @@ class KotlinKaptPlugin : Plugin<Project> {
|
||||
}
|
||||
}
|
||||
project.tasks
|
||||
.matching { it.name.startsWith("kapt") && it.name.endsWith("UnitTestKotlin") }
|
||||
.named { it.startsWith("kapt") && it.endsWith("UnitTestKotlin") }
|
||||
.configureEach { enabled = false }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -34,7 +34,11 @@ object LintConfig {
|
||||
}
|
||||
baseline = project.file("lint-baseline.xml")
|
||||
// This is extremely annoying
|
||||
disable += "AndroidGradlePluginVersion"
|
||||
disable += "GradleDependency"
|
||||
disable += "NewerVersionAvailable"
|
||||
// Can't do anything about this
|
||||
disable += "ObsoleteLintCustomCheck"
|
||||
}
|
||||
|
||||
fun configureRootProject(project: Project) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -10,54 +10,71 @@ import com.android.build.api.variant.ApplicationAndroidComponentsExtension
|
||||
import io.sentry.android.gradle.extensions.InstrumentationFeature
|
||||
import io.sentry.android.gradle.extensions.SentryPluginExtension
|
||||
import java.util.EnumSet
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
@Suppress("Unused")
|
||||
class SentryPlugin : Plugin<Project> {
|
||||
|
||||
override fun apply(project: Project) {
|
||||
project.pluginManager.withPlugin("com.android.application") {
|
||||
val catalog = project.extensions.getByType<VersionCatalogsExtension>()
|
||||
val libs = catalog.named("libs")
|
||||
val libs = project.extensions.getByName("libs") as LibrariesForLibs
|
||||
project.extensions.configure<ApplicationAndroidComponentsExtension> {
|
||||
onVariants(selector().all()) { variant ->
|
||||
val sentryDsn = project.providers.environmentVariable(SENTRY_DSN_PROPERTY)
|
||||
val enableSentry =
|
||||
project.providers.gradleProperty(SENTRY_ENABLE_GRADLE_PROPERTY).isPresent
|
||||
variant.manifestPlaceholders.put("sentryDsn", sentryDsn.getOrElse(""))
|
||||
variant.manifestPlaceholders.put("enableSentry", "${variant.name == "release"}")
|
||||
variant.manifestPlaceholders.put(
|
||||
"sentryEnvironment",
|
||||
when {
|
||||
variant.name.contains("release", true) && enableSentry -> "production"
|
||||
variant.name.contains("release", true) && !enableSentry -> "nightly"
|
||||
else -> "dev"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
project.plugins.apply(io.sentry.android.gradle.SentryPlugin::class)
|
||||
project.extensions.configure<SentryPluginExtension> {
|
||||
val enableMappings =
|
||||
project.providers.gradleProperty(SENTRY_UPLOAD_MAPPINGS_PROPERTY).isPresent
|
||||
includeProguardMapping.set(enableMappings)
|
||||
autoUploadProguardMapping.set(enableMappings)
|
||||
includeProguardMapping.set(true)
|
||||
autoUploadProguardMapping.set(true)
|
||||
uploadNativeSymbols.set(false)
|
||||
autoUploadNativeSymbols.set(false)
|
||||
includeNativeSources.set(false)
|
||||
ignoredVariants.set(emptySet())
|
||||
ignoredBuildTypes.set(setOf("benchmark", "debug"))
|
||||
ignoredBuildTypes.set(setOf("benchmark"))
|
||||
ignoredFlavors.set(emptySet())
|
||||
tracingInstrumentation {
|
||||
enabled.set(true)
|
||||
debug.set(false)
|
||||
logcat.enabled.set(true)
|
||||
forceInstrumentDependencies.set(false)
|
||||
features.set(EnumSet.allOf(InstrumentationFeature::class.java))
|
||||
}
|
||||
experimentalGuardsquareSupport.set(false)
|
||||
dexguardEnabled.set(false)
|
||||
autoInstallation {
|
||||
enabled.set(true)
|
||||
sentryVersion.set(libs.findVersion("sentry-sdk").get().requiredVersion)
|
||||
sentryVersion.set(libs.versions.sentry.sdk)
|
||||
}
|
||||
includeDependenciesReport.set(true)
|
||||
includeSourceContext.set(true)
|
||||
autoUploadSourceContext.set(true)
|
||||
additionalSourceDirsForSourceContext.set(emptySet())
|
||||
debug.set(false)
|
||||
org.set("claw")
|
||||
projectName.set("compose-lobsters")
|
||||
authToken.set(project.providers.environmentVariable("SENTRY_AUTH_TOKEN"))
|
||||
url.set(null)
|
||||
telemetry.set(false)
|
||||
telemetryDsn.set(null)
|
||||
}
|
||||
with(project.dependencies) {
|
||||
addProvider("implementation", platform(libs.findLibrary("sentry-bom").get()))
|
||||
addProvider("implementation", platform(libs.sentry.bom))
|
||||
addProvider("implementation", libs.sentry.android)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +82,6 @@ class SentryPlugin : Plugin<Project> {
|
||||
private companion object {
|
||||
|
||||
private const val SENTRY_DSN_PROPERTY = "SENTRY_DSN"
|
||||
private const val SENTRY_UPLOAD_MAPPINGS_PROPERTY = "sentryUploadMappings"
|
||||
private const val SENTRY_ENABLE_GRADLE_PROPERTY = "enableSentry"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -8,11 +8,13 @@ package dev.msfjarvis.claw.gradle
|
||||
|
||||
import com.diffplug.gradle.spotless.SpotlessExtension
|
||||
import com.diffplug.gradle.spotless.SpotlessPlugin
|
||||
import com.diffplug.gradle.spotless.SpotlessTask
|
||||
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.configure
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
|
||||
@Suppress("Unused")
|
||||
class SpotlessPlugin : Plugin<Project> {
|
||||
@@ -43,13 +45,16 @@ class SpotlessPlugin : Plugin<Project> {
|
||||
endWithNewline()
|
||||
licenseHeaderFile(
|
||||
project.file("spotless/license.xml"),
|
||||
"<manifest|<resources|<full-backup-content|<data-extraction-rules|<adaptive-icon"
|
||||
"<(adaptive-icon|appwidget-provider|data-extraction-rules|full-backup-content|manifest|vector|resources)",
|
||||
)
|
||||
}
|
||||
}
|
||||
project.tasks.withType<SpotlessTask>().configureEach {
|
||||
notCompatibleWithConfigurationCache("https://github.com/diffplug/spotless/issues/987")
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val KTFMT_VERSION = "0.45"
|
||||
private const val KTFMT_VERSION = "0.47"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* 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.
|
||||
@@ -12,6 +12,7 @@ import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import kotlin.io.path.createDirectory
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
@@ -21,6 +22,7 @@ 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.Optional
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
import org.gradle.api.tasks.PathSensitive
|
||||
import org.gradle.api.tasks.PathSensitivity
|
||||
@@ -33,6 +35,7 @@ abstract class CollectApksTask : DefaultTask() {
|
||||
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.NONE)
|
||||
@get:Optional
|
||||
abstract val mappingFile: RegularFileProperty
|
||||
|
||||
@get:Input abstract val variantName: Property<String>
|
||||
@@ -41,27 +44,30 @@ abstract class CollectApksTask : DefaultTask() {
|
||||
|
||||
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
|
||||
|
||||
override fun getGroup(): String {
|
||||
return "artifact collection"
|
||||
}
|
||||
|
||||
@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 outputDir = outputDirectory.asFile.get().toPath()
|
||||
Files.walk(outputDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete)
|
||||
outputDir.createDirectory()
|
||||
val builtArtifacts =
|
||||
builtArtifactsLoader.get().load(apkFolder.get()) ?: error("Cannot load APKs")
|
||||
builtArtifacts.elements.forEach { artifact ->
|
||||
Files.copy(
|
||||
Paths.get(artifact.outputFile),
|
||||
outputDir.resolve("Claw-${variantName.get()}-${artifact.versionName}.apk").toPath(),
|
||||
outputDir.resolve("Claw-${variantName.get()}-${artifact.versionName}.apk"),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
mappingFile.orNull?.apply {
|
||||
Files.copy(
|
||||
asFile.toPath(),
|
||||
outputDir.resolve("mapping.txt"),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
Files.copy(
|
||||
mappingFile.get().asFile.toPath(),
|
||||
outputDir.resolve("mapping.txt").toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* 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.
|
||||
@@ -10,19 +10,28 @@ import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption
|
||||
import kotlin.io.path.createDirectory
|
||||
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.Input
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.Optional
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
import org.gradle.api.tasks.PathSensitive
|
||||
import org.gradle.api.tasks.PathSensitivity
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
abstract class CollectBundleTask : DefaultTask() {
|
||||
@get:InputFile abstract val bundleFile: RegularFileProperty
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.NONE)
|
||||
abstract val bundleFile: RegularFileProperty
|
||||
|
||||
@get:InputFile abstract val mappingFile: RegularFileProperty
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.NONE)
|
||||
@get:Optional
|
||||
abstract val mappingFile: RegularFileProperty
|
||||
|
||||
@get:Input abstract val variantName: Property<String>
|
||||
|
||||
@@ -30,23 +39,26 @@ abstract class CollectBundleTask : DefaultTask() {
|
||||
|
||||
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
|
||||
|
||||
override fun getGroup(): String {
|
||||
return "artifact collection"
|
||||
}
|
||||
|
||||
@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()
|
||||
val outputDir = outputDirectory.asFile.get().toPath()
|
||||
Files.walk(outputDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete)
|
||||
outputDir.createDirectory()
|
||||
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(),
|
||||
outputDir.resolve("Claw-${variantName.get()}-${versionName.get()}.aab"),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
mappingFile.orNull?.apply {
|
||||
Files.copy(
|
||||
asFile.toPath(),
|
||||
outputDir.resolve("mapping.txt"),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,29 +6,27 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.ExternalModuleDependency
|
||||
import org.gradle.api.artifacts.MinimalExternalModuleDependency
|
||||
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||
import org.gradle.kotlin.dsl.DependencyHandlerScope
|
||||
import org.gradle.kotlin.dsl.exclude
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
/** Extension function to configure JUnit5 dependencies with the Truth assertion library. */
|
||||
fun DependencyHandlerScope.addTestDependencies(project: Project) {
|
||||
val catalog = project.extensions.getByType<VersionCatalogsExtension>()
|
||||
val libs = catalog.named("libs")
|
||||
addProvider("testImplementation", libs.findLibrary("junit-jupiter-api").get())
|
||||
val libs = project.extensions.getByName("libs") as LibrariesForLibs
|
||||
addProvider("testImplementation", libs.junit.jupiter.api)
|
||||
addProvider<MinimalExternalModuleDependency, ExternalModuleDependency>(
|
||||
"testImplementation",
|
||||
libs.findLibrary("truth").get(),
|
||||
libs.truth,
|
||||
) {
|
||||
exclude(group = "junit", module = "junit")
|
||||
}
|
||||
addProvider("testRuntimeOnly", libs.findLibrary("junit-jupiter-engine").get())
|
||||
addProvider("testRuntimeOnly", libs.junit.jupiter.engine)
|
||||
addProvider<MinimalExternalModuleDependency, ExternalModuleDependency>(
|
||||
"testRuntimeOnly",
|
||||
libs.findLibrary("junit-legacy").get(),
|
||||
libs.junit.legacy,
|
||||
) {
|
||||
// See https://github.com/google/truth/issues/333
|
||||
because("Truth needs it")
|
||||
|
||||
@@ -1,37 +1,34 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.signing
|
||||
|
||||
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
|
||||
import com.android.build.api.dsl.ApplicationBuildType
|
||||
import com.android.build.api.dsl.CommonExtension
|
||||
import java.util.Properties
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
private const val KEYSTORE_CONFIG_PATH = "keystore.properties"
|
||||
|
||||
/** Configure signing for all build types. */
|
||||
@Suppress("UnstableApiUsage")
|
||||
internal fun Project.configureBuildSigning() {
|
||||
val keystoreConfigFile = rootProject.layout.projectDirectory.file(KEYSTORE_CONFIG_PATH)
|
||||
if (keystoreConfigFile.asFile.exists()) {
|
||||
extensions.configure<BaseAppModuleExtension> {
|
||||
extensions.configure<CommonExtension<*, ApplicationBuildType, *, *, *, *>>("android") {
|
||||
val contents = providers.fileContents(keystoreConfigFile).asText
|
||||
val keystoreProperties = Properties()
|
||||
keystoreProperties.load(contents.get().byteInputStream())
|
||||
signingConfigs {
|
||||
register("release") {
|
||||
val releaseSigningConfig =
|
||||
signingConfigs.register("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
val signingConfig = signingConfigs.getByName("release")
|
||||
buildTypes.all { setSigningConfig(signingConfig) }
|
||||
buildTypes.configureEach { signingConfig = releaseSigningConfig.get() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||