Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a223470da5 | ||
|
|
1e5421fbc2 | ||
|
|
4087ce1941 | ||
|
|
63e8ca3df8 | ||
|
|
a9a304cd96 | ||
|
|
8dd15b8276 | ||
|
|
5c2793fff3 | ||
|
|
245437c739 | ||
|
|
2fdbff6f12 | ||
|
|
2bc86509cf | ||
|
|
1dfea689c6 | ||
|
|
a0276c4873 | ||
|
|
fbb13c5acf | ||
|
|
95b3030e19 | ||
|
|
0f2bb909d4 | ||
|
|
d2e1ae91da | ||
|
|
82a024baee | ||
|
|
3021b55fce | ||
|
|
7506dd5ca9 | ||
|
|
d3d4519daa | ||
|
|
83b58aa7fb | ||
|
|
aa2093424c | ||
|
|
501721fca9 | ||
|
|
aa088f5dd7 | ||
|
|
b0b47d98a5 | ||
|
|
2109bcf12c | ||
|
|
6bc6b13ea6 | ||
|
|
0f20366cf3 | ||
|
|
723ab0add3 | ||
|
|
77dd4496a5 | ||
|
|
1e8a93fde8 | ||
|
|
fe0e0ea10e | ||
|
|
d26245da17 | ||
|
|
aa60160378 | ||
|
|
943cddd8a4 | ||
|
|
d5a0bb256f | ||
|
|
1ebf610794 | ||
|
|
784dcf16f8 | ||
|
|
cc5b95e9c8 | ||
|
|
e317a1f4f3 | ||
|
|
ed4c573ee4 | ||
|
|
7037d55bf2 | ||
|
|
4ab76c60bb | ||
|
|
0fe83e8583 | ||
|
|
68bcae572b | ||
|
|
edfe72f12a | ||
|
|
4ae70e2129 | ||
|
|
54c1d4438c | ||
|
|
3e673b09b6 | ||
|
|
77439a5799 | ||
|
|
adf1af691f | ||
|
|
1d60fca7eb | ||
|
|
8224b71465 | ||
|
|
bf52966772 | ||
|
|
dc06410fc4 | ||
|
|
7b78438385 | ||
|
|
077c855ef3 | ||
|
|
f7f3f55261 | ||
|
|
74af4ad5b6 | ||
|
|
6988b5d412 | ||
|
|
ecc7b79720 | ||
|
|
1de4916c9c | ||
|
|
4f3bafc051 | ||
|
|
8651a4f66b | ||
|
|
6f424ae2d5 | ||
|
|
c8d1491196 | ||
|
|
e68af9be16 | ||
|
|
482258a162 | ||
|
|
ee44d99f94 | ||
|
|
6a7de43b15 | ||
|
|
89b490d841 | ||
|
|
1cbd3b5bd0 | ||
|
|
1dfcda4a64 | ||
|
|
18f6689c47 | ||
|
|
022e05696b | ||
|
|
e2bbe9c264 | ||
|
|
d0762b5cae | ||
|
|
f452833f6d | ||
|
|
eb32c84b18 | ||
|
|
8f19f794a1 | ||
|
|
6bb5994a37 | ||
|
|
26ccc0d687 | ||
|
|
39e3d783cd | ||
|
|
c50790e4f9 | ||
|
|
d55ff24add | ||
|
|
9b2a1cba64 | ||
|
|
327cce9075 | ||
|
|
466ec7c79f | ||
|
|
06c9fa1477 | ||
|
|
6223198191 | ||
|
|
789e4a124d | ||
|
|
e3745d0c58 | ||
|
|
26b35d4dd8 | ||
|
|
691a18b889 | ||
|
|
789339eacb | ||
|
|
78a470dfdd | ||
|
|
eb3185d883 | ||
|
|
bf3d507990 | ||
|
|
faa79bcb12 | ||
|
|
e8dcb0dfba | ||
|
|
d502716652 | ||
|
|
bb29924822 | ||
|
|
101cd8894f | ||
|
|
995caae174 | ||
|
|
1a7b1fbdd1 | ||
|
|
9c6008b36e | ||
|
|
7875c63bde | ||
|
|
68fe458f7d | ||
|
|
f611b9aa25 | ||
|
|
67b253ba62 | ||
|
|
a8f559fe06 | ||
|
|
45dab6d513 | ||
|
|
13f6467a16 | ||
|
|
871abf7873 | ||
|
|
866b487256 | ||
|
|
63adacbea8 | ||
|
|
65b10a0071 | ||
|
|
e20af353ac | ||
|
|
ba0f62febe | ||
|
|
3a1012288d | ||
|
|
8424798257 | ||
|
|
a7e5ede51a | ||
|
|
1197e7fd7f | ||
|
|
72a684dbd1 | ||
|
|
ba47885490 | ||
|
|
9f153d62ba | ||
|
|
9bceb5c680 | ||
|
|
99d69f1bef | ||
|
|
1a72d2cbbe | ||
|
|
e22ecb51d3 | ||
|
|
68f779c395 | ||
|
|
954341c0fb | ||
|
|
798ffb8bb6 | ||
|
|
a137900ac7 | ||
|
|
fdd4f3d1dc | ||
|
|
4c975471fd | ||
|
|
e2d530635d | ||
|
|
371780a1fd | ||
|
|
b1d6cbcb39 | ||
|
|
a07c55a430 | ||
|
|
8415c46492 | ||
|
|
771892bfb1 | ||
|
|
66e69a333c | ||
|
|
67dccf949c | ||
|
|
f411460841 | ||
|
|
17bd98d96d | ||
|
|
fa2c0bc047 | ||
|
|
3dbca71de3 | ||
|
|
2c4f0fa4e2 | ||
|
|
ce7e9db135 | ||
|
|
babd40109b | ||
|
|
f1fcaf53f4 | ||
|
|
d678843230 | ||
|
|
f7d9447ca0 | ||
|
|
03b0097ae1 | ||
|
|
a53af77705 | ||
|
|
05a8b9bc67 | ||
|
|
9815ef1910 | ||
|
|
7c9f61eb5e | ||
|
|
8f69490c11 | ||
|
|
a7dd2c52f5 | ||
|
|
100387c759 | ||
|
|
ce8d79f08c | ||
|
|
4a75ea99a3 | ||
|
|
bd8148b74b | ||
|
|
2b9810c5d4 | ||
|
|
0e5052a5bb | ||
|
|
ffe40d41fd | ||
|
|
46cf368b99 | ||
|
|
1d0386f332 | ||
|
|
4a621fa120 | ||
|
|
a53dece4d1 | ||
|
|
a2c6fb40bf | ||
|
|
b132410282 | ||
|
|
4ebb99bf7d | ||
|
|
3a15eea2fd | ||
|
|
3a6f7be808 | ||
|
|
1cb978aa92 | ||
|
|
e0de9c97de | ||
|
|
7ae94ee2e6 | ||
|
|
5d8467bc67 | ||
|
|
378f4e413a | ||
|
|
6727424092 | ||
|
|
9594a4dc9e | ||
|
|
f7fb007f85 | ||
|
|
710d2d2c35 | ||
|
|
c3139bfc9d | ||
|
|
e2bcabe4e5 | ||
|
|
9610241489 | ||
|
|
cef05306ba | ||
|
|
c4aad66c03 | ||
|
|
7f2b052df5 | ||
|
|
0a3bf1e06a | ||
|
|
a779bee74c | ||
|
|
7ac87046ae | ||
|
|
742a30bfdd | ||
|
|
ecad03e749 | ||
|
|
d2289da184 | ||
|
|
2e91db6908 | ||
|
|
679e177d47 | ||
|
|
feb4aa7b17 | ||
|
|
c6bae7d92a | ||
|
|
0277b987da | ||
|
|
5024bfa160 | ||
|
|
60c3d91087 | ||
|
|
559cfbb223 | ||
|
|
8e1e63b573 | ||
|
|
b0c6806226 | ||
|
|
6e29d61799 | ||
|
|
1ee9d92b40 | ||
|
|
9c07169b1b | ||
|
|
81f4d35f93 | ||
|
|
361082ded4 | ||
|
|
4fe43dbdc9 | ||
|
|
cef45bd3e5 | ||
|
|
c45dd76fc4 | ||
|
|
9a1f42764e | ||
|
|
e62a0eaa84 | ||
|
|
6296afe51a | ||
|
|
cf390de486 | ||
|
|
99a45ab5df | ||
|
|
5257d0876a | ||
|
|
bb3cef2f5b | ||
|
|
a791588423 | ||
|
|
535624bf22 | ||
|
|
b5a7225bd8 | ||
|
|
8c0734a19a | ||
|
|
71afc634b9 | ||
|
|
fbaede8215 | ||
|
|
948aea7ea8 | ||
|
|
6acab19b28 | ||
|
|
2555140ca1 | ||
|
|
4a5980fd62 | ||
|
|
215afd208e | ||
|
|
806ef65e08 | ||
|
|
35e143a458 | ||
|
|
5b4d1fc48e | ||
|
|
c9098bbd44 | ||
|
|
b7eecd10b4 | ||
|
|
c471fee4ee | ||
|
|
e3ed059fde | ||
|
|
44355de333 | ||
|
|
92dc6d9e4c | ||
|
|
68ca569b9d | ||
|
|
105a33e335 | ||
|
|
7862b4b5c8 | ||
|
|
b12b5c800f | ||
|
|
b56de40b12 | ||
|
|
8243b8998c | ||
|
|
195b50f919 | ||
|
|
2d7903efc1 | ||
|
|
e2ab469ecc | ||
|
|
4ccf8959d9 | ||
|
|
b73a758501 | ||
|
|
cfaa1c17a0 | ||
|
|
47332c22ec | ||
|
|
ce64454cec | ||
|
|
b3313efbbe | ||
|
|
bc14e58513 | ||
|
|
83eb3c13b2 | ||
|
|
59acbe2a37 | ||
|
|
7e31de67b4 | ||
|
|
37230e03c4 | ||
|
|
fbf937dce2 | ||
|
|
ee9db517f7 | ||
|
|
a4dcdc6f88 | ||
|
|
c9c908d1a5 | ||
|
|
2ce804ed90 | ||
|
|
3b044a8d23 | ||
|
|
bdca0799db | ||
|
|
ca37557297 | ||
|
|
9c0c270f5c | ||
|
|
8a9cc844b9 | ||
|
|
452ba7dbf9 | ||
|
|
9edfc23ee3 | ||
|
|
3057f5fede | ||
|
|
157748bc61 | ||
|
|
68a5de84f7 | ||
|
|
b69fa43d29 | ||
|
|
6f76a15ded | ||
|
|
9fe6e34015 | ||
|
|
847858d852 | ||
|
|
8735f7f8e8 | ||
|
|
5061843779 | ||
|
|
b8480f1994 | ||
|
|
60a7b815ea | ||
|
|
bff56aa52b | ||
|
|
3137a1bb42 | ||
|
|
127a69249e | ||
|
|
9361c7ef47 | ||
|
|
e8a9594e4c | ||
|
|
0a8ca5719d | ||
|
|
d87e2d1b9c | ||
|
|
774c6cea9b | ||
|
|
5753f65737 | ||
|
|
17965097bc | ||
|
|
f1d06116bc | ||
|
|
319e30c174 | ||
|
|
8e8b710603 | ||
|
|
a142563ad8 | ||
|
|
9bd0df2aa8 | ||
|
|
54ec5a4c8d | ||
|
|
c79b7114fb | ||
|
|
a6fb1fd788 | ||
|
|
5a268fa5bd | ||
|
|
431dabbc09 | ||
|
|
d7f71878f6 | ||
|
|
196dfd056b | ||
|
|
6d3e749ad6 | ||
|
|
e808e77ab8 | ||
|
|
d8b2ff6c8d | ||
|
|
55635f1101 | ||
|
|
a70801c635 | ||
|
|
3a60e8f2a1 | ||
|
|
80183fd248 | ||
|
|
7cbb8e6237 | ||
|
|
32cba40e91 | ||
|
|
a8cef93096 | ||
|
|
b3ea9c6faf | ||
|
|
215b259288 | ||
|
|
ab1f276a08 | ||
|
|
ca03561df0 | ||
|
|
687cb32f7c | ||
|
|
12e57298af | ||
|
|
5abb5f109b | ||
|
|
c8eaf8d769 | ||
|
|
24a4d20a03 | ||
|
|
57f95d790d | ||
|
|
09a6e3e32b | ||
|
|
491be53dc3 | ||
|
|
d1e6260136 | ||
|
|
c71b33a07f | ||
|
|
7d2094866b | ||
|
|
a3e9957e49 | ||
|
|
cec2d70fda | ||
|
|
ec4066c472 | ||
|
|
433727a386 | ||
|
|
1238012e0b | ||
|
|
96e53f104d | ||
|
|
e1fed9ccc8 | ||
|
|
ced43b00be | ||
|
|
0ca18d9216 | ||
|
|
1aa2dee779 | ||
|
|
7f4a382f40 | ||
|
|
ed5b6c21da | ||
|
|
d0bf2a4fb2 | ||
|
|
2b9680d3d8 | ||
|
|
d19c1e11be | ||
|
|
809cec61cd | ||
|
|
13feb83063 | ||
|
|
40b7884520 | ||
|
|
75b05b83c1 | ||
|
|
9afe913548 | ||
|
|
44d589e729 | ||
|
|
ace1a3c94c | ||
|
|
31c94dc8cc | ||
|
|
493ab8712c | ||
|
|
1fff9d9aa2 | ||
|
|
9357362ef6 | ||
|
|
7b07a4676b | ||
|
|
e272427031 | ||
|
|
7882b86b39 | ||
|
|
afb00c64fc | ||
|
|
3a7727e563 | ||
|
|
d28c9c2e61 | ||
|
|
99aff514f2 | ||
|
|
0e8f1a51bf | ||
|
|
52829fedb2 | ||
|
|
4e479cf0f1 | ||
|
|
2020f43dd6 | ||
|
|
659eaf9789 | ||
|
|
a410267f99 | ||
|
|
6f248c529e | ||
|
|
142fafba23 | ||
|
|
7119cda579 | ||
|
|
e3c1213be6 | ||
|
|
5487615436 | ||
|
|
619b771aff | ||
|
|
a12a1f92a5 | ||
|
|
dc70293bd4 | ||
|
|
f9ecdebc63 | ||
|
|
4a96af17f4 | ||
|
|
e9b69c5cad | ||
|
|
d3f9bb4c00 | ||
|
|
feae5d2630 | ||
|
|
df7868de2d | ||
|
|
8a66361a98 | ||
|
|
3099ffe201 | ||
|
|
0fc9c28354 | ||
|
|
6ba7dec98d | ||
|
|
332a84c6ec | ||
|
|
a19a5e926c | ||
|
|
bd952b74d8 | ||
|
|
3e374df3aa | ||
|
|
6b67819007 | ||
|
|
83af44b0e6 | ||
|
|
bec12d8e09 | ||
|
|
b34fc8c721 | ||
|
|
dc51fa7802 | ||
|
|
42a667462f | ||
|
|
c76661762f | ||
|
|
9dab223da8 | ||
|
|
1ef6b2bf91 | ||
|
|
f2f3319ac4 | ||
|
|
7fd9cef051 | ||
|
|
4d4cccbba9 | ||
|
|
aff0e366fe |
@@ -3,3 +3,4 @@
|
||||
**/clawicons/** linguist-generated
|
||||
android/src/main/baseline-prof.txt linguist-generated -diff
|
||||
android/src/main/generated/** linguist-generated -diff
|
||||
**/screenshotTest/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 319 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 315 KiB |
+13
-13
@@ -7,6 +7,11 @@
|
||||
"github>msfjarvis/shared-workflows//renovate/automerge"
|
||||
],
|
||||
"branchConcurrentLimit": 15,
|
||||
"github-actions": {
|
||||
"fileMatch": [
|
||||
".github/reusable-workflows/.+\\.ya?ml$",
|
||||
],
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
matchDatasources: [
|
||||
@@ -20,11 +25,11 @@
|
||||
],
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"androidx.compose.compiler:compiler",
|
||||
"matchDepNames": [
|
||||
"com.google.devtools.ksp",
|
||||
"dev.drewhamilton.poko",
|
||||
"org.jetbrains.kotlin:kotlin-gradle-plugin",
|
||||
"org.jetbrains.kotlin.plugin.compose",
|
||||
"org.jetbrains.kotlin.plugin.serialization",
|
||||
],
|
||||
"groupName": "Kotlin and Compose",
|
||||
@@ -48,6 +53,12 @@
|
||||
],
|
||||
"allowedVersions": "!/.*-1-8$/",
|
||||
},
|
||||
{
|
||||
"matchDepNames": [
|
||||
"androidx.compose:compose-bom",
|
||||
],
|
||||
"allowedVersions": "/.*-alpha$/",
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^androidx.glance",
|
||||
@@ -67,16 +78,5 @@
|
||||
"depNameTemplate": "com.facebook:ktfmt",
|
||||
"registryUrlTemplate": "https://repo1.maven.org/maven2/",
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"gradle/libs.versions.toml"
|
||||
],
|
||||
"matchStrings": [
|
||||
"composeCompiler = \"(?<currentValue>.*)\""
|
||||
],
|
||||
"datasourceTemplate": "maven",
|
||||
"depNameTemplate": "androidx.compose.compiler:compiler",
|
||||
"registryUrlTemplate": "https://maven.google.com",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
name: 'Setup Gradle'
|
||||
description: 'Checks out the repository and sets up Java and Gradle'
|
||||
inputs:
|
||||
token:
|
||||
description: 'token input for actions/checkout'
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
fetch-depth:
|
||||
description: 'fetch-depth input for actions/checkout'
|
||||
required: false
|
||||
default: 1
|
||||
cache-read-only:
|
||||
description: 'cache-read-only input for gradle/actions/setup-gradle'
|
||||
required: false
|
||||
default: ${{ github.event.repository != null && github.ref_name != github.event.repository.default_branch }}
|
||||
dependency-graph:
|
||||
description: 'dependency-graph input for gradle/actions/setup-gradle'
|
||||
required: false
|
||||
default: 'disabled'
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
fetch-depth: ${{ inputs.fetch-depth }}
|
||||
token: ${{ inputs.token }}
|
||||
lfs: true
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 22
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: ${{ inputs.cache-read-only }}
|
||||
add-job-summary: always
|
||||
dependency-graph: ${{ inputs.dependency-graph }}
|
||||
validate-wrappers: true
|
||||
@@ -18,23 +18,11 @@ jobs:
|
||||
check:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Setup build environment
|
||||
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@main
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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: ${{ github.ref != 'refs/heads/main' }}
|
||||
gradle-home-cache-cleanup: true
|
||||
add-job-summary: always
|
||||
dependency-graph: generate-and-submit
|
||||
|
||||
- name: Check changelog format
|
||||
@@ -48,7 +36,7 @@ jobs:
|
||||
|
||||
- name: (Fail-only) Upload test report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: Test report
|
||||
path: '**/build/reports/tests/**'
|
||||
@@ -60,10 +48,10 @@ jobs:
|
||||
- check
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # v4
|
||||
uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4
|
||||
with:
|
||||
base-ref: refs/heads/main
|
||||
head-ref: ${{ github.ref }}
|
||||
|
||||
@@ -22,22 +22,14 @@ jobs:
|
||||
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
|
||||
- name: Setup build environment
|
||||
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@main
|
||||
with:
|
||||
fetch-depth: 0
|
||||
cache-read-only: true
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@05963f47d870e2cb19a537396c1f668a348c7d8f # v3.24.8
|
||||
uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
||||
with:
|
||||
languages: java
|
||||
tools: latest
|
||||
@@ -49,7 +41,7 @@ jobs:
|
||||
./gradlew assembleDebug assembleInternal
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@05963f47d870e2cb19a537396c1f668a348c7d8f # v3.24.8
|
||||
uses: github/codeql-action/analyze@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
||||
with:
|
||||
category: "/language:java"
|
||||
mobsfscan:
|
||||
@@ -57,19 +49,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5
|
||||
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Run mobsfscan
|
||||
uses: MobSF/mobsfscan@e29e85c36fa1aa950d7f1b1064345d94a70b2a28
|
||||
uses: MobSF/mobsfscan@25184ed98afa315d90f982d10a44e42c065dcebe
|
||||
with:
|
||||
args: . --sarif --output results.sarif || true
|
||||
|
||||
- name: Upload mobsfscan report
|
||||
uses: github/codeql-action/upload-sarif@05963f47d870e2cb19a537396c1f668a348c7d8f # v3.24.8
|
||||
uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
inAppUpdatePriority:
|
||||
description: 'In app update priority (0-5)'
|
||||
type: number
|
||||
default: "0"
|
||||
default: 0
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
@@ -15,23 +15,12 @@ jobs:
|
||||
publish-google-play-release:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Setup build environment
|
||||
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@main
|
||||
with:
|
||||
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- 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: ${{ github.ref != 'refs/heads/main' }}
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- name: Set up Git author
|
||||
shell: bash
|
||||
@@ -54,7 +43,7 @@ jobs:
|
||||
echo VERSION="${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Update changelog
|
||||
uses: msfjarvis/keep-a-changelog-new-release@78cddbe3549f1c9263a2d754446d3765e865c518 # v2.1.1
|
||||
uses: msfjarvis/keep-a-changelog-new-release@e60399f488b25149c0963a4e22fff928a9277ce7 # v2.2.0
|
||||
with:
|
||||
tag: v${{ env.VERSION }}
|
||||
version: ${{ env.VERSION }}
|
||||
@@ -70,7 +59,7 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: |
|
||||
./gradlew --no-configuration-cache collectReleaseApks collectReleaseBundle -PenableSentry
|
||||
./gradlew --no-configuration-cache assembleRelease collectReleaseApks collectReleaseBundle -PenableSentry
|
||||
|
||||
- name: Clean secrets
|
||||
run: scripts/signing-cleanup.sh
|
||||
|
||||
@@ -16,26 +16,16 @@ 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
|
||||
- name: Setup build environment
|
||||
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@main
|
||||
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
|
||||
./gradlew --no-configuration-cache --stacktrace assembleRelease collectReleaseBundle -PenableSentry
|
||||
./scripts/signing-cleanup.sh
|
||||
env:
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
@@ -45,7 +35,7 @@ jobs:
|
||||
ORG_GRADLE_PROJECT_VERSION_CODE: ${{ github.event.inputs.versionCode }}
|
||||
|
||||
- name: Upload app bundle
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: release-aab
|
||||
path: android/build/outputs/bundle/release/android-release.aab
|
||||
|
||||
@@ -247,3 +247,4 @@ keystore.properties
|
||||
distribution/
|
||||
screenshots/
|
||||
claw-export.json
|
||||
.kotlin/
|
||||
|
||||
Generated
+1
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KtfmtSettings">
|
||||
<option name="enableKtfmt" value="Enabled" />
|
||||
<option name="enabled" value="true" />
|
||||
<option name="uiFormatterStyle" value="Google (internal)" />
|
||||
</component>
|
||||
|
||||
+221
-171
@@ -7,469 +7,519 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.51.0] - 2024-09-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Pull to refresh indicator is no longer hiding behind the top app bar
|
||||
- User profile page correctly renders contents below the top app bar
|
||||
- Revert unread comment handling changes that caused false positives
|
||||
|
||||
## [1.50.0] - 2024-08-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- Search bar draws under the status bar when hiding keyboard
|
||||
- Bottom of the comments page is cut off by the navigation bar
|
||||
- Rare crash in comments page
|
||||
|
||||
## [1.49.0] - 2024-08-29
|
||||
|
||||
### Added
|
||||
|
||||
- Opening posts you have previously seen will show the number of new comments since last visit
|
||||
|
||||
### Fixed
|
||||
|
||||
- Saving posts no longer triggers a page refresh that invalidates scroll position
|
||||
|
||||
### Changed
|
||||
|
||||
- Change submitter text to 'authored' when applicable
|
||||
- Unread comments now have a brighter background rather than a text badge
|
||||
- Bottom navigation bar has been redesigned
|
||||
|
||||
## [1.48.0] - 2024-06-05
|
||||
|
||||
### Added
|
||||
|
||||
- Swiping a post from left to right now offers a share action
|
||||
|
||||
### Fixed
|
||||
|
||||
- Try to workaround an infrequent crash when scrolling hottest/newest posts
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Kotlin 2.0.0
|
||||
- Upgrade to Compose May beta releases
|
||||
|
||||
## [1.47.0] - 2024-05-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Bring back dividers between posts (I regret my earlier choices)
|
||||
- Upgrade to Compose May stable releases
|
||||
|
||||
### Fixed
|
||||
|
||||
- Navigating to user profiles now works when invoked from the search
|
||||
results page
|
||||
- Fix occasional crashes due to the app incorrectly trying to open
|
||||
multiple database connections
|
||||
|
||||
## [1.46.0] - 2024-04-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove dividers between posts
|
||||
- Post titles in lists are now truncated to be a single line
|
||||
|
||||
### Fixed
|
||||
|
||||
- Disable logging of network errors to Sentry
|
||||
- Add potential workaround for navigation-related crashes
|
||||
|
||||
## [1.45.0] - 2024-04-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Compose April releases
|
||||
- Story items are now more compact so you can see more items
|
||||
on your screen.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a crash when clicking an item on the bottom navigation bar
|
||||
too quickly
|
||||
- Removed buggy deeplinks
|
||||
- Clicking a username now correctly navigates to the right page in-app
|
||||
|
||||
## [1.44.0] - 2024-03-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed a bug in the database code that was triggering crashes for
|
||||
a subset of upgrading users
|
||||
- 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
|
||||
- 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
|
||||
- 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.
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- Fixed a crash triggered when swiping items
|
||||
- Adjusted pull to refresh component to match Material You theme
|
||||
|
||||
## [1.35.0] - 2023-09-19
|
||||
|
||||
### Changed
|
||||
|
||||
* Fixed missing vertical spacing between post tags
|
||||
* Fixed user profile links not displaying correctly in-app
|
||||
* Update to Compose October release
|
||||
- Fixed missing vertical spacing between post tags
|
||||
- Fixed user profile links not displaying correctly in-app
|
||||
- Update to Compose October release
|
||||
|
||||
## [1.34.0] - 2023-08-30
|
||||
|
||||
### Added
|
||||
|
||||
* Add a swipe action on each story to open the comments page on `lobste.rs`
|
||||
* Automatically mark posts as "read" and visually distinguish between them
|
||||
- Add a swipe action on each story to open the comments page on `lobste.rs`
|
||||
- Automatically mark posts as "read" and visually distinguish between them
|
||||
|
||||
### Changed
|
||||
|
||||
* Tweak list items to reduce vertical size and adjust colors
|
||||
* Drop bogus workarounds for native library crashes
|
||||
* Updated Jetpack Compose and SQLite
|
||||
- Tweak list items to reduce vertical size and adjust colors
|
||||
- Drop bogus workarounds for native library crashes
|
||||
- Updated Jetpack Compose and SQLite
|
||||
|
||||
### Fixed
|
||||
|
||||
* Rework how comments are displayed to adapt to lobste.rs API change
|
||||
- Rework how comments are displayed to adapt to lobste.rs API change
|
||||
|
||||
## [1.33.0] - 2023-07-31
|
||||
|
||||
### Changed
|
||||
|
||||
* Yet another attempt at fixing native library crashes
|
||||
* Rework how baseline profiles are generated
|
||||
- Yet another attempt at fixing native library crashes
|
||||
- Rework how baseline profiles are generated
|
||||
|
||||
## [1.32.0] - 2023-07-27
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to AndroidX July 26 release
|
||||
* Revert selectable text feature due to sporadic crashes
|
||||
- Upgrade to AndroidX July 26 release
|
||||
- Revert selectable text feature due to sporadic crashes
|
||||
|
||||
## [1.31.0] - 2023-07-25
|
||||
|
||||
### Added
|
||||
|
||||
* Added the ability to search for posts
|
||||
* Text in the comments page is now selectable
|
||||
- Added the ability to search for posts
|
||||
- Text in the comments page is now selectable
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to Compose July release
|
||||
* Upgrade to Kotlin 1.9.0
|
||||
- Upgrade to Compose July release
|
||||
- Upgrade to Kotlin 1.9.0
|
||||
|
||||
## [1.30.0] - 2023-07-02
|
||||
|
||||
### Changed
|
||||
|
||||
* Added another workaround for native library loading crash
|
||||
- Added another workaround for native library loading crash
|
||||
|
||||
## [1.29.0] - 2023-06-08
|
||||
|
||||
### Added
|
||||
|
||||
* Backup and restore options for saved posts
|
||||
- Backup and restore options for saved posts
|
||||
|
||||
### Fixed
|
||||
|
||||
* Spamming the comments button no longer causes it to be opened multiple times
|
||||
* Saved posts screen now has a visual indication when you have nothing saved
|
||||
* Iconography has been updated across the board to be more consistent
|
||||
- Spamming the comments button no longer causes it to be opened multiple times
|
||||
- Saved posts screen now has a visual indication when you have nothing saved
|
||||
- Iconography has been updated across the board to be more consistent
|
||||
|
||||
## [1.28.0] - 2023-06-03
|
||||
|
||||
### Changed
|
||||
|
||||
* Navigation transitions have been slightly sped up
|
||||
- Navigation transitions have been slightly sped up
|
||||
|
||||
### Fixed
|
||||
|
||||
* Add workaround for a native library loading crash observed on some devices
|
||||
- Add workaround for a native library loading crash observed on some devices
|
||||
|
||||
## [1.27.0] - 2023-05-31
|
||||
|
||||
### Changed
|
||||
|
||||
* Small accessibility improvements
|
||||
* Slightly tweak the layout for story items to take less vertical space
|
||||
* Upgrade to Compose `1.5.0-beta01` release
|
||||
* Set accessibility web URI for profile screen
|
||||
* Directly use Material Icons from upstream artifacts
|
||||
* Upgrade dependencies
|
||||
* Add adaptive navigation support (thanks @Yash-Garg)
|
||||
* Use latest SQLite for backing databases
|
||||
- Small accessibility improvements
|
||||
- Slightly tweak the layout for story items to take less vertical space
|
||||
- Upgrade to Compose `1.5.0-beta01` release
|
||||
- Set accessibility web URI for profile screen
|
||||
- Directly use Material Icons from upstream artifacts
|
||||
- Upgrade dependencies
|
||||
- Add adaptive navigation support (thanks @Yash-Garg)
|
||||
- Use latest SQLite for backing databases
|
||||
|
||||
## [1.26.0] - 2023-05-03
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to Compose May release
|
||||
* Disable Sentry performance reporting
|
||||
* Migrate to Compose Foundation `FlowRow` in `LobstersCard`
|
||||
- Upgrade to Compose May release
|
||||
- Disable Sentry performance reporting
|
||||
- Migrate to Compose Foundation `FlowRow` in `LobstersCard`
|
||||
|
||||
## [1.25.0] - 2023-04-18
|
||||
|
||||
### Changed
|
||||
|
||||
* Make the app draw edge to edge
|
||||
* Simplify Top App Bar color scheme
|
||||
* Rework how baseline profiles are generated
|
||||
* Upgrade to Compose April release
|
||||
- Make the app draw edge to edge
|
||||
- Simplify Top App Bar color scheme
|
||||
- Rework how baseline profiles are generated
|
||||
- Upgrade to Compose April release
|
||||
|
||||
## [1.24.0] - 2023-03-24
|
||||
|
||||
### Changed
|
||||
|
||||
* Rebuild app icon assets to align better with Material guidelines
|
||||
* Adopt Slack's Compose lints and address issues found by it
|
||||
* Upgrade to OkHttp 4.x
|
||||
* Upgrade to Compose March release
|
||||
* Start work on support for logging in with lobste.rs
|
||||
- Rebuild app icon assets to align better with Material guidelines
|
||||
- Adopt Slack's Compose lints and address issues found by it
|
||||
- Upgrade to OkHttp 4.x
|
||||
- Upgrade to Compose March release
|
||||
- Start work on support for logging in with lobste.rs
|
||||
|
||||
## [1.23.0] - 2023-03-02
|
||||
|
||||
### Added
|
||||
|
||||
* Introduce [Sentry](https://sentry.io) for error reporting and performance monitoring
|
||||
- Introduce [Sentry](https://sentry.io) for error reporting and performance monitoring
|
||||
|
||||
## [1.22.0] - 2023-03-02
|
||||
|
||||
### Changed
|
||||
|
||||
* In-app browser now respects the user's choice for dark mode
|
||||
* Upgrade dependencies
|
||||
- In-app browser now respects the user's choice for dark mode
|
||||
- Upgrade dependencies
|
||||
|
||||
## [1.21.0] - 2023-02-09
|
||||
|
||||
### Changed
|
||||
|
||||
* Fix bug that caused the app to crash on launch
|
||||
- Fix bug that caused the app to crash on launch
|
||||
|
||||
## [1.20.0] - 2023-02-09
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade dependencies
|
||||
- Upgrade dependencies
|
||||
|
||||
## [1.19.0] - 2023-02-01
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade dependencies
|
||||
* Optimize packaged baseline profiles
|
||||
- Upgrade dependencies
|
||||
- Optimize packaged baseline profiles
|
||||
|
||||
## [1.18.0] - 2023-01-21
|
||||
|
||||
### Changed
|
||||
|
||||
* Special-case deleted stories in comments API
|
||||
* Fix regression where save button had the incorrect visual state
|
||||
- Special-case deleted stories in comments API
|
||||
- Fix regression where save button had the incorrect visual state
|
||||
|
||||
## [1.17.0] - 2023-01-12
|
||||
|
||||
### Added
|
||||
|
||||
* The app will now mark new comments as unread when returning to posts
|
||||
- The app will now mark new comments as unread when returning to posts
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to Kotlin 1.8.0
|
||||
* Revert questionable app bar scrolling behaviour change
|
||||
- Upgrade to Kotlin 1.8.0
|
||||
- Revert questionable app bar scrolling behaviour change
|
||||
|
||||
## [1.16.0] - 2022-12-24
|
||||
|
||||
### Changed
|
||||
|
||||
* Fix bug where newest posts tab actually showed hottest posts instead
|
||||
- Fix bug where newest posts tab actually showed hottest posts instead
|
||||
|
||||
## [1.15.0] - 2022-12-20
|
||||
|
||||
### Changed
|
||||
|
||||
* Fix a case where collapsing the parent of an already collapsed comment caused it to become expanded
|
||||
* Tweak progress bar UI to use a linear indicator
|
||||
* Allow app bar to hide when scrolling down
|
||||
* Fix bug where refreshing a list of posts caused it to jump around multiple times
|
||||
- Fix a case where collapsing the parent of an already collapsed comment caused it to become expanded
|
||||
- Tweak progress bar UI to use a linear indicator
|
||||
- Allow app bar to hide when scrolling down
|
||||
- Fix bug where refreshing a list of posts caused it to jump around multiple times
|
||||
|
||||
## [1.14.0] - 2022-12-12
|
||||
|
||||
### Changed
|
||||
|
||||
* Rework how often saved posts are updated
|
||||
* Collapsing a comment now collapses all comments under it similar to how it works on the website
|
||||
- Rework how often saved posts are updated
|
||||
- Collapsing a comment now collapses all comments under it similar to how it works on the website
|
||||
|
||||
## [1.13.0] - 2022-12-10
|
||||
|
||||
### Changed
|
||||
|
||||
* Fix crash when viewing [jcs](https://lobste.rs/u/jcs)' comments
|
||||
- Fix crash when viewing [jcs](https://lobste.rs/u/jcs)' comments
|
||||
|
||||
## [1.12.0] - 2022-12-09
|
||||
|
||||
### Changed
|
||||
|
||||
* Do not schedule post update job every time the app starts
|
||||
* Improve favicon loading to reduce unnecessary redraws
|
||||
* Fix post lists being reloaded unnecessarily
|
||||
- Do not schedule post update job every time the app starts
|
||||
- Improve favicon loading to reduce unnecessary redraws
|
||||
- Fix post lists being reloaded unnecessarily
|
||||
|
||||
## [1.11.0] - 2022-12-07
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to Compose 2022.12.00 release
|
||||
* Refresh packaged baseline profile
|
||||
* Improve how saved state of posts is queried
|
||||
- Upgrade to Compose 2022.12.00 release
|
||||
- Refresh packaged baseline profile
|
||||
- Improve how saved state of posts is queried
|
||||
|
||||
## [1.10.0] - 2022-11-30
|
||||
|
||||
### Changed
|
||||
|
||||
* Configure Coil image loader with better caching settings
|
||||
* Refresh packaged baseline profile
|
||||
- Configure Coil image loader with better caching settings
|
||||
- Refresh packaged baseline profile
|
||||
|
||||
## [1.9.0] - 2022-11-28
|
||||
|
||||
### Changed
|
||||
|
||||
* A loading indicator is added to the bottom of the post list to identify when new posts are being fetched
|
||||
- A loading indicator is added to the bottom of the post list to identify when new posts are being fetched
|
||||
|
||||
## [1.8.0] - 2022-11-21
|
||||
|
||||
### Changed
|
||||
|
||||
* The release pipeline now automatically generates Play Store release notes from the changelog file
|
||||
- The release pipeline now automatically generates Play Store release notes from the changelog file
|
||||
|
||||
### Fixed
|
||||
|
||||
* Downgrade AGP to 8.0.0-alpha07 to fix Baseline Profiles not being packaged
|
||||
- Downgrade AGP to 8.0.0-alpha07 to fix Baseline Profiles not being packaged
|
||||
|
||||
## [1.7.0] - 2022-11-19
|
||||
|
||||
### Changed
|
||||
|
||||
* Switch to Compose Material's swipe refresh feature
|
||||
* Change metadata extractor logic to no longer prefer canonical URLs. The submitted URL will be retained as-is now.
|
||||
* Remove navigation transition animations
|
||||
- Switch to Compose Material's swipe refresh feature
|
||||
- Change metadata extractor logic to no longer prefer canonical URLs. The submitted URL will be retained as-is now.
|
||||
- Remove navigation transition animations
|
||||
|
||||
## [1.6.0] - 2022-11-14
|
||||
|
||||
### Changed
|
||||
|
||||
* Migrate dependency injection from Hilt to Anvil + Whetstone
|
||||
- Migrate dependency injection from Hilt to Anvil + Whetstone
|
||||
|
||||
## [1.5.0] - 2022-11-10
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to Compose 2022.11.00 release
|
||||
- Upgrade to Compose 2022.11.00 release
|
||||
|
||||
## [1.4.0] - 2022-11-02
|
||||
|
||||
### Changed
|
||||
|
||||
* Misc fixes to link metadata extractor
|
||||
- Misc fixes to link metadata extractor
|
||||
|
||||
## [1.3.0] - 2022-10-24
|
||||
|
||||
### Changed
|
||||
|
||||
* Prevent errors during link metadata retrieval from crashing the app
|
||||
* Update Jetpack libraries to 2022-10-24 release
|
||||
- Prevent errors during link metadata retrieval from crashing the app
|
||||
- Update Jetpack libraries to 2022-10-24 release
|
||||
|
||||
## [1.2.0] - 2022-10-17
|
||||
|
||||
### Changed
|
||||
|
||||
* Lazily load link metadata to improve comment page loading speed
|
||||
* Upgrade to latest Compose release
|
||||
* Refactor code to align with Twitter's public Compose guidelines
|
||||
- Lazily load link metadata to improve comment page loading speed
|
||||
- Upgrade to latest Compose release
|
||||
- Refactor code to align with Twitter's public Compose guidelines
|
||||
|
||||
## [1.1.0] - 2022-09-30
|
||||
|
||||
### Added
|
||||
|
||||
* Add score and relative time to comments
|
||||
- Add score and relative time to comments
|
||||
|
||||
### Changed
|
||||
|
||||
* Update Jetpack Compose
|
||||
* Declare data backup and transfer rules
|
||||
* Improve legibility of links in comments and user profiles
|
||||
* Commonize and improve how network errors are displayed
|
||||
- Update Jetpack Compose
|
||||
- Declare data backup and transfer rules
|
||||
- Improve legibility of links in comments and user profiles
|
||||
- Commonize and improve how network errors are displayed
|
||||
|
||||
## [1.0.0] - 2022-09-20
|
||||
|
||||
* Initial Play Store release
|
||||
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.44.0...HEAD
|
||||
- Initial Play Store release
|
||||
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.51.0...HEAD
|
||||
[1.51.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.50.0...v1.51.0
|
||||
[1.50.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.49.0...v1.50.0
|
||||
[1.49.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.48.0...v1.49.0
|
||||
[1.48.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.47.0...v1.48.0
|
||||
[1.47.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.46.0...v1.47.0
|
||||
[1.46.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.45.0...v1.46.0
|
||||
[1.45.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.44.0...v1.45.0
|
||||
[1.44.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.43.0...v1.44.0
|
||||
|
||||
[1.43.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.42.0...v1.43.0
|
||||
|
||||
[1.42.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.41.0...v1.42.0
|
||||
|
||||
[1.41.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.40.0...v1.41.0
|
||||
|
||||
[1.40.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.39.0...v1.40.0
|
||||
|
||||
[1.39.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.38.0...v1.39.0
|
||||
|
||||
[1.38.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.37.0...v1.38.0
|
||||
|
||||
[1.37.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.36.0...v1.37.0
|
||||
|
||||
[1.36.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.35.0...v1.36.0
|
||||
|
||||
[1.35.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.34.0...v1.35.0
|
||||
|
||||
[1.34.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.33.0...v1.34.0
|
||||
|
||||
[1.33.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.32.0...v1.33.0
|
||||
|
||||
[1.32.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.31.0...v1.32.0
|
||||
|
||||
[1.31.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.30.0...v1.31.0
|
||||
|
||||
[1.30.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.29.0...v1.30.0
|
||||
|
||||
[1.29.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.28.0...v1.29.0
|
||||
|
||||
[1.28.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.27.0...v1.28.0
|
||||
|
||||
[1.27.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.26.0...v1.27.0
|
||||
|
||||
[1.26.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.25.0...v1.26.0
|
||||
|
||||
[1.25.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.24.0...v1.25.0
|
||||
|
||||
[1.24.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.23.0...v1.24.0
|
||||
|
||||
[1.23.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.22.0...v1.23.0
|
||||
|
||||
[1.22.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.21.0...v1.22.0
|
||||
|
||||
[1.21.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.20.0...v1.21.0
|
||||
|
||||
[1.20.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.19.0...v1.20.0
|
||||
|
||||
[1.19.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.18.0...v1.19.0
|
||||
|
||||
[1.18.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.17.0...v1.18.0
|
||||
|
||||
[1.17.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.16.0...v1.17.0
|
||||
|
||||
[1.16.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.15.0...v1.16.0
|
||||
|
||||
[1.15.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.14.0...v1.15.0
|
||||
|
||||
[1.14.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.13.0...v1.14.0
|
||||
|
||||
[1.13.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.12.0...v1.13.0
|
||||
|
||||
[1.12.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.11.0...v1.12.0
|
||||
|
||||
[1.11.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.10.0...v1.11.0
|
||||
|
||||
[1.10.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.9.0...v1.10.0
|
||||
|
||||
[1.9.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.8.0...v1.9.0
|
||||
|
||||
[1.8.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.7.0...1.8.0
|
||||
|
||||
[1.7.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.6.0...v1.7.0
|
||||
|
||||
[1.6.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.5.0...v1.6.0
|
||||
|
||||
[1.5.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.4.0...v1.5.0
|
||||
|
||||
[1.4.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.3.0...v1.4.0
|
||||
|
||||
[1.3.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.2.0...v1.3.0
|
||||
|
||||
[1.2.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.1.0...v1.2.0
|
||||
|
||||
[1.1.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.0.0...v1.1.0
|
||||
|
||||
[1.0.0]: https://github.com/msfjarvis/compose-lobsters/compare/29c374859b17c5fcef03585b8a01c00070de9097...v1.0.0
|
||||
|
||||
@@ -8,6 +8,12 @@ Unofficial Android app for read-only access to [lobste.rs](https://lobste.rs), b
|
||||
height="80" />
|
||||
</a>
|
||||
|
||||
<a href="https://android.izzysoft.de/repo/apk/dev.msfjarvis.claw.android">
|
||||
<img src="./.github/izzy-badge.webp"
|
||||
alt="Get it on IzzyOnDroid"
|
||||
height="80" />
|
||||
</a>
|
||||
|
||||
> [!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)
|
||||
|
||||
|
||||
@@ -16,27 +16,32 @@ plugins {
|
||||
id("dev.msfjarvis.claw.sentry")
|
||||
id("dev.msfjarvis.claw.versioning-plugin")
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
alias(libs.plugins.android.junit5)
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.modulegraphassert)
|
||||
alias(libs.plugins.whetstone)
|
||||
alias(libs.plugins.baselineprofile)
|
||||
alias(libs.plugins.licensee)
|
||||
alias(libs.plugins.tracelog)
|
||||
alias(libs.plugins.kotlin.composeCompiler)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.navigation.safeargs)
|
||||
alias(libs.plugins.screenshot)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.msfjarvis.claw.android"
|
||||
defaultConfig.applicationId = "dev.msfjarvis.claw.android"
|
||||
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
buildFeatures.compose = true
|
||||
composeOptions {
|
||||
useLiveLiterals = false
|
||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
}
|
||||
composeOptions { useLiveLiterals = false }
|
||||
buildTypes.create("internal") {
|
||||
matchingFallbacks += "release"
|
||||
signingConfig = signingConfigs["debug"]
|
||||
applicationIdSuffix = ".internal"
|
||||
debuggable(true)
|
||||
}
|
||||
experimentalProperties["android.experimental.enableScreenshotTest"] = true
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
@@ -51,6 +56,7 @@ licensee {
|
||||
ignoreDependencies("com.michael-bull.kotlin-result") { because("kotlin-result is ISC licensed") }
|
||||
ignoreDependencies("org.commonmark") { because("Commonmark is BSD licensed") }
|
||||
allowUrl("https://jsoup.org/license") { because("Jsoup is MIT licensed") }
|
||||
allowUrl("https://opensource.org/licenses/MIT") { because("That's the MIT license text") }
|
||||
}
|
||||
|
||||
moduleGraphAssert {
|
||||
@@ -71,38 +77,54 @@ dependencies {
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.aboutLibraries.m3)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.animation)
|
||||
implementation(libs.androidx.compose.foundation)
|
||||
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.compose.runtime)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
implementation(libs.androidx.compose.ui.util)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.androidx.lifecycle.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.coil)
|
||||
implementation(libs.copydown)
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.jsoup)
|
||||
implementation(libs.eithernet)
|
||||
implementation(libs.haze)
|
||||
implementation(libs.haze.materials)
|
||||
implementation(libs.javax.inject)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.material3.pulltorefresh)
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.napier)
|
||||
implementation(libs.okhttp.core)
|
||||
implementation(libs.okhttp.loggingInterceptor)
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.sqldelight.extensions.coroutines)
|
||||
implementation(libs.swipe)
|
||||
implementation(libs.unfurl)
|
||||
implementation(projects.api)
|
||||
implementation(projects.common)
|
||||
implementation(projects.core)
|
||||
implementation(projects.database.core)
|
||||
implementation(projects.database.impl)
|
||||
implementation(projects.model)
|
||||
implementation(projects.web)
|
||||
|
||||
kapt(libs.dagger.compiler)
|
||||
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.okhttp.mockwebserver)
|
||||
screenshotTestImplementation(libs.androidx.compose.ui.tooling)
|
||||
|
||||
addTestDependencies(project)
|
||||
androidTestImplementation(libs.androidx.test.espresso.core)
|
||||
androidTestImplementation(libs.androidx.test.uiautomator)
|
||||
androidTestImplementation(libs.leakcanary.android.test)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 format="6" by="lint 8.4.2" type="baseline" client="gradle" dependencies="true" name="AGP (8.4.2)" variant="all" version="8.4.2">
|
||||
|
||||
<issue
|
||||
id="VectorPath"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Performing the heap growth analysis in process requires more heap. -->
|
||||
<application
|
||||
android:largeHeap="true"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.BySelector
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import androidx.test.uiautomator.UiObject2
|
||||
import androidx.test.uiautomator.Until
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import de.mannodermaus.junit5.ActivityScenarioExtension
|
||||
import leakcanary.repeatingAndroidInProcessScenario
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
import shark.HeapDiff
|
||||
|
||||
class HeapGrowthCheck {
|
||||
@JvmField
|
||||
@RegisterExtension
|
||||
@Suppress("Unused")
|
||||
val scenarioExtension = ActivityScenarioExtension.launch<MainActivity>()
|
||||
private val detector = HeapDiff.repeatingAndroidInProcessScenario()
|
||||
private lateinit var device: UiDevice
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
device = UiDevice.getInstance(instrumentation)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verify_heap_growth() {
|
||||
val heapGrowth = detector.findRepeatedlyGrowingObjects { device.exploreScreens() }
|
||||
assertThat(heapGrowth.growingObjects).isEmpty()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
fun UiDevice.exploreScreens() {
|
||||
listOf("HOTTEST", "NEWEST", "SAVED").forEach { tag ->
|
||||
waitForObject(By.res(tag)).click()
|
||||
waitForIdle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun UiDevice.waitForObject(selector: BySelector, timeout: Long = 10_000L): UiObject2 {
|
||||
if (wait(Until.hasObject(selector), timeout)) {
|
||||
return findObject(selector)
|
||||
}
|
||||
|
||||
error("Object with selector [$selector] not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:10defb7f6abec5d8bd07e07625e30e5b68d3a330e87f157e8138688f8d7ef0aa
|
||||
size 214795
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:64dd5c002c021774469d7d9a15433678a014f720b6cde2281fbbf20daee98712
|
||||
size 216093
|
||||
@@ -11,7 +11,6 @@
|
||||
<application
|
||||
android:name=".ClawApplication"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/full_backup_content"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -32,108 +31,8 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/......" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/....../" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/....../...*" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/......" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/....../" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/....../...*" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/......" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/....../" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/....../...*" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/u/......*" />
|
||||
<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"
|
||||
@@ -161,6 +60,7 @@
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
<receiver
|
||||
android:name=".glance.WidgetReceiver"
|
||||
android:exported="true">
|
||||
@@ -171,6 +71,7 @@
|
||||
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"
|
||||
|
||||
@@ -23,8 +23,7 @@ import javax.inject.Inject
|
||||
@ContributesAppInjector(generateAppComponent = true)
|
||||
class ClawApplication : Application(), ApplicationComponentOwner {
|
||||
|
||||
override val applicationComponent by
|
||||
lazy(LazyThreadSafetyMode.NONE) { GeneratedApplicationComponent.create(this) }
|
||||
override val applicationComponent = GeneratedApplicationComponent.create(this)
|
||||
|
||||
@Inject lateinit var plugins: Set<@JvmSuppressWildcards AppPlugin>
|
||||
|
||||
|
||||
+2
-2
@@ -22,11 +22,11 @@ object MetadataExtractorModule {
|
||||
fun provideUnfurlLogger(): UnfurlLogger {
|
||||
return object : UnfurlLogger {
|
||||
override fun log(message: String) {
|
||||
Napier.d { message }
|
||||
Napier.d(tag = "Unfurler") { message }
|
||||
}
|
||||
|
||||
override fun log(e: Throwable, message: String) {
|
||||
Napier.e(e) { message }
|
||||
Napier.e(tag = "Unfurler", throwable = e) { message }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ 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.android.ui.toError
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
@@ -28,31 +27,20 @@ class LobstersPagingSource
|
||||
constructor(
|
||||
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val readPostsRepository: ReadPostsRepository,
|
||||
) : PagingSource<Int, UIPost>() {
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UIPost> {
|
||||
val page = params.key ?: STARTING_PAGE_INDEX
|
||||
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
|
||||
is Success ->
|
||||
LoadResult.Page(
|
||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||
data =
|
||||
result.value.map {
|
||||
it
|
||||
.toUIPost()
|
||||
.copy(
|
||||
isSaved = savedPostsRepository.isPostSaved(it.shortId),
|
||||
isRead = readPostsRepository.isPostRead(it.shortId),
|
||||
)
|
||||
},
|
||||
data = result.value.map(LobstersPost::toUIPost),
|
||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||
nextKey = page + 1,
|
||||
)
|
||||
is Failure.NetworkFailure -> LoadResult.Error(result.error)
|
||||
is Failure.UnknownFailure -> LoadResult.Error(result.error)
|
||||
is Failure.HttpFailure,
|
||||
is Failure.HttpFailure -> LoadResult.Error(result.toError())
|
||||
is Failure.ApiFailure -> LoadResult.Error(IOException("API returned an invalid response"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ 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.android.ui.toError
|
||||
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
|
||||
@@ -36,15 +36,7 @@ constructor(
|
||||
private val searchApi: LobstersSearchApi,
|
||||
@Assisted private val queryProvider: () -> String,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
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, UIPost> {
|
||||
val searchQuery = queryProvider()
|
||||
// If there is no query, we don't need to call the API at all.
|
||||
@@ -60,27 +52,25 @@ constructor(
|
||||
val nextKey = if (result.value.isEmpty()) null else page + 1
|
||||
LoadResult.Page(
|
||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||
data =
|
||||
result.value.map {
|
||||
it
|
||||
.toUIPost()
|
||||
.copy(
|
||||
isSaved = savedPostsRepository.isPostSaved(it.shortId),
|
||||
isRead = readPostsRepository.isPostRead(it.shortId),
|
||||
)
|
||||
},
|
||||
data = result.value.map(LobstersPost::toUIPost),
|
||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||
nextKey = nextKey,
|
||||
)
|
||||
}
|
||||
is ApiResult.Failure.NetworkFailure -> LoadResult.Error(result.error)
|
||||
is ApiResult.Failure.UnknownFailure -> LoadResult.Error(result.error)
|
||||
is ApiResult.Failure.HttpFailure,
|
||||
is ApiResult.Failure.HttpFailure -> LoadResult.Error(result.toError())
|
||||
is ApiResult.Failure.ApiFailure ->
|
||||
LoadResult.Error(IOException("API returned an invalid response"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, UIPost>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(queryProvider: () -> String): SearchPagingSource
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.android.plugins
|
||||
|
||||
import android.app.Application
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesMultibinding
|
||||
import dev.msfjarvis.claw.core.injection.AppPlugin
|
||||
import io.sentry.android.core.SentryAndroid
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesMultibinding(ApplicationScope::class)
|
||||
class SentryAndroidPlugin @Inject constructor() : AppPlugin {
|
||||
override fun apply(application: Application) {
|
||||
SentryAndroid.init(application) { options ->
|
||||
options.experimental.sessionReplay.errorSampleRate = 1.0
|
||||
options.experimental.sessionReplay.sessionSampleRate = 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
-34
@@ -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.
|
||||
@@ -13,16 +13,27 @@ import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.HazeStyle
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destination
|
||||
import dev.msfjarvis.claw.android.ui.navigation.matches
|
||||
import dev.msfjarvis.claw.common.ui.FloatingNavigationBar
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
const val AnimationDuration = 100
|
||||
@@ -32,6 +43,7 @@ fun ClawNavigationBar(
|
||||
navController: NavController,
|
||||
items: ImmutableList<NavigationItem>,
|
||||
isVisible: Boolean,
|
||||
hazeState: HazeState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
@@ -48,44 +60,65 @@ fun ClawNavigationBar(
|
||||
targetOffsetY = { fullHeight -> fullHeight },
|
||||
animationSpec = tween(durationMillis = AnimationDuration, easing = FastOutLinearInEasing),
|
||||
),
|
||||
modifier = Modifier,
|
||||
) {
|
||||
NavigationBar(modifier = modifier) {
|
||||
items.forEach { navItem ->
|
||||
val isCurrentDestination = navController.currentDestination?.route == navItem.route
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Crossfade(isCurrentDestination, label = "nav-label") {
|
||||
Icon(
|
||||
imageVector = if (it) navItem.selectedIcon else navItem.icon,
|
||||
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { Text(text = navItem.label) },
|
||||
selected = isCurrentDestination,
|
||||
onClick = {
|
||||
if (isCurrentDestination) {
|
||||
navItem.listStateResetCallback()
|
||||
} else {
|
||||
navController.graph.startDestinationRoute?.let { startDestination ->
|
||||
navController.popBackStack(startDestination, false)
|
||||
label = "",
|
||||
content = {
|
||||
FloatingNavigationBar(
|
||||
tonalElevation = 16.dp,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
modifier =
|
||||
modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.navigationBarsPadding()
|
||||
.clip(MaterialTheme.shapes.extraLarge)
|
||||
.hazeChild(
|
||||
hazeState,
|
||||
style =
|
||||
HazeStyle(
|
||||
backgroundColor = MaterialTheme.colorScheme.surface,
|
||||
tints = emptyList(),
|
||||
blurRadius = 24.dp,
|
||||
noiseFactor = 0f,
|
||||
),
|
||||
),
|
||||
containerColor = Color.Transparent,
|
||||
) {
|
||||
val navBackStackEntry = navController.currentBackStackEntryAsState().value
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
items.forEach { navItem ->
|
||||
val isSelected = currentDestination.matches(navItem.destination)
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Crossfade(isSelected, label = "nav-label") {
|
||||
Icon(
|
||||
imageVector = if (it) navItem.selectedIcon else navItem.icon,
|
||||
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
|
||||
)
|
||||
}
|
||||
if (navItem.route != Destinations.startDestination.route) {
|
||||
navController.navigate(navItem.route)
|
||||
},
|
||||
label = { Text(text = navItem.label) },
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (isSelected) {
|
||||
navItem.listStateResetCallback()
|
||||
} else {
|
||||
navController.navigate(navItem.destination) {
|
||||
popUpTo(navController.graph.startDestinationId) { saveState = true }
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.testTag(navItem.label.uppercase()),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.testTag(navItem.label.uppercase()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
class NavigationItem(
|
||||
val label: String,
|
||||
val route: String,
|
||||
val destination: Destination,
|
||||
val icon: ImageVector,
|
||||
val selectedIcon: ImageVector,
|
||||
val listStateResetCallback: () -> Unit,
|
||||
|
||||
+13
-11
@@ -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.
|
||||
@@ -22,7 +22,8 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.navigation.NavController
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import dev.msfjarvis.claw.android.ui.navigation.matches
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
@@ -49,12 +50,14 @@ fun ClawNavigationRail(
|
||||
modifier = Modifier,
|
||||
) {
|
||||
NavigationRail(modifier = modifier) {
|
||||
val navBackStackEntry = navController.currentBackStackEntryAsState().value
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
Spacer(Modifier.weight(1f))
|
||||
items.forEach { navItem ->
|
||||
val isCurrentDestination = navController.currentDestination?.route == navItem.route
|
||||
val isSelected = currentDestination.matches(navItem.destination)
|
||||
NavigationRailItem(
|
||||
icon = {
|
||||
Crossfade(isCurrentDestination, label = "nav-label") {
|
||||
Crossfade(isSelected, label = "nav-label") {
|
||||
Icon(
|
||||
imageVector = if (it) navItem.selectedIcon else navItem.icon,
|
||||
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
|
||||
@@ -62,16 +65,15 @@ fun ClawNavigationRail(
|
||||
}
|
||||
},
|
||||
label = { Text(text = navItem.label) },
|
||||
selected = isCurrentDestination,
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (isCurrentDestination) {
|
||||
if (isSelected) {
|
||||
navItem.listStateResetCallback()
|
||||
} else {
|
||||
navController.graph.startDestinationRoute?.let { startDestination ->
|
||||
navController.popBackStack(startDestination, false)
|
||||
}
|
||||
if (navItem.route != Destinations.startDestination.route) {
|
||||
navController.navigate(navItem.route)
|
||||
navController.navigate(navItem.destination) {
|
||||
popUpTo(navController.graph.startDestinationId) { saveState = true }
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,16 +8,20 @@ package dev.msfjarvis.claw.android.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.NavController
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Comments
|
||||
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.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
fun Context.getActivity(): ComponentActivity? {
|
||||
return when (this) {
|
||||
@@ -29,6 +33,7 @@ fun Context.getActivity(): ComponentActivity? {
|
||||
|
||||
@Composable
|
||||
fun rememberPostActions(
|
||||
context: Context,
|
||||
urlLauncher: UrlLauncher,
|
||||
navController: NavController,
|
||||
viewModel: ClawViewModel,
|
||||
@@ -42,26 +47,33 @@ fun rememberPostActions(
|
||||
|
||||
override fun viewComments(postId: String) {
|
||||
viewModel.markPostAsRead(postId)
|
||||
val currentRoute = navController.currentDestination?.route
|
||||
val newRoute =
|
||||
Destinations.Comments.route.replace(Destinations.Comments.PLACEHOLDER, postId)
|
||||
if (currentRoute != Destinations.Comments.route) navController.navigate(newRoute)
|
||||
navController.navigate(Comments(postId))
|
||||
}
|
||||
|
||||
override fun viewCommentsPage(commentsUrl: String) {
|
||||
// Post links from lobste.rs are of the form $baseUrl/s/$postId/$postTitle
|
||||
// Interestingly, lobste.rs does not actually care for the value of $postTitle, and will
|
||||
// happily accept both a missing as well as a completely arbitrary $postTitle. We
|
||||
// leverage this to create a new URL format which looks like
|
||||
// $baseUrl/s/$postId/$postTitle/r, and does not trigger our deeplinks,
|
||||
// instead opening in the custom tab as we want it to.
|
||||
urlLauncher.openUri(commentsUrl.replaceAfterLast('/', "r"))
|
||||
override fun viewCommentsPage(post: UIPost) {
|
||||
urlLauncher.openUri(post.commentsUrl)
|
||||
}
|
||||
|
||||
override fun toggleSave(post: UIPost) {
|
||||
viewModel.toggleSave(post)
|
||||
}
|
||||
|
||||
override fun share(post: UIPost) {
|
||||
val sendIntent: Intent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, post.url.ifEmpty { post.commentsUrl })
|
||||
putExtra(Intent.EXTRA_TITLE, post.title)
|
||||
type = "text/plain"
|
||||
}
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
context.startActivity(shareIntent)
|
||||
}
|
||||
|
||||
override fun isPostRead(post: UIPost): Boolean = viewModel.isPostRead(post)
|
||||
|
||||
override fun isPostSaved(post: UIPost): Boolean = viewModel.isPostSaved(post)
|
||||
|
||||
override suspend fun getComments(postId: String): UIPost {
|
||||
return viewModel.getPostComments(postId)
|
||||
}
|
||||
@@ -72,3 +84,17 @@ fun rememberPostActions(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an [ApiResult.Failure.HttpFailure] to a scoped down error with a more useful user-facing
|
||||
* message.
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE") // We inline this to eliminate the stacktrace frame.
|
||||
inline fun <T : Any> ApiResult.Failure.HttpFailure<T>.toError(): Throwable =
|
||||
when (code) {
|
||||
HttpURLConnection.HTTP_NOT_FOUND -> IOException("Story was removed by moderator")
|
||||
HttpURLConnection.HTTP_INTERNAL_ERROR,
|
||||
HttpURLConnection.HTTP_BAD_GATEWAY,
|
||||
HttpURLConnection.HTTP_UNAVAILABLE -> IOException("It appears lobste.rs is currently down")
|
||||
else -> IOException("API returned an invalid response")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.activity.compose.ReportDrawn
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -36,6 +37,7 @@ fun DatabasePosts(
|
||||
items: ImmutableMap<String, List<UIPost>>,
|
||||
listState: LazyListState,
|
||||
postActions: PostActions,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ReportDrawn()
|
||||
@@ -50,11 +52,11 @@ fun DatabasePosts(
|
||||
Text(text = "No saved posts", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(state = listState) {
|
||||
LazyColumn(state = listState, contentPadding = contentPadding) {
|
||||
items.forEach { (month, posts) ->
|
||||
stickyHeader(contentType = "month-header") { MonthHeader(label = month) }
|
||||
items(items = posts, key = { it.shortId }, contentType = { "LobstersItem" }) { item ->
|
||||
LobstersListItem(item = item, refresh = {}, postActions = postActions)
|
||||
LobstersListItem(item = item, postActions = postActions)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ package dev.msfjarvis.claw.android.ui.lists
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Reply
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import dev.msfjarvis.claw.common.posts.LobstersCard
|
||||
@@ -20,19 +20,20 @@ import me.saket.swipe.SwipeAction
|
||||
import me.saket.swipe.SwipeableActionsBox
|
||||
|
||||
@Composable
|
||||
fun LobstersListItem(
|
||||
item: UIPost,
|
||||
postActions: PostActions,
|
||||
refresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun LobstersListItem(item: UIPost, postActions: PostActions, modifier: Modifier = Modifier) {
|
||||
val commentsAction =
|
||||
SwipeAction(
|
||||
icon = rememberVectorPainter(Icons.AutoMirrored.Filled.Reply),
|
||||
background = MaterialTheme.colorScheme.tertiary,
|
||||
onSwipe = { postActions.viewCommentsPage(item.commentsUrl) },
|
||||
onSwipe = { postActions.viewCommentsPage(item) },
|
||||
)
|
||||
SwipeableActionsBox(endActions = listOf(commentsAction)) {
|
||||
LobstersCard(post = item, postActions = postActions, refresh = refresh, modifier = modifier)
|
||||
val shareAction =
|
||||
SwipeAction(
|
||||
icon = rememberVectorPainter(Icons.Filled.Share),
|
||||
background = MaterialTheme.colorScheme.tertiary,
|
||||
onSwipe = { postActions.share(item) },
|
||||
)
|
||||
SwipeableActionsBox(startActions = listOf(shareAction), endActions = listOf(commentsAction)) {
|
||||
LobstersCard(post = item, postActions = postActions, modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,30 +7,40 @@
|
||||
package dev.msfjarvis.claw.android.ui.lists
|
||||
|
||||
import androidx.activity.compose.ReportDrawnWhen
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemContentType
|
||||
import androidx.paging.compose.itemKey
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.posts.TEST_POST
|
||||
import dev.msfjarvis.claw.common.posts.TEST_POST_ACTIONS
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.NetworkError
|
||||
import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||
import dev.msfjarvis.claw.common.ui.preview.DevicePreviews
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import eu.bambooapps.material3.pullrefresh.PullRefreshIndicator
|
||||
import eu.bambooapps.material3.pullrefresh.pullRefresh
|
||||
import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -38,21 +48,33 @@ fun NetworkPosts(
|
||||
lazyPagingItems: LazyPagingItems<UIPost>,
|
||||
listState: LazyListState,
|
||||
postActions: PostActions,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ReportDrawnWhen { lazyPagingItems.itemCount > 0 }
|
||||
val refreshLoadState = lazyPagingItems.loadState.refresh
|
||||
val isRefreshing = refreshLoadState == LoadState.Loading && lazyPagingItems.itemCount == 0
|
||||
val pullRefreshState = rememberPullRefreshState(isRefreshing, lazyPagingItems::refresh)
|
||||
Box(modifier = modifier.fillMaxSize().pullRefresh(pullRefreshState)) {
|
||||
val refreshLoadState by rememberUpdatedState(lazyPagingItems.loadState.refresh)
|
||||
val state = rememberPullToRefreshState()
|
||||
PullToRefreshBox(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
isRefreshing = refreshLoadState == LoadState.Loading,
|
||||
state = state,
|
||||
onRefresh = { lazyPagingItems.refresh() },
|
||||
indicator = {
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = state,
|
||||
isRefreshing = refreshLoadState == LoadState.Loading,
|
||||
modifier = Modifier.align(Alignment.TopCenter).padding(contentPadding),
|
||||
)
|
||||
},
|
||||
) {
|
||||
if (lazyPagingItems.itemCount == 0 && refreshLoadState is LoadState.Error) {
|
||||
NetworkError(
|
||||
label = "Failed to load posts",
|
||||
error = refreshLoadState.error,
|
||||
error = (refreshLoadState as LoadState.Error).error,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
} else {
|
||||
LazyColumn(state = listState) {
|
||||
LazyColumn(contentPadding = contentPadding, state = listState) {
|
||||
items(
|
||||
count = lazyPagingItems.itemCount,
|
||||
key = lazyPagingItems.itemKey { it.shortId },
|
||||
@@ -60,11 +82,7 @@ fun NetworkPosts(
|
||||
) { index ->
|
||||
val item = lazyPagingItems[index]
|
||||
if (item != null) {
|
||||
LobstersListItem(
|
||||
item = item,
|
||||
postActions = postActions,
|
||||
refresh = { lazyPagingItems.refresh() },
|
||||
)
|
||||
LobstersListItem(item = item, postActions = postActions)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
@@ -80,11 +98,20 @@ fun NetworkPosts(
|
||||
}
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(
|
||||
refreshing = isRefreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
shadowElevation = 6.dp, // From M2 implementation
|
||||
}
|
||||
}
|
||||
|
||||
@DevicePreviews
|
||||
@Composable
|
||||
private fun ListPreview() {
|
||||
val items = List(20) { TEST_POST.copy(shortId = "${TEST_POST.shortId}${it}") }
|
||||
val flow = MutableStateFlow(PagingData.from(items))
|
||||
LobstersTheme {
|
||||
NetworkPosts(
|
||||
lazyPagingItems = flow.collectAsLazyPagingItems(),
|
||||
listState = rememberLazyListState(),
|
||||
postActions = TEST_POST_ACTIONS,
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
package dev.msfjarvis.claw.android.ui.lists
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -17,28 +19,29 @@ import androidx.compose.ui.semantics.isTraversalGroup
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.ui.SearchBar
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Composable
|
||||
fun SearchList(
|
||||
items: Flow<PagingData<UIPost>>,
|
||||
lazyPagingItems: LazyPagingItems<UIPost>,
|
||||
listState: LazyListState,
|
||||
postActions: PostActions,
|
||||
searchQuery: String,
|
||||
setSearchQuery: (String) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val lazyPagingItems = items.collectAsLazyPagingItems()
|
||||
val triggerSearch = { query: String ->
|
||||
setSearchQuery(query)
|
||||
lazyPagingItems.refresh()
|
||||
}
|
||||
Column(modifier = modifier.semantics { isTraversalGroup = true }.zIndex(1f).fillMaxWidth()) {
|
||||
Column(
|
||||
modifier =
|
||||
modifier.systemBarsPadding().semantics { isTraversalGroup = true }.zIndex(1f).fillMaxWidth()
|
||||
) {
|
||||
SearchBar(
|
||||
value = searchQuery,
|
||||
onValueChange = setSearchQuery,
|
||||
@@ -49,6 +52,7 @@ fun SearchList(
|
||||
lazyPagingItems = lazyPagingItems,
|
||||
listState = listState,
|
||||
postActions = postActions,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.navigation
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
sealed interface Destination
|
||||
|
||||
@Serializable data object Hottest : Destination
|
||||
|
||||
@Serializable data object Newest : Destination
|
||||
|
||||
@Serializable data object Saved : Destination
|
||||
|
||||
@Serializable data class Comments(val postId: String) : Destination
|
||||
|
||||
@Serializable data class User(val username: String) : Destination
|
||||
|
||||
@Serializable data object Search : Destination
|
||||
|
||||
@Serializable data object Settings : Destination
|
||||
|
||||
@Serializable data object AboutLibraries : Destination
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* 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.navigation
|
||||
|
||||
sealed class Destinations {
|
||||
abstract val route: String
|
||||
|
||||
data object Hottest : Destinations() {
|
||||
override val route = "hottest"
|
||||
}
|
||||
|
||||
data object Newest : Destinations() {
|
||||
override val route = "newest"
|
||||
}
|
||||
|
||||
data object Saved : Destinations() {
|
||||
override val route = "saved"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
data object Settings : Destinations() {
|
||||
override val route = "settings"
|
||||
}
|
||||
|
||||
data object AboutLibraries : Destinations() {
|
||||
override val route = "about_libraries"
|
||||
}
|
||||
|
||||
companion object {
|
||||
val startDestination
|
||||
get() = Hottest
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.navigation
|
||||
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* Walk through the [NavDestination]'s [hierarchy] to see if it has any destination that matches the
|
||||
* route defined by [dest].
|
||||
*/
|
||||
fun NavDestination?.matches(dest: Destination): Boolean {
|
||||
return this?.hierarchy?.any { it.hasRoute(dest::class) } == true
|
||||
}
|
||||
|
||||
/** Check if this [NavDestination] [matches] any of the potential navigation [destinations]. */
|
||||
fun NavDestination?.any(destinations: ImmutableList<Destination>): Boolean {
|
||||
return destinations.any { this?.matches(it) == true }
|
||||
}
|
||||
|
||||
/** Check if this [NavDestination] [matches] none of the potential navigation [destinations]. */
|
||||
fun NavDestination?.none(destinations: ImmutableList<Destination>): Boolean {
|
||||
return destinations.none { this?.matches(it) == true }
|
||||
}
|
||||
+88
-81
@@ -11,10 +11,8 @@ 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
|
||||
@@ -27,24 +25,23 @@ import androidx.compose.material.icons.filled.Whatshot
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.Whatshot
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
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.TopAppBar
|
||||
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.draw.shadow
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -52,16 +49,17 @@ 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.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.navigation.toRoute
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.haze
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.android.SearchActivity
|
||||
@@ -71,21 +69,28 @@ 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.navigation.AboutLibraries
|
||||
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Comments
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Hottest
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Newest
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Saved
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Settings
|
||||
import dev.msfjarvis.claw.android.ui.navigation.User
|
||||
import dev.msfjarvis.claw.android.ui.navigation.any
|
||||
import dev.msfjarvis.claw.android.ui.navigation.none
|
||||
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
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LobstersPostsScreen(
|
||||
urlLauncher: UrlLauncher,
|
||||
@@ -95,29 +100,28 @@ fun LobstersPostsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val hottestListState = rememberLazyListState()
|
||||
val newestListState = rememberLazyListState()
|
||||
val savedListState = rememberLazyListState()
|
||||
val navController = rememberNavController()
|
||||
val navBackStackEntry = navController.currentBackStackEntryAsState().value
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val postActions = rememberPostActions(urlLauncher, navController, viewModel)
|
||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentDestination = backStackEntry?.destination?.route
|
||||
val context = LocalContext.current
|
||||
val postActions = rememberPostActions(context, urlLauncher, navController, viewModel)
|
||||
val hazeState = remember { HazeState() }
|
||||
|
||||
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
|
||||
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
|
||||
val savedPosts by viewModel.savedPostsByMonth.collectAsState(persistentMapOf())
|
||||
val savedPosts by viewModel.savedPostsByMonth.collectAsStateWithLifecycle(persistentMapOf())
|
||||
|
||||
val navigationType = ClawNavigationType.fromSize(windowSizeClass.widthSizeClass)
|
||||
|
||||
val postIdOverride = context.getActivity()?.intent?.extras?.getString(MainActivity.NAVIGATION_KEY)
|
||||
LaunchedEffect(false) {
|
||||
LaunchedEffect(Unit) {
|
||||
if (postIdOverride != null) {
|
||||
navController.navigate(
|
||||
Destinations.Comments.route.replace(Destinations.Comments.PLACEHOLDER, postIdOverride)
|
||||
)
|
||||
navController.navigate(Comments(postIdOverride))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,37 +129,42 @@ fun LobstersPostsScreen(
|
||||
persistentListOf(
|
||||
NavigationItem(
|
||||
label = "Hottest",
|
||||
route = Destinations.Hottest.route,
|
||||
destination = Hottest,
|
||||
icon = Icons.Outlined.Whatshot,
|
||||
selectedIcon = Icons.Filled.Whatshot,
|
||||
) {
|
||||
coroutineScope.launch { hottestListState.animateScrollToItem(index = 0) }
|
||||
coroutineScope.launch {
|
||||
if (hottestPosts.itemCount > 0) hottestListState.animateScrollToItem(index = 0)
|
||||
}
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Newest",
|
||||
route = Destinations.Newest.route,
|
||||
destination = Newest,
|
||||
icon = Icons.Outlined.NewReleases,
|
||||
selectedIcon = Icons.Filled.NewReleases,
|
||||
) {
|
||||
coroutineScope.launch { newestListState.animateScrollToItem(index = 0) }
|
||||
coroutineScope.launch {
|
||||
if (newestPosts.itemCount > 0) newestListState.animateScrollToItem(index = 0)
|
||||
}
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Saved",
|
||||
route = Destinations.Saved.route,
|
||||
destination = Saved,
|
||||
icon = Icons.Outlined.FavoriteBorder,
|
||||
selectedIcon = Icons.Filled.Favorite,
|
||||
) {
|
||||
coroutineScope.launch { savedListState.animateScrollToItem(index = 0) }
|
||||
coroutineScope.launch { if (savedPosts.isNotEmpty()) savedListState.scrollToItem(0) }
|
||||
},
|
||||
)
|
||||
val navDestinations = navItems.map(NavigationItem::destination).toPersistentList()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
ClawAppBar(
|
||||
TopAppBar(
|
||||
modifier = Modifier.shadow(8.dp),
|
||||
navigationIcon = {
|
||||
if (
|
||||
navController.previousBackStackEntry != null &&
|
||||
navItems.none { it.route == currentDestination }
|
||||
navController.previousBackStackEntry != null && currentDestination.none(navDestinations)
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() }
|
||||
@@ -165,29 +174,27 @@ fun LobstersPostsScreen(
|
||||
contentDescription = "Go back to previous screen",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||
contentDescription = "The app icon for Claw",
|
||||
modifier = Modifier.size(48.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
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)
|
||||
}
|
||||
if (currentDestination.any(navDestinations)) {
|
||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
if (currentDestination.any(navDestinations)) {
|
||||
IconButton(
|
||||
onClick = { context.startActivity(Intent(context, SearchActivity::class.java)) }
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Search, contentDescription = "Search posts")
|
||||
}
|
||||
IconButton(onClick = { navController.navigate(Destinations.Settings.route) }) {
|
||||
IconButton(onClick = { navController.navigate(Settings) }) {
|
||||
Icon(imageVector = Icons.Filled.Tune, contentDescription = "Settings")
|
||||
}
|
||||
}
|
||||
@@ -199,63 +206,60 @@ fun LobstersPostsScreen(
|
||||
ClawNavigationBar(
|
||||
navController = navController,
|
||||
items = navItems,
|
||||
isVisible = navItems.any { it.route == currentDestination },
|
||||
isVisible = currentDestination.any(navDestinations),
|
||||
hazeState = hazeState,
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
modifier = modifier.semantics { testTagsAsResourceId = true },
|
||||
) { paddingValues ->
|
||||
Row(modifier = Modifier.padding(paddingValues)) {
|
||||
) { contentPadding ->
|
||||
Row {
|
||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.NAVIGATION_RAIL) {
|
||||
ClawNavigationRail(
|
||||
navController = navController,
|
||||
items = navItems,
|
||||
isVisible = navItems.any { it.route == currentDestination },
|
||||
isVisible = currentDestination.any(navDestinations),
|
||||
)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Destinations.startDestination.route,
|
||||
startDestination = Hottest,
|
||||
// Make animations 2x faster than default specs
|
||||
enterTransition = { fadeIn(animationSpec = tween(350)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(350)) },
|
||||
modifier = Modifier.haze(hazeState),
|
||||
) {
|
||||
val uri = LobstersApi.BASE_URL
|
||||
composable(route = Destinations.Hottest.route) {
|
||||
composable<Hottest> {
|
||||
setWebUri("https://lobste.rs/")
|
||||
NetworkPosts(
|
||||
lazyPagingItems = hottestPosts,
|
||||
listState = hottestListState,
|
||||
postActions = postActions,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.Newest.route) {
|
||||
composable<Newest> {
|
||||
setWebUri("https://lobste.rs/")
|
||||
NetworkPosts(
|
||||
lazyPagingItems = newestPosts,
|
||||
listState = newestListState,
|
||||
postActions = postActions,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.Saved.route) {
|
||||
composable<Saved> {
|
||||
setWebUri(null)
|
||||
DatabasePosts(items = savedPosts, listState = savedListState, postActions = postActions)
|
||||
DatabasePosts(
|
||||
items = savedPosts,
|
||||
listState = savedListState,
|
||||
postActions = postActions,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
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}" },
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val postId =
|
||||
requireNotNull(backStackEntry.arguments?.getString("postId")) {
|
||||
"Navigating to ${Destinations.Comments.route} without necessary 'postId' argument"
|
||||
}
|
||||
composable<Comments> { backStackEntry ->
|
||||
val postId = backStackEntry.toRoute<Comments>().postId
|
||||
setWebUri("https://lobste.rs/s/$postId")
|
||||
CommentsPage(
|
||||
postId = postId,
|
||||
@@ -263,32 +267,35 @@ fun LobstersPostsScreen(
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
contentPadding = contentPadding,
|
||||
openUserProfile = { navController.navigate(User(it)) },
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Destinations.User.route,
|
||||
arguments = listOf(navArgument("username") { type = NavType.StringType }),
|
||||
deepLinks = listOf(navDeepLink { uriPattern = "$uri/u/${Destinations.User.PLACEHOLDER}" }),
|
||||
) { backStackEntry ->
|
||||
val username =
|
||||
requireNotNull(backStackEntry.arguments?.getString("username")) {
|
||||
"Navigating to ${Destinations.User.route} without necessary 'username' argument"
|
||||
}
|
||||
composable<User> { backStackEntry ->
|
||||
val username = backStackEntry.toRoute<User>().username
|
||||
setWebUri("https://lobste.rs/u/$username")
|
||||
UserProfile(username = username, getProfile = viewModel::getUserProfile)
|
||||
UserProfile(
|
||||
username = username,
|
||||
getProfile = viewModel::getUserProfile,
|
||||
contentPadding = contentPadding,
|
||||
openUserProfile = { navController.navigate(User(it)) },
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.Settings.route) {
|
||||
composable<Settings> {
|
||||
SettingsScreen(
|
||||
context = context,
|
||||
openLibrariesScreen = { navController.navigate(Destinations.AboutLibraries.route) },
|
||||
openInputStream = context.contentResolver::openInputStream,
|
||||
openOutputStream = context.contentResolver::openOutputStream,
|
||||
openLibrariesScreen = { navController.navigate(AboutLibraries) },
|
||||
importPosts = viewModel::importPosts,
|
||||
exportPostsAsJson = viewModel::exportPostsAsJson,
|
||||
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
||||
snackbarHostState = snackbarHostState,
|
||||
contentPadding = contentPadding,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.AboutLibraries.route) {
|
||||
LibrariesContainer(modifier = Modifier.fillMaxSize())
|
||||
composable<AboutLibraries> {
|
||||
LibrariesContainer(contentPadding = contentPadding, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,24 +6,27 @@
|
||||
*/
|
||||
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.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.toRoute
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
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.navigation.Comments
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Search
|
||||
import dev.msfjarvis.claw.android.ui.navigation.User
|
||||
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
|
||||
import dev.msfjarvis.claw.common.user.UserProfile
|
||||
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
@@ -34,32 +37,24 @@ fun SearchScreen(
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val postActions = rememberPostActions(urlLauncher, navController, viewModel)
|
||||
val postActions = rememberPostActions(LocalContext.current, 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) {
|
||||
val searchResults = viewModel.searchResults.collectAsLazyPagingItems()
|
||||
Scaffold(modifier = modifier) { contentPadding ->
|
||||
NavHost(navController = navController, startDestination = Search) {
|
||||
composable<Search> {
|
||||
setWebUri("https://lobste.rs/search")
|
||||
SearchList(
|
||||
items = viewModel.searchResults,
|
||||
lazyPagingItems = searchResults,
|
||||
listState = listState,
|
||||
postActions = postActions,
|
||||
searchQuery = viewModel.searchQuery,
|
||||
contentPadding = contentPadding,
|
||||
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"
|
||||
}
|
||||
composable<Comments> { backStackEntry ->
|
||||
val postId = backStackEntry.toRoute<Comments>().postId
|
||||
setWebUri("https://lobste.rs/s/$postId")
|
||||
CommentsPage(
|
||||
postId = postId,
|
||||
@@ -67,6 +62,18 @@ fun SearchScreen(
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
openUserProfile = { navController.navigate(User(it)) },
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
composable<User> { backStackEntry ->
|
||||
val username = backStackEntry.toRoute<User>().username
|
||||
setWebUri("https://lobste.rs/u/$username")
|
||||
UserProfile(
|
||||
username = username,
|
||||
getProfile = viewModel::getUserProfile,
|
||||
openUserProfile = { navController.navigate(User(it)) },
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,19 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
||||
import androidx.compose.material.icons.filled.Bookmarks
|
||||
@@ -39,6 +40,8 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -49,28 +52,19 @@ private const val HTML_MIME_TYPE = "application/html"
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
context: Context,
|
||||
openLibrariesScreen: () -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
openInputStream: (Uri) -> InputStream?,
|
||||
openOutputStream: (Uri) -> OutputStream?,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
exportPostsAsJson: suspend (OutputStream) -> Unit,
|
||||
exportPostsAsHtml: suspend (OutputStream) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
Box(modifier = modifier.padding(contentPadding)) {
|
||||
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 = {
|
||||
@@ -85,24 +79,35 @@ fun SettingsScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ImportPosts(context, coroutineScope, snackbarHostState, importPosts)
|
||||
ImportPosts(openInputStream, coroutineScope, snackbarHostState, importPosts)
|
||||
ExportPosts(
|
||||
context,
|
||||
coroutineScope,
|
||||
snackbarHostState,
|
||||
openOutputStream,
|
||||
exportPostsAsJson,
|
||||
exportPostsAsHtml,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text("Libraries") },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.LibraryBooks,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = openLibrariesScreen),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.ImportPosts(
|
||||
context: Context,
|
||||
openInputStream: (Uri) -> InputStream?,
|
||||
coroutineScope: CoroutineScope,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
@@ -115,7 +120,7 @@ private fun RowScope.ImportPosts(
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
openInputStream(uri)?.use { stream ->
|
||||
importPosts(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully imported posts")
|
||||
}
|
||||
@@ -132,9 +137,9 @@ private fun RowScope.ImportPosts(
|
||||
|
||||
@Composable
|
||||
private fun RowScope.ExportPosts(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
openOutputStream: (Uri) -> OutputStream?,
|
||||
exportPostsAsJson: suspend (OutputStream) -> Unit,
|
||||
exportPostsAsHtml: suspend (OutputStream) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -147,7 +152,7 @@ private fun RowScope.ExportPosts(
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openOutputStream(uri)?.use { stream ->
|
||||
openOutputStream(uri)?.use { stream ->
|
||||
exportPostsAsJson(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
}
|
||||
@@ -161,7 +166,7 @@ private fun RowScope.ExportPosts(
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openOutputStream(uri)?.use { stream ->
|
||||
openOutputStream(uri)?.use { stream ->
|
||||
exportPostsAsHtml(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
}
|
||||
@@ -207,3 +212,11 @@ private suspend fun SnackbarHostState.showSnackbarDismissing(text: String) {
|
||||
currentSnackbarData?.dismiss()
|
||||
showSnackbar(text)
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun SettingsScreenPreview() {
|
||||
LobstersTheme {
|
||||
SettingsScreen({}, SnackbarHostState(), { null }, { null }, {}, {}, {}, PaddingValues())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ 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.android.ui.toError
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.core.injection.MainDispatcher
|
||||
@@ -39,7 +40,6 @@ 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.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -133,19 +133,21 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun isPostRead(post: UIPost): Boolean {
|
||||
return _readPosts.contains(post.shortId)
|
||||
}
|
||||
|
||||
fun isPostSaved(post: UIPost): Boolean {
|
||||
return _savedPosts.contains(post.shortId)
|
||||
}
|
||||
|
||||
suspend fun getPostComments(postId: String) =
|
||||
withContext(ioDispatcher) {
|
||||
when (val result = api.getPostDetails(postId)) {
|
||||
is Success -> result.value.toUIPost()
|
||||
is Failure.NetworkFailure -> throw result.error
|
||||
is Failure.UnknownFailure -> throw result.error
|
||||
is Failure.HttpFailure -> {
|
||||
if (result.code == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||
throw IOException("Story was removed by moderator")
|
||||
} else {
|
||||
throw IOException("API returned an invalid response")
|
||||
}
|
||||
}
|
||||
is Failure.HttpFailure -> throw result.toError()
|
||||
is Failure.ApiFailure -> throw IOException("API returned an invalid response")
|
||||
}
|
||||
}
|
||||
@@ -165,7 +167,7 @@ constructor(
|
||||
is Success -> result.value
|
||||
is Failure.NetworkFailure -> throw result.error
|
||||
is Failure.UnknownFailure -> throw result.error
|
||||
is Failure.HttpFailure,
|
||||
is Failure.HttpFailure -> throw result.toError()
|
||||
is Failure.ApiFailure -> throw IOException("API returned an invalid response")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +25,4 @@ constructor(
|
||||
suspend fun markRead(postId: String) {
|
||||
withContext(dbDispatcher) { readPostsQueries.markRead(postId) }
|
||||
}
|
||||
|
||||
suspend fun isPostRead(shortId: String): Boolean {
|
||||
return withContext(dbDispatcher) { readPostsQueries.isPostRead(shortId).executeAsOne() }
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -16,6 +16,7 @@ import dev.msfjarvis.claw.model.toSavedPost
|
||||
import io.github.aakira.napier.Napier
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SavedPostsRepository
|
||||
@@ -27,7 +28,7 @@ constructor(
|
||||
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
|
||||
|
||||
suspend fun toggleSave(post: UIPost) {
|
||||
if (post.isSaved) {
|
||||
if (savedPosts.firstOrNull().orEmpty().any { it.shortId == post.shortId }) {
|
||||
Napier.d(tag = TAG) { "Removing post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
|
||||
} else {
|
||||
@@ -45,10 +46,6 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun isPostSaved(postId: String): Boolean {
|
||||
return withContext(dbDispatcher) { savedPostQueries.isPostSaved(postId).executeAsOne() }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "SavedPostsRepository"
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.lists
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import dev.msfjarvis.claw.common.posts.TEST_POST
|
||||
import dev.msfjarvis.claw.common.posts.TEST_POST_ACTIONS
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@SuppressLint("ComposePreviewPublic", "ComposeUnstableReceiver")
|
||||
class NetworkPostsTest {
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun DefaultPreview() {
|
||||
val items = List(20) { TEST_POST.copy(shortId = "${TEST_POST.shortId}${it}") }
|
||||
val flow = MutableStateFlow(PagingData.from(items))
|
||||
LobstersTheme {
|
||||
NetworkPosts(
|
||||
lazyPagingItems = flow.collectAsLazyPagingItems(),
|
||||
listState = rememberLazyListState(),
|
||||
postActions = TEST_POST_ACTIONS,
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
|
||||
#
|
||||
versioning-plugin.versionCode=14400
|
||||
versioning-plugin.versionName=1.44.0
|
||||
versioning-plugin.versionCode=15100
|
||||
versioning-plugin.versionName=1.51.0
|
||||
|
||||
@@ -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.
|
||||
@@ -18,17 +18,18 @@ android { namespace = "dev.msfjarvis.claw.api" }
|
||||
anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
dependencies {
|
||||
api(libs.eithernet)
|
||||
api(libs.dagger)
|
||||
api(libs.javax.inject)
|
||||
api(libs.okhttp.core)
|
||||
api(libs.retrofit)
|
||||
api(projects.model)
|
||||
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.javax.inject)
|
||||
implementation(libs.eithernet)
|
||||
implementation(libs.jsoup)
|
||||
|
||||
testImplementation(testFixtures(libs.eithernet))
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.kotlinx.serialization.core)
|
||||
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" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
<issues format="6" by="lint 8.4.2" type="baseline" client="gradle" dependencies="true" name="AGP (8.4.2)" variant="all" version="8.4.2">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -32,6 +32,11 @@ object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
|
||||
val tags = elem.select("span.tags > a").map(Element::text)
|
||||
val (commentCount, commentsUrl) = getCommentsData(elem.select("span.comments_label"))
|
||||
val submitter = elem.select("div.byline > a.u-author").text()
|
||||
val userIsAuthor =
|
||||
(elem.select("div.byline > span").first()?.text() ?: "").contains(
|
||||
"authored",
|
||||
ignoreCase = true,
|
||||
)
|
||||
return LobstersPost(
|
||||
shortId = shortId,
|
||||
title = title,
|
||||
@@ -43,6 +48,7 @@ object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
|
||||
// The value of these fields is irrelevant for our use case
|
||||
createdAt = "",
|
||||
description = "",
|
||||
userIsAuthor = userIsAuthor,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class SearchApiTest {
|
||||
submitter = "xenodium",
|
||||
tags = listOf("ai", "emacs"),
|
||||
description = "",
|
||||
userIsAuthor = true,
|
||||
),
|
||||
LobstersPost(
|
||||
shortId = "astcqf",
|
||||
@@ -52,6 +53,7 @@ class SearchApiTest {
|
||||
submitter = "asteroid",
|
||||
tags = listOf("ai"),
|
||||
description = "",
|
||||
userIsAuthor = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +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 format="6" by="lint 8.4.2" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.2)" variant="all" version="8.4.2">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -27,8 +27,8 @@ pluginManagement {
|
||||
forRepository { google() }
|
||||
filter {
|
||||
includeGroup("androidx.databinding")
|
||||
includeGroupByRegex("com.android.*")
|
||||
includeGroup("com.google.testing.platform")
|
||||
includeGroupAndSubgroups("com.android")
|
||||
}
|
||||
}
|
||||
mavenCentral { mavenContent { releasesOnly() } }
|
||||
|
||||
@@ -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.
|
||||
@@ -25,21 +25,14 @@ import org.gradle.kotlin.dsl.findByType
|
||||
|
||||
class AndroidCommonPlugin : Plugin<Project> {
|
||||
|
||||
private companion object {
|
||||
const val COMPILE_SDK = 34
|
||||
const val MIN_SDK = 26
|
||||
const val TARGET_SDK = 34
|
||||
const val SLIM_TESTS_PROPERTY = "slimTests"
|
||||
}
|
||||
|
||||
override fun apply(project: Project) {
|
||||
project.configureSlimTests()
|
||||
project.pluginManager.apply(AndroidCacheFixPlugin::class)
|
||||
project.extensions.configure<BaseExtension> {
|
||||
compileSdkVersion(COMPILE_SDK)
|
||||
compileSdkVersion(35)
|
||||
defaultConfig {
|
||||
minSdk = MIN_SDK
|
||||
targetSdk = TARGET_SDK
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
@@ -64,7 +57,7 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
}
|
||||
|
||||
private fun Project.configureSlimTests() {
|
||||
if (!providers.gradleProperty(SLIM_TESTS_PROPERTY).isPresent) {
|
||||
if (!providers.gradleProperty("slimTests").isPresent) {
|
||||
return
|
||||
}
|
||||
// Disable unit test tasks on the release build type for Android Library projects
|
||||
|
||||
@@ -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,15 +7,12 @@
|
||||
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> {
|
||||
@@ -26,25 +23,5 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class KotlinCommonPlugin : Plugin<Project> {
|
||||
project.tasks.run {
|
||||
withType<KotlinCompile>().configureEach {
|
||||
compilerOptions {
|
||||
allWarningsAsErrors.set(true)
|
||||
allWarningsAsErrors.set(project.providers.environmentVariable("CI").isPresent)
|
||||
freeCompilerArgs.addAll(ADDITIONAL_COMPILER_ARGS)
|
||||
languageVersion.set(KotlinVersion.KOTLIN_1_9)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class KotlinKaptPlugin : Plugin<Project> {
|
||||
// https://github.com/google/dagger/commit/0d2505a727b54f47b8677f42dd4fc5c1924e37f5
|
||||
option("-Adagger.experimentalDaggerErrorMessages=enabled")
|
||||
// KAPT nests errors causing real issues to be suppressed in CI logs
|
||||
option("-Xmaxerrs", 500)
|
||||
option("-Xmaxerrs", "500")
|
||||
// Enables per-module validation for faster error detection
|
||||
// https://github.com/google/dagger/commit/325b516ac6a53d3fc973d247b5231fafda9870a2
|
||||
option("-Adagger.moduleBindingValidation=ERROR")
|
||||
|
||||
@@ -55,6 +55,6 @@ class SpotlessPlugin : Plugin<Project> {
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val KTFMT_VERSION = "0.47"
|
||||
private const val KTFMT_VERSION = "0.52"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -16,19 +16,23 @@ import org.gradle.kotlin.dsl.exclude
|
||||
/** Extension function to configure JUnit5 dependencies with the Truth assertion library. */
|
||||
fun DependencyHandlerScope.addTestDependencies(project: Project) {
|
||||
val libs = project.extensions.getByName("libs") as LibrariesForLibs
|
||||
addProvider("testImplementation", libs.junit.jupiter.api)
|
||||
addProvider<MinimalExternalModuleDependency, ExternalModuleDependency>(
|
||||
"testImplementation",
|
||||
libs.truth,
|
||||
) {
|
||||
exclude(group = "junit", module = "junit")
|
||||
}
|
||||
addProvider("testRuntimeOnly", libs.junit.jupiter.engine)
|
||||
addProvider<MinimalExternalModuleDependency, ExternalModuleDependency>(
|
||||
"testRuntimeOnly",
|
||||
libs.junit.legacy,
|
||||
) {
|
||||
// See https://github.com/google/truth/issues/333
|
||||
because("Truth needs it")
|
||||
}
|
||||
arrayOf("test", "androidTest", "screenshotTest")
|
||||
.filter { sourceSet -> project.configurations.findByName("${sourceSet}Implementation") != null }
|
||||
.forEach { sourceSet ->
|
||||
addProvider("${sourceSet}Implementation", libs.junit.jupiter.api)
|
||||
addProvider<MinimalExternalModuleDependency, ExternalModuleDependency>(
|
||||
"${sourceSet}Implementation",
|
||||
libs.truth,
|
||||
) {
|
||||
exclude(group = "junit", module = "junit")
|
||||
}
|
||||
addProvider("${sourceSet}RuntimeOnly", libs.junit.jupiter.engine)
|
||||
addProvider<MinimalExternalModuleDependency, ExternalModuleDependency>(
|
||||
"${sourceSet}RuntimeOnly",
|
||||
libs.junit.legacy,
|
||||
) {
|
||||
// See https://github.com/google/truth/issues/333
|
||||
because("Truth needs it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,6 @@ plugins {
|
||||
id("dev.msfjarvis.claw.spotless")
|
||||
id("dev.msfjarvis.claw.versions")
|
||||
id("dev.msfjarvis.claw.kotlin-common")
|
||||
alias(libs.plugins.dependencyAnalysis)
|
||||
alias(libs.plugins.invert)
|
||||
}
|
||||
|
||||
+16
-5
@@ -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,12 +7,15 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.android.build.api.variant.HasUnitTestBuilder
|
||||
import dev.msfjarvis.claw.gradle.addTestDependencies
|
||||
|
||||
plugins {
|
||||
id("dev.msfjarvis.claw.android-library")
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.whetstone)
|
||||
alias(libs.plugins.kotlin.composeCompiler)
|
||||
alias(libs.plugins.screenshot)
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -20,11 +23,9 @@ android {
|
||||
androidResources = true
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
useLiveLiterals = false
|
||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
}
|
||||
composeOptions { useLiveLiterals = false }
|
||||
namespace = "dev.msfjarvis.claw.common"
|
||||
experimentalProperties["android.experimental.enableScreenshotTest"] = true
|
||||
}
|
||||
|
||||
androidComponents { beforeVariants { (it as HasUnitTestBuilder).enableUnitTest = false } }
|
||||
@@ -32,6 +33,9 @@ androidComponents { beforeVariants { (it as HasUnitTestBuilder).enableUnitTest =
|
||||
anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
dependencies {
|
||||
api(libs.androidx.compose.ui)
|
||||
api(libs.dagger)
|
||||
api(libs.javax.inject)
|
||||
api(projects.core)
|
||||
api(projects.database.core)
|
||||
api(projects.model)
|
||||
@@ -44,16 +48,23 @@ dependencies {
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.coil)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.compose.richtext.markdown)
|
||||
implementation(libs.compose.richtext.material3)
|
||||
implementation(libs.compose.richtext.ui)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinResult)
|
||||
implementation(libs.kotlinResult.coroutines)
|
||||
implementation(libs.napier)
|
||||
|
||||
screenshotTestImplementation(libs.androidx.compose.ui.tooling)
|
||||
|
||||
compileOnly(libs.androidx.compose.ui.tooling.preview)
|
||||
|
||||
runtimeOnly(libs.androidx.compose.ui.tooling)
|
||||
|
||||
addTestDependencies(project)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 format="6" by="lint 8.4.2" type="baseline" client="gradle" dependencies="true" name="AGP (8.4.2)" variant="all" version="8.4.2">
|
||||
|
||||
</issues>
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9d9d4a162b7ba995f625b4feb683879de46ad1c583a37c5cc721030521cece11
|
||||
size 6633
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.comments
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -27,15 +26,9 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.michaelbull.result.coroutines.runSuspendCatching
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
@@ -47,8 +40,6 @@ import dev.msfjarvis.claw.common.ui.NetworkImage
|
||||
import dev.msfjarvis.claw.common.ui.ThemedRichText
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import java.time.Instant
|
||||
import java.time.temporal.TemporalAccessor
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
@@ -56,9 +47,9 @@ internal fun CommentsHeader(
|
||||
post: UIPost,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val linkMetadata by
|
||||
produceState(initialValue = LinkMetadata(post.url, null)) {
|
||||
runSuspendCatching { postActions.getLinkMetadata(post.url) }
|
||||
@@ -70,7 +61,7 @@ internal fun CommentsHeader(
|
||||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
PostTitle(title = post.title, isRead = false)
|
||||
PostTitle(title = post.title, isRead = false, singleLineTitle = false)
|
||||
TagRow(tags = post.tags.toImmutableList())
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
@@ -93,8 +84,7 @@ internal fun CommentsHeader(
|
||||
text = AnnotatedString("Submitted by ${post.submitter}"),
|
||||
avatarUrl = "https://lobste.rs/avatars/${post.submitter}-100.png",
|
||||
contentDescription = "User avatar for ${post.submitter}",
|
||||
modifier =
|
||||
Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${post.submitter}") },
|
||||
modifier = Modifier.clickable { openUserProfile(post.submitter) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -125,105 +115,3 @@ private fun PostLink(linkMetadata: LinkMetadata, modifier: Modifier = Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val CommentEntryPadding = 16f.dp
|
||||
|
||||
@Composable
|
||||
internal fun CommentEntry(
|
||||
commentNode: CommentNode,
|
||||
htmlConverter: HTMLConverter,
|
||||
toggleExpanded: (CommentNode) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val comment = commentNode.comment
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { toggleExpanded(commentNode) }
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(
|
||||
start = CommentEntryPadding * commentNode.indentLevel,
|
||||
end = CommentEntryPadding,
|
||||
top = CommentEntryPadding,
|
||||
bottom = CommentEntryPadding,
|
||||
)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Submitter(
|
||||
text =
|
||||
buildCommenterString(
|
||||
commenterName = comment.user,
|
||||
score = comment.score,
|
||||
isUnread = commentNode.isUnread,
|
||||
createdAt = comment.createdAt,
|
||||
updatedAt = comment.updatedAt,
|
||||
),
|
||||
avatarUrl = "https://lobste.rs/avatars/${comment.user}-100.png",
|
||||
contentDescription = "User avatar for ${comment.user}",
|
||||
modifier = Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${comment.user}") },
|
||||
)
|
||||
if (commentNode.isExpanded) {
|
||||
ThemedRichText(
|
||||
text = htmlConverter.convertHTMLToMarkdown(comment.comment),
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun buildCommenterString(
|
||||
commenterName: String,
|
||||
score: Int,
|
||||
isUnread: Boolean,
|
||||
createdAt: TemporalAccessor,
|
||||
updatedAt: TemporalAccessor,
|
||||
): AnnotatedString {
|
||||
val now = System.currentTimeMillis()
|
||||
val createdRelative =
|
||||
remember(createdAt) {
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
Instant.from(createdAt).toEpochMilli(),
|
||||
now,
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
)
|
||||
}
|
||||
val updatedRelative =
|
||||
remember(updatedAt) {
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
Instant.from(updatedAt).toEpochMilli(),
|
||||
now,
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
)
|
||||
}
|
||||
return buildAnnotatedString {
|
||||
append(commenterName)
|
||||
append(' ')
|
||||
append('•')
|
||||
append(' ')
|
||||
append("$score points")
|
||||
append(' ')
|
||||
append('•')
|
||||
append(' ')
|
||||
append(createdRelative.toString())
|
||||
if (updatedRelative != createdRelative) {
|
||||
append(' ')
|
||||
append('(')
|
||||
append("Updated")
|
||||
append(' ')
|
||||
append(updatedRelative.toString())
|
||||
append(')')
|
||||
}
|
||||
if (isUnread) {
|
||||
append(' ')
|
||||
withStyle(
|
||||
style = SpanStyle(fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.error)
|
||||
) {
|
||||
append("(unread)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,28 +6,61 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.comments
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
|
||||
internal data class CommentNode(
|
||||
internal class CommentNode(
|
||||
val comment: Comment,
|
||||
var parent: CommentNode? = null,
|
||||
private var parent: CommentNode? = null,
|
||||
val children: MutableList<CommentNode> = mutableListOf(),
|
||||
val isUnread: Boolean = false,
|
||||
var isExpanded: Boolean = true,
|
||||
var indentLevel: Int,
|
||||
val indentLevel: Int,
|
||||
) {
|
||||
|
||||
fun addChild(child: CommentNode) {
|
||||
if (comment.shortId == child.comment.parentComment) {
|
||||
children.add(child)
|
||||
child.parent = this
|
||||
} else {
|
||||
child.indentLevel += 1
|
||||
children.lastOrNull()?.addChild(child)
|
||||
children
|
||||
.lastOrNull()
|
||||
?.addChild(
|
||||
CommentNode(
|
||||
comment = child.comment,
|
||||
parent = child.parent,
|
||||
isUnread = child.isUnread,
|
||||
indentLevel = child.indentLevel + 1,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [CommentNode.equals] and [CommentNode.hashCode] are hand-rolled to drop the
|
||||
* [CommentNode.parent] field from the comparison since it's possible for there to be cycles in
|
||||
* this comparison check. For our purposes we're fine with foregoing the field.
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as CommentNode
|
||||
|
||||
if (comment != other.comment) return false
|
||||
if (children != other.children) return false
|
||||
if (isUnread != other.isUnread) return false
|
||||
if (indentLevel != other.indentLevel) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = comment.hashCode()
|
||||
result = 31 * result + children.hashCode()
|
||||
result = 31 * result + isUnread.hashCode()
|
||||
result = 31 * result + indentLevel
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createListNode(
|
||||
@@ -47,63 +80,16 @@ internal fun createListNode(
|
||||
)
|
||||
)
|
||||
} else {
|
||||
commentNodes.lastOrNull()?.let {
|
||||
it.addChild(
|
||||
commentNodes.lastOrNull()?.let { commentNode ->
|
||||
commentNode.addChild(
|
||||
CommentNode(
|
||||
comment = comments[i],
|
||||
isUnread = isUnread(comments[i].shortId),
|
||||
indentLevel = it.indentLevel + 1,
|
||||
indentLevel = commentNode.indentLevel + 1,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commentNodes
|
||||
}
|
||||
|
||||
internal fun setExpanded(commentNode: CommentNode, expanded: Boolean): CommentNode {
|
||||
commentNode.isExpanded = expanded
|
||||
|
||||
if (commentNode.children.isNotEmpty()) {
|
||||
commentNode.children.forEach { setExpanded(it, expanded) }
|
||||
}
|
||||
return commentNode
|
||||
}
|
||||
|
||||
internal fun findTopMostParent(node: CommentNode): CommentNode {
|
||||
val parent = node.parent
|
||||
return if (parent != null) {
|
||||
findTopMostParent(parent)
|
||||
} else {
|
||||
node
|
||||
}
|
||||
}
|
||||
|
||||
internal fun LazyListScope.nodes(
|
||||
nodes: List<CommentNode>,
|
||||
htmlConverter: HTMLConverter,
|
||||
toggleExpanded: (CommentNode) -> Unit,
|
||||
) {
|
||||
nodes.forEach { node ->
|
||||
node(node = node, htmlConverter = htmlConverter, toggleExpanded = toggleExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.node(
|
||||
node: CommentNode,
|
||||
htmlConverter: HTMLConverter,
|
||||
toggleExpanded: (CommentNode) -> Unit,
|
||||
) {
|
||||
// Skip the node if neither the node nor its parent is expanded
|
||||
if (!node.isExpanded && node.parent?.isExpanded == false) {
|
||||
return
|
||||
}
|
||||
item {
|
||||
CommentEntry(commentNode = node, htmlConverter = htmlConverter, toggleExpanded = toggleExpanded)
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (node.children.isNotEmpty()) {
|
||||
nodes(node.children, htmlConverter = htmlConverter, toggleExpanded = toggleExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-69
@@ -9,22 +9,11 @@ package dev.msfjarvis.claw.common.comments
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.michaelbull.result.coroutines.runSuspendCatching
|
||||
import com.github.michaelbull.result.fold
|
||||
import dev.msfjarvis.claw.common.NetworkState
|
||||
@@ -38,64 +27,7 @@ import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun CommentsPageInternal(
|
||||
details: UIPost,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
commentState: PostComments?,
|
||||
markSeenComments: (String, List<Comment>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val commentNodes = createListNode(details.comments, commentState).toMutableStateList()
|
||||
LaunchedEffect(key1 = commentNodes) { markSeenComments(details.shortId, details.comments) }
|
||||
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
|
||||
LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) {
|
||||
item {
|
||||
CommentsHeader(post = details, postActions = postActions, htmlConverter = htmlConverter)
|
||||
}
|
||||
|
||||
if (commentNodes.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "Comments",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
)
|
||||
}
|
||||
|
||||
nodes(
|
||||
nodes = commentNodes,
|
||||
htmlConverter = htmlConverter,
|
||||
toggleExpanded = { node ->
|
||||
val newNode = setExpanded(node, !node.isExpanded)
|
||||
val parent = findTopMostParent(newNode)
|
||||
val index =
|
||||
commentNodes.indexOf(commentNodes.find { it.comment.url == parent.comment.url })
|
||||
if (index != -1) {
|
||||
commentNodes.removeAt(index)
|
||||
commentNodes.add(index, parent)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
item {
|
||||
Text(
|
||||
text = "No Comments",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "LongParameterList")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
fun CommentsPage(
|
||||
postId: String,
|
||||
@@ -103,7 +35,9 @@ fun CommentsPage(
|
||||
htmlConverter: HTMLConverter,
|
||||
getSeenComments: suspend (String) -> PostComments?,
|
||||
markSeenComments: (String, List<Comment>) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
openUserProfile: (String) -> Unit,
|
||||
) {
|
||||
val postDetails by
|
||||
produceState<NetworkState>(Loading) {
|
||||
@@ -124,6 +58,8 @@ fun CommentsPage(
|
||||
htmlConverter = htmlConverter,
|
||||
commentState = commentState,
|
||||
markSeenComments = markSeenComments,
|
||||
openUserProfile = openUserProfile,
|
||||
contentPadding = contentPadding,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* 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.common.comments
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
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.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.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.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.posts.Submitter
|
||||
import dev.msfjarvis.claw.common.ui.ThemedRichText
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import java.time.Instant
|
||||
import java.time.temporal.TemporalAccessor
|
||||
|
||||
@Composable
|
||||
internal fun CommentsPageInternal(
|
||||
details: UIPost,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
commentState: PostComments?,
|
||||
markSeenComments: (String, List<Comment>) -> Unit,
|
||||
openUserProfile: (String) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val commentNodes = createListNode(details.comments, commentState)
|
||||
LaunchedEffect(key1 = commentNodes) {
|
||||
if (details.comments.isNotEmpty() && !commentState?.commentIds.isNullOrEmpty()) {
|
||||
val unreadCount = details.comments.size - (commentState?.commentIds?.size ?: 0)
|
||||
if (unreadCount > 0) {
|
||||
val text =
|
||||
when (unreadCount) {
|
||||
1 -> "$unreadCount unread comment"
|
||||
else -> "$unreadCount unread comments"
|
||||
}
|
||||
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
markSeenComments(details.shortId, details.comments)
|
||||
}
|
||||
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
|
||||
LazyColumn(modifier = modifier, contentPadding = contentPadding) {
|
||||
item {
|
||||
CommentsHeader(
|
||||
post = details,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
openUserProfile = openUserProfile,
|
||||
)
|
||||
}
|
||||
|
||||
if (commentNodes.isNotEmpty()) {
|
||||
item(key = "comments_header") {
|
||||
Text(
|
||||
text = "Comments",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
)
|
||||
}
|
||||
|
||||
commentNodes.forEach { node ->
|
||||
item(key = node.comment.shortId) { Node(node, htmlConverter, openUserProfile) }
|
||||
}
|
||||
|
||||
item(key = "bottom_spacer") {
|
||||
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
Text(
|
||||
text = "No Comments",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple tree view implementation by Anton Shilov who was smarter in 2020 than I am today
|
||||
* https://gist.github.com/antonshilov/ef8cd0a360a5cc0f823b2a4e85084720
|
||||
*/
|
||||
@Composable
|
||||
private fun Node(
|
||||
node: CommentNode,
|
||||
htmlConverter: HTMLConverter,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
var isChildrenShown by remember { mutableStateOf(true) }
|
||||
|
||||
NodeBox(
|
||||
node = node,
|
||||
isExpanded = isChildrenShown,
|
||||
htmlConverter = htmlConverter,
|
||||
openUserProfile,
|
||||
modifier = modifier.clickable(onClick = { isChildrenShown = !isChildrenShown }),
|
||||
)
|
||||
|
||||
if (isChildrenShown) {
|
||||
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
node.children.forEach { model -> Node(model, htmlConverter, openUserProfile) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NodeBox(
|
||||
node: CommentNode,
|
||||
isExpanded: Boolean,
|
||||
htmlConverter: HTMLConverter,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
CommentEntry(isExpanded, node, htmlConverter, openUserProfile, modifier)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
private val CommentEntryPadding = 16f.dp
|
||||
|
||||
@Composable
|
||||
private fun CommentEntry(
|
||||
isExpanded: Boolean,
|
||||
commentNode: CommentNode,
|
||||
htmlConverter: HTMLConverter,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val comment = commentNode.comment
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
if (commentNode.isUnread) MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
else MaterialTheme.colorScheme.background
|
||||
)
|
||||
.padding(
|
||||
start = CommentEntryPadding * commentNode.indentLevel,
|
||||
end = CommentEntryPadding,
|
||||
top = CommentEntryPadding,
|
||||
bottom = CommentEntryPadding,
|
||||
)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Submitter(
|
||||
text =
|
||||
buildCommenterString(
|
||||
commenterName = comment.user,
|
||||
score = comment.score,
|
||||
createdAt = comment.createdAt,
|
||||
updatedAt = comment.updatedAt,
|
||||
),
|
||||
avatarUrl = "https://lobste.rs/avatars/${comment.user}-100.png",
|
||||
contentDescription = "User avatar for ${comment.user}",
|
||||
modifier = Modifier.clickable { openUserProfile(comment.user) },
|
||||
)
|
||||
if (isExpanded) {
|
||||
ThemedRichText(
|
||||
text = htmlConverter.convertHTMLToMarkdown(comment.comment),
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildCommenterString(
|
||||
commenterName: String,
|
||||
score: Int,
|
||||
createdAt: TemporalAccessor,
|
||||
updatedAt: TemporalAccessor,
|
||||
): AnnotatedString {
|
||||
val now = System.currentTimeMillis()
|
||||
val createdRelative =
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
Instant.from(createdAt).toEpochMilli(),
|
||||
now,
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
)
|
||||
val updatedRelative =
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
Instant.from(updatedAt).toEpochMilli(),
|
||||
now,
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
)
|
||||
return buildAnnotatedString {
|
||||
append(commenterName)
|
||||
append(' ')
|
||||
append('•')
|
||||
append(' ')
|
||||
append("$score points")
|
||||
append(' ')
|
||||
append('•')
|
||||
append(' ')
|
||||
append(createdRelative.toString())
|
||||
if (updatedRelative != createdRelative) {
|
||||
append(' ')
|
||||
append('(')
|
||||
append("Updated")
|
||||
append(' ')
|
||||
append(updatedRelative.toString())
|
||||
append(')')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.posts
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -35,7 +36,9 @@ import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -47,6 +50,7 @@ import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.NetworkImage
|
||||
@@ -57,56 +61,39 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun LobstersCard(
|
||||
post: UIPost,
|
||||
postActions: PostActions,
|
||||
refresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var localReadState by remember(post) { mutableStateOf(post.isRead) }
|
||||
var localSavedState by remember(post) { mutableStateOf(post.isSaved) }
|
||||
fun LobstersCard(post: UIPost, postActions: PostActions, modifier: Modifier = Modifier) {
|
||||
val readState by remember(post.shortId) { derivedStateOf { postActions.isPostRead(post) } }
|
||||
val savedState by remember(post.shortId) { derivedStateOf { postActions.isPostSaved(post) } }
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
postActions.viewPost(post.shortId, post.url, post.commentsUrl)
|
||||
localReadState = true
|
||||
refresh()
|
||||
}
|
||||
.clickable { postActions.viewPost(post.shortId, post.url, post.commentsUrl) }
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(start = 16.dp, top = 16.dp, end = 4.dp, bottom = 16.dp)
|
||||
.padding(start = 8.dp, top = 4.dp, bottom = 4.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
PostDetails(post = post, isRead = localReadState, modifier = Modifier.weight(1f))
|
||||
PostDetails(
|
||||
post = post,
|
||||
isRead = { readState },
|
||||
singleLineTitle = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SaveButton(
|
||||
isSaved = localSavedState,
|
||||
modifier =
|
||||
Modifier.clickable(role = Role.Button) {
|
||||
localSavedState = !localSavedState
|
||||
postActions.toggleSave(post)
|
||||
refresh()
|
||||
},
|
||||
)
|
||||
SaveButton(isSaved = { savedState }, onClick = { postActions.toggleSave(post) })
|
||||
HorizontalDivider(modifier = Modifier.width(48.dp))
|
||||
CommentsButton(
|
||||
commentCount = post.commentCount,
|
||||
modifier =
|
||||
Modifier.clickable(
|
||||
role = Role.Button,
|
||||
onClick = {
|
||||
postActions.viewComments(post.shortId)
|
||||
localReadState = true
|
||||
refresh()
|
||||
},
|
||||
onClick = { postActions.viewComments(post.shortId) },
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -115,13 +102,21 @@ fun LobstersCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PostDetails(post: UIPost, isRead: Boolean, modifier: Modifier = Modifier) {
|
||||
fun PostDetails(
|
||||
post: UIPost,
|
||||
isRead: () -> Boolean,
|
||||
singleLineTitle: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PostTitle(title = post.title, isRead = isRead)
|
||||
PostTitle(title = post.title, isRead = isRead(), singleLineTitle = singleLineTitle)
|
||||
TagRow(tags = post.tags.toImmutableList())
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Submitter(
|
||||
text = AnnotatedString("Submitted by ${post.submitter}"),
|
||||
text =
|
||||
AnnotatedString(
|
||||
"${if (post.userIsAuthor) "Authored" else "Submitted"} by ${post.submitter}"
|
||||
),
|
||||
avatarUrl = "https://lobste.rs/avatars/${post.submitter}-100.png",
|
||||
contentDescription = "User avatar for ${post.submitter}",
|
||||
)
|
||||
@@ -129,13 +124,20 @@ fun PostDetails(post: UIPost, isRead: Boolean, modifier: Modifier = Modifier) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun PostTitle(title: String, isRead: Boolean, modifier: Modifier = Modifier) {
|
||||
internal fun PostTitle(
|
||||
title: String,
|
||||
isRead: Boolean,
|
||||
singleLineTitle: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = modifier,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = if (isRead) FontWeight.Normal else FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = if (singleLineTitle) 1 else Int.MAX_VALUE,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -168,36 +170,38 @@ internal fun Submitter(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveButton(isSaved: Boolean, modifier: Modifier = Modifier) {
|
||||
Crossfade(targetState = isSaved, label = "save-button") { saved ->
|
||||
Box(modifier = modifier.padding(12.dp)) {
|
||||
private fun SaveButton(isSaved: () -> Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
var localSavedState by remember { mutableStateOf(isSaved()) }
|
||||
Crossfade(targetState = localSavedState, label = "save-button") { saved ->
|
||||
Box(modifier = modifier.minimumInteractiveComponentSize()) {
|
||||
Icon(
|
||||
imageVector = if (saved) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
contentDescription = if (saved) "Remove from saved posts" else "Add to saved posts",
|
||||
modifier = Modifier.align(Alignment.Center).testTag("save_button"),
|
||||
modifier =
|
||||
Modifier.align(Alignment.Center).testTag("save_button").clickable(role = Role.Button) {
|
||||
onClick()
|
||||
localSavedState = !localSavedState
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CommentsButton(commentCount: Int?, modifier: Modifier = Modifier) {
|
||||
private fun CommentsButton(commentCount: Int, modifier: Modifier = Modifier) {
|
||||
BadgedBox(
|
||||
modifier = modifier.padding(12.dp),
|
||||
modifier = modifier.minimumInteractiveComponentSize(),
|
||||
badge = {
|
||||
if (commentCount != null) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
// Required to make the badge label look visually okay
|
||||
modifier = Modifier.absoluteOffset(y = (-8).dp),
|
||||
) {
|
||||
Text(
|
||||
text = commentCount.toString(),
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
modifier = Modifier.absoluteOffset(y = (-8).dp),
|
||||
) {
|
||||
Text(
|
||||
text = commentCount.toString(),
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
@@ -235,55 +239,63 @@ private fun TagText(tag: String, modifier: Modifier = Modifier) {
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
val TEST_POST_ACTIONS =
|
||||
object : PostActions {
|
||||
override fun viewPost(postId: String, postUrl: String, commentsUrl: String) {}
|
||||
|
||||
override fun viewComments(postId: String) {}
|
||||
|
||||
override fun viewCommentsPage(post: UIPost) {}
|
||||
|
||||
override fun toggleSave(post: UIPost) {}
|
||||
|
||||
override fun share(post: UIPost) {}
|
||||
|
||||
override fun isPostRead(post: UIPost): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isPostSaved(post: UIPost): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun getComments(postId: String): UIPost {
|
||||
return UIPost(
|
||||
shortId = "ooga",
|
||||
title = "Simple Anomaly Detection Using Plain SQL",
|
||||
url = "https://hakibenita.com/sql-anomaly-detection",
|
||||
createdAt = "2020-09-21T08:04:24.000-05:00",
|
||||
commentCount = 1,
|
||||
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
|
||||
tags = listOf("databases", "apis"),
|
||||
description = "",
|
||||
submitter = "Haki",
|
||||
comments = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getLinkMetadata(url: String): LinkMetadata {
|
||||
return LinkMetadata("", "")
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
val TEST_POST =
|
||||
UIPost(
|
||||
shortId = "ooga",
|
||||
title = "Simple Anomaly Detection Using Plain SQL",
|
||||
url = "https://hakibenita.com/sql-anomaly-detection",
|
||||
createdAt = "2020-09-21T08:04:24.000-05:00",
|
||||
commentCount = 1,
|
||||
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
|
||||
submitter = "Haki",
|
||||
tags = listOf("databases", "apis"),
|
||||
description = "",
|
||||
)
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun LobstersCardPreview() {
|
||||
LobstersTheme {
|
||||
LobstersCard(
|
||||
post =
|
||||
UIPost(
|
||||
shortId = "ooga",
|
||||
title = "Simple Anomaly Detection Using Plain SQL",
|
||||
url = "https://hakibenita.com/sql-anomaly-detection",
|
||||
createdAt = "2020-09-21T08:04:24.000-05:00",
|
||||
commentCount = 1,
|
||||
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
|
||||
submitter = "Haki",
|
||||
tags = listOf("databases", "apis"),
|
||||
description = "",
|
||||
isSaved = true,
|
||||
isRead = true,
|
||||
),
|
||||
postActions =
|
||||
object : PostActions {
|
||||
override fun viewPost(postId: String, postUrl: String, commentsUrl: String) {}
|
||||
|
||||
override fun viewComments(postId: String) {}
|
||||
|
||||
override fun viewCommentsPage(commentsUrl: String) {}
|
||||
|
||||
override fun toggleSave(post: UIPost) {}
|
||||
|
||||
override suspend fun getComments(postId: String): UIPost {
|
||||
return UIPost(
|
||||
shortId = "ooga",
|
||||
title = "Simple Anomaly Detection Using Plain SQL",
|
||||
url = "https://hakibenita.com/sql-anomaly-detection",
|
||||
createdAt = "2020-09-21T08:04:24.000-05:00",
|
||||
commentCount = 1,
|
||||
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
|
||||
tags = listOf("databases", "apis"),
|
||||
description = "",
|
||||
submitter = "Haki",
|
||||
comments = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getLinkMetadata(url: String): LinkMetadata {
|
||||
return LinkMetadata("", "")
|
||||
}
|
||||
},
|
||||
refresh = {},
|
||||
)
|
||||
}
|
||||
LobstersTheme { LobstersCard(post = TEST_POST, postActions = TEST_POST_ACTIONS) }
|
||||
}
|
||||
|
||||
@@ -16,10 +16,16 @@ interface PostActions {
|
||||
|
||||
fun viewComments(postId: String)
|
||||
|
||||
fun viewCommentsPage(commentsUrl: String)
|
||||
fun viewCommentsPage(post: UIPost)
|
||||
|
||||
fun toggleSave(post: UIPost)
|
||||
|
||||
fun share(post: UIPost)
|
||||
|
||||
fun isPostRead(post: UIPost): Boolean
|
||||
|
||||
fun isPostSaved(post: UIPost): Boolean
|
||||
|
||||
suspend fun getComments(postId: String): UIPost
|
||||
|
||||
suspend fun getLinkMetadata(url: String): LinkMetadata
|
||||
|
||||
@@ -18,8 +18,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ProvidedValue
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
@@ -106,7 +104,6 @@ fun LobstersTheme(
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = Color.Transparent.toArgb()
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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.common.ui
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BarChart
|
||||
import androidx.compose.material.icons.filled.BrokenImage
|
||||
import androidx.compose.material.icons.filled.HeartBroken
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBarDefaults
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
// Taken from Chris Banes' amazing app Tivi
|
||||
// https://github.com/chrisbanes/tivi/blob/836d596d74959f4235ca2395b5bbfdd6fd9c9a9e/ui/root/src/commonMain/kotlin/app/tivi/home/Home.kt#L173
|
||||
@Composable
|
||||
fun FloatingNavigationBar(
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape = MaterialTheme.shapes.extraLarge,
|
||||
containerColor: Color = NavigationBarDefaults.containerColor,
|
||||
contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
|
||||
tonalElevation: Dp = NavigationBarDefaults.Elevation,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
color = containerColor,
|
||||
contentColor = contentColor,
|
||||
tonalElevation = tonalElevation,
|
||||
shape = shape,
|
||||
border =
|
||||
BorderStroke(
|
||||
width = 0.5.dp,
|
||||
brush =
|
||||
Brush.verticalGradient(
|
||||
colors =
|
||||
listOf(
|
||||
MaterialTheme.colorScheme.surfaceVariant,
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||
)
|
||||
),
|
||||
),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 8.dp).fillMaxWidth().height(80.dp).selectableGroup(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun FloatingNavigationBarPreview() {
|
||||
FloatingNavigationBar {
|
||||
NavigationBarItem(
|
||||
selected = true,
|
||||
onClick = {},
|
||||
icon = { Icon(imageVector = Icons.Filled.HeartBroken, contentDescription = "Home") },
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = true,
|
||||
onClick = {},
|
||||
icon = { Icon(imageVector = Icons.Filled.BarChart, contentDescription = "Home") },
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = true,
|
||||
onClick = {},
|
||||
icon = { Icon(imageVector = Icons.Filled.BrokenImage, contentDescription = "Home") },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import io.github.aakira.napier.Napier
|
||||
|
||||
@Composable
|
||||
fun NetworkError(label: String, error: Throwable, modifier: Modifier = Modifier) {
|
||||
LaunchedEffect(true) { Napier.e(error, "NetworkError") { "Failed to load posts" } }
|
||||
LaunchedEffect(Unit) { Napier.e(error, "NetworkError") { "Failed to load posts" } }
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier) {
|
||||
Text(
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* 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.common.ui.decorations
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.DevicePreviews
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun ClawAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
navigationIcon: @Composable () -> Unit = {},
|
||||
title: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
TopAppBar(
|
||||
title = title,
|
||||
modifier = modifier.shadow(8.dp),
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actions,
|
||||
)
|
||||
}
|
||||
|
||||
@DevicePreviews
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun ClawAppBarPreview() {
|
||||
LobstersTheme { ClawAppBar(title = { Text("Claw", fontWeight = FontWeight.Bold) }) }
|
||||
}
|
||||
@@ -9,6 +9,7 @@ package dev.msfjarvis.claw.common.user
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
@@ -24,6 +25,8 @@ import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.michaelbull.result.coroutines.runSuspendCatching
|
||||
import com.github.michaelbull.result.fold
|
||||
@@ -42,6 +45,8 @@ import dev.msfjarvis.claw.model.User
|
||||
fun UserProfile(
|
||||
username: String,
|
||||
getProfile: suspend (username: String) -> User,
|
||||
openUserProfile: (String) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val user by
|
||||
@@ -56,11 +61,15 @@ fun UserProfile(
|
||||
}
|
||||
when (user) {
|
||||
is Success<*> -> {
|
||||
UserProfileInternal(user = (user as Success<User>).data, modifier = modifier)
|
||||
UserProfileInternal(
|
||||
user = (user as Success<User>).data,
|
||||
openUserProfile = openUserProfile,
|
||||
modifier = modifier.padding(contentPadding),
|
||||
)
|
||||
}
|
||||
is Error -> {
|
||||
val error = user as Error
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Box(modifier = Modifier.padding(contentPadding).fillMaxSize()) {
|
||||
NetworkError(
|
||||
label = error.description,
|
||||
error = error.error,
|
||||
@@ -69,7 +78,7 @@ fun UserProfile(
|
||||
}
|
||||
}
|
||||
Loading -> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(contentPadding)) {
|
||||
ProgressBar(modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
}
|
||||
@@ -77,7 +86,11 @@ fun UserProfile(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserProfileInternal(user: User, modifier: Modifier = Modifier) {
|
||||
private fun UserProfileInternal(
|
||||
user: User,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(modifier = modifier) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -93,7 +106,19 @@ private fun UserProfileInternal(user: User, modifier: Modifier = Modifier) {
|
||||
Text(text = user.username, style = MaterialTheme.typography.displaySmall)
|
||||
ThemedRichText(text = user.about)
|
||||
user.invitedBy?.let { invitedBy ->
|
||||
ThemedRichText(text = "Invited by [${invitedBy}](https://lobste.rs/u/${user.invitedBy})")
|
||||
Text(
|
||||
text =
|
||||
buildAnnotatedString {
|
||||
append("Invited by ")
|
||||
pushLink(
|
||||
LinkAnnotation.Clickable(
|
||||
tag = "username",
|
||||
linkInteractionListener = { openUserProfile(invitedBy) },
|
||||
)
|
||||
)
|
||||
append(invitedBy)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.common.ui.decorations
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
|
||||
@SuppressLint("ComposePreviewPublic", "ComposeUnstableReceiver")
|
||||
class MonthHeaderTest {
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun DefaultPreview() {
|
||||
LobstersTheme { MonthHeader(label = "April 2023") }
|
||||
}
|
||||
}
|
||||
@@ -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,16 +16,17 @@ android { namespace = "dev.msfjarvis.claw.core" }
|
||||
anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
dependencies {
|
||||
api(libs.dagger)
|
||||
api(libs.javax.inject)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.okhttp.loggingInterceptor)
|
||||
api(libs.napier)
|
||||
api(libs.okhttp.core)
|
||||
api(libs.retrofit)
|
||||
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(platform(libs.sentry.bom))
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.napier)
|
||||
implementation(libs.okhttp.core)
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
implementation(libs.retrofit.kotlinxSerializationConverter)
|
||||
implementation(libs.sentry.okhttp)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 format="6" by="lint 8.4.2" type="baseline" client="gradle" dependencies="true" name="AGP (8.4.2)" variant="all" version="8.4.2">
|
||||
|
||||
</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.
|
||||
@@ -11,10 +11,8 @@ package dev.msfjarvis.claw.core.coroutines
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
||||
/** Interface to allow abstracting individual [CoroutineDispatcher]s out of class dependencies. */
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
interface DispatcherProvider {
|
||||
|
||||
fun main(): CoroutineDispatcher = Dispatchers.Main
|
||||
@@ -23,7 +21,8 @@ interface DispatcherProvider {
|
||||
|
||||
fun io(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
fun database(): CoroutineDispatcher = Dispatchers.IO.limitedParallelism(1)
|
||||
fun database(): CoroutineDispatcher =
|
||||
Dispatchers.IO.limitedParallelism(1, name = "DatabaseDispatcher")
|
||||
}
|
||||
|
||||
/** Concrete type for [DispatcherProvider] with all the defaults from the class. */
|
||||
|
||||
@@ -18,7 +18,6 @@ import dagger.multibindings.IntoSet
|
||||
import dev.msfjarvis.claw.core.network.DelegatingSocketFactory
|
||||
import dev.msfjarvis.claw.core.network.NapierLogger
|
||||
import dev.msfjarvis.claw.core.network.UserAgentInterceptor
|
||||
import io.sentry.okhttp.SentryOkHttpInterceptor
|
||||
import java.net.Socket
|
||||
import javax.net.SocketFactory
|
||||
import okhttp3.Cache
|
||||
@@ -88,13 +87,5 @@ interface OkHttpModule {
|
||||
fun provideHttpLoggingInterceptor(logger: HttpLoggingInterceptor.Logger): Interceptor {
|
||||
return HttpLoggingInterceptor(logger).setLevel(HttpLoggingInterceptor.Level.BASIC)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideSentryInterceptor(): Interceptor {
|
||||
// Disable capturing of failed requests since there is no real upstream for this app,
|
||||
// if something is going wrong it is almost always someone else's problem.
|
||||
return SentryOkHttpInterceptor(captureFailedRequests = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 format="6" by="lint 8.4.2" type="baseline" client="gradle" dependencies="true" name="AGP (8.4.2)" variant="all" version="8.4.2">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -2,13 +2,6 @@ CREATE TABLE IF NOT EXISTS ReadPosts(
|
||||
id TEXT NOT NULL PRIMARY KEY
|
||||
);
|
||||
|
||||
isPostRead:
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM ReadPosts
|
||||
WHERE id = ?
|
||||
) AS isRead;
|
||||
|
||||
selectAllPosts:
|
||||
SELECT *
|
||||
FROM ReadPosts;
|
||||
|
||||
@@ -14,13 +14,6 @@ CREATE TABLE IF NOT EXISTS SavedPost(
|
||||
description TEXT NOT NULL DEFAULT ""
|
||||
);
|
||||
|
||||
isPostSaved:
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM SavedPost
|
||||
WHERE shortId = ?
|
||||
) AS isSaved;
|
||||
|
||||
insertOrReplacePost:
|
||||
INSERT OR REPLACE
|
||||
INTO SavedPost
|
||||
|
||||
@@ -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.
|
||||
@@ -18,6 +18,8 @@ android { namespace = "dev.msfjarvis.claw.database" }
|
||||
anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
dependencies {
|
||||
api(libs.dagger)
|
||||
api(libs.javax.inject)
|
||||
api(projects.database.core)
|
||||
implementation(libs.napier)
|
||||
implementation(libs.dagger)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 format="6" by="lint 8.4.2" type="baseline" client="gradle" dependencies="true" name="AGP (8.4.2)" variant="all" version="8.4.2">
|
||||
|
||||
</issues>
|
||||
|
||||
+5
-2
@@ -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,6 +13,7 @@ import app.cash.sqldelight.logs.LogSqliteDriver
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import com.squareup.anvil.annotations.optional.ForScope
|
||||
import com.squareup.anvil.annotations.optional.SingleIn
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dev.msfjarvis.claw.database.LobstersDatabase
|
||||
@@ -28,7 +29,9 @@ object DatabaseModule {
|
||||
|
||||
private const val LOBSTERS_DATABASE_NAME = "SavedPosts.db"
|
||||
|
||||
@[Provides InternalDatabaseApi]
|
||||
@Provides
|
||||
@InternalDatabaseApi
|
||||
@SingleIn(ApplicationScope::class)
|
||||
fun provideDatabase(@ForScope(ApplicationScope::class) context: Context): LobstersDatabase {
|
||||
val driver =
|
||||
LogSqliteDriver(
|
||||
|
||||
+7
-3
@@ -13,8 +13,8 @@ org.gradle.configuration-cache=true
|
||||
org.gradle.configuration-cache.problems=warn
|
||||
org.gradle.configuration-cache.max-problems=2
|
||||
|
||||
# Set JVM heap size to 4G
|
||||
org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
# Set JVM heap size to 3G
|
||||
org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options=-Xmx1500m,Xms500m
|
||||
|
||||
# Enable Kotlin incremental compilation
|
||||
kotlin.incremental=true
|
||||
@@ -49,7 +49,11 @@ android.dependencyResolutionAtConfigurationTime.disallow=true
|
||||
android.disableEarlyManifestParsing=true
|
||||
|
||||
# Disable warnings about unsupported features
|
||||
android.suppressUnsupportedOptionWarnings=android.dependencyResolutionAtConfigurationTime.disallow,android.disableEarlyManifestParsing,android.suppressUnsupportedOptionWarnings
|
||||
android.suppressUnsupportedOptionWarnings=android.dependencyResolutionAtConfigurationTime.disallow,android.disableEarlyManifestParsing,android.experimental.enableScreenshotTest,android.suppressUnsupportedOptionWarnings
|
||||
android.suppressUnsupportedCompileSdk=35
|
||||
|
||||
# Enable screenshot tests
|
||||
android.experimental.enableScreenshotTest=true
|
||||
|
||||
# Make Spotless default to keeping license headers years in sync
|
||||
spotlessSetLicenseHeaderYearsFromGitHistory=true
|
||||
|
||||
+60
-48
@@ -1,33 +1,36 @@
|
||||
[versions]
|
||||
aboutLibraries = "11.1.0"
|
||||
agp = "8.3.1"
|
||||
benchmark = "1.3.0-alpha01"
|
||||
coil = "2.6.0"
|
||||
# @keep used for kotlinCompilerExtensionVersion
|
||||
composeCompiler = "1.5.11-dev-k1.9.23-96ef9dc6af1"
|
||||
coroutines = "1.8.0"
|
||||
dagger = "2.51"
|
||||
aboutLibraries = "11.2.3"
|
||||
agp = "8.6.1"
|
||||
android-junit5 = "1.11.0.0"
|
||||
benchmark = "1.3.1"
|
||||
coil = "2.7.0"
|
||||
coroutines = "1.9.0"
|
||||
dagger = "2.52"
|
||||
glance = "1.0.0"
|
||||
junit = "5.10.2"
|
||||
konvert = "3.0.1"
|
||||
kotlin = "1.9.23"
|
||||
haze = "0.9.0-beta01"
|
||||
junit = "5.11.0"
|
||||
konvert = "3.2.2"
|
||||
kotlin = "2.0.20"
|
||||
kotlinResult = "2.0.0"
|
||||
lifecycle = "2.7.0"
|
||||
retrofit = "2.10.0"
|
||||
leakcanary = "3.0-alpha-8"
|
||||
lifecycle = "2.8.6"
|
||||
navigation = "2.8.1"
|
||||
retrofit = "2.11.0"
|
||||
richtext = "1.0.0-alpha01"
|
||||
sentry-sdk = "7.6.0"
|
||||
serialization = "1.6.3"
|
||||
sqldelight = "2.0.1"
|
||||
whetstone = "0.9.0-beta01"
|
||||
workmanager = "2.10.0-alpha01"
|
||||
sentry-sdk = "7.14.0"
|
||||
serialization = "1.7.3"
|
||||
store = "5.1.0-alpha04"
|
||||
sqldelight = "2.0.2"
|
||||
whetstone = "0.9.0-beta02"
|
||||
workmanager = "2.10.0-alpha04"
|
||||
|
||||
[libraries]
|
||||
aboutLibraries-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" }
|
||||
androidx-activity-compose = "androidx.activity:activity-compose:1.9.0-alpha03"
|
||||
androidx-activity-compose = "androidx.activity:activity-compose:1.9.2"
|
||||
androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmark" }
|
||||
androidx-browser = "androidx.browser:browser:1.8.0"
|
||||
androidx-compose-animation = { module = "androidx.compose.animation:animation" }
|
||||
androidx-compose-bom = "dev.chrisbanes.compose:compose-bom:2024.03.00-alpha01"
|
||||
androidx-compose-bom = "androidx.compose:compose-bom:2024.09.00-alpha"
|
||||
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" }
|
||||
androidx-compose-glance = { module = "androidx.glance:glance-appwidget", version.ref = "glance" }
|
||||
androidx-compose-glance-m3 = { module = "androidx.glance:glance-material3", version.ref = "glance" }
|
||||
@@ -39,28 +42,28 @@ androidx-compose-ui = { module = "androidx.compose.ui:ui" }
|
||||
androidx-compose-ui-text = { module = "androidx.compose.ui:ui-text" }
|
||||
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
androidx-compose-ui-unit = { module = "androidx.compose.ui:ui-unit" }
|
||||
androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util" }
|
||||
androidx-core-splashscreen = "androidx.core:core-splashscreen:1.1.0-alpha02"
|
||||
androidx-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycle" }
|
||||
androidx-core = "androidx.core:core:1.13.1"
|
||||
androidx-core-splashscreen = "androidx.core:core-splashscreen:1.2.0-alpha02"
|
||||
androidx-lifecycle-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" }
|
||||
androidx-lint = "androidx.lint:lint-gradle:1.0.0-alpha01"
|
||||
androidx-navigation-compose = "androidx.navigation:navigation-compose:2.8.0-alpha04"
|
||||
androidx-paging-compose = "androidx.paging:paging-compose:3.3.0-alpha04"
|
||||
androidx-profileinstaller = "androidx.profileinstaller:profileinstaller:1.4.0-alpha01"
|
||||
androidx-test-core = "androidx.test:core:1.6.0-alpha05"
|
||||
androidx-test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha03"
|
||||
androidx-test-ext-junit = "androidx.test.ext:junit:1.2.0-alpha03"
|
||||
androidx-test-rules = "androidx.test:rules:1.6.0-alpha03"
|
||||
androidx-test-runner = "androidx.test:runner:1.6.0-alpha06"
|
||||
androidx-lint = "androidx.lint:lint-gradle:1.0.0-alpha02"
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
|
||||
androidx-paging-compose = "androidx.paging:paging-compose:3.3.2"
|
||||
androidx-profileinstaller = "androidx.profileinstaller:profileinstaller:1.4.0"
|
||||
androidx-test-core = "androidx.test:core:1.6.1"
|
||||
androidx-test-espresso-core = "androidx.test.espresso:espresso-core:3.6.1"
|
||||
androidx-test-ext-junit = "androidx.test.ext:junit:1.2.1"
|
||||
androidx-test-rules = "androidx.test:rules:1.6.1"
|
||||
androidx-test-runner = "androidx.test:runner:1.6.2"
|
||||
androidx-test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0"
|
||||
androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "workmanager" }
|
||||
build-agp = { module = "com.android.tools.build:gradle", version.ref = "agp" }
|
||||
build-cachefix = "org.gradle.android.cache-fix:org.gradle.android.cache-fix.gradle.plugin:3.0.1"
|
||||
build-kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
build-semver = "com.github.zafarkhaja:java-semver:0.10.2"
|
||||
build-sentry = "io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin:4.3.1"
|
||||
build-spotless = "com.diffplug.spotless:spotless-plugin-gradle:6.25.0"
|
||||
build-sentry = "io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin:4.11.0"
|
||||
build-spotless = "com.diffplug.spotless:spotless-plugin-gradle:7.0.0.BETA2"
|
||||
build-vcu = "nl.littlerobots.version-catalog-update:nl.littlerobots.version-catalog-update.gradle.plugin:0.8.4"
|
||||
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||
@@ -70,55 +73,64 @@ compose-richtext-ui = { module = "com.halilibo.compose-richtext:richtext-ui", ve
|
||||
copydown = "io.github.furstenheim:copy_down:1.1"
|
||||
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
|
||||
dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
|
||||
eithernet = "com.slack.eithernet:eithernet:1.8.1"
|
||||
eithernet = "com.slack.eithernet:eithernet:1.9.0"
|
||||
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
|
||||
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
|
||||
javax-inject = "javax.inject:javax.inject:1"
|
||||
jsoup = "org.jsoup:jsoup:1.17.2"
|
||||
jsoup = "org.jsoup:jsoup:1.18.1"
|
||||
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
|
||||
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
|
||||
junit-legacy = "junit:junit:4.13.2"
|
||||
konvert-annotations = { module = "io.mcarle:konvert-annotations", version.ref = "konvert" }
|
||||
konvert-api = { module = "io.mcarle:konvert-api", version.ref = "konvert" }
|
||||
konvert-processor = { module = "io.mcarle:konvert", version.ref = "konvert" }
|
||||
kotlinResult = { module = "com.michael-bull.kotlin-result:kotlin-result", version.ref = "kotlinResult" }
|
||||
kotlinResult-coroutines = { module = "com.michael-bull.kotlin-result:kotlin-result-coroutines", version.ref = "kotlinResult" }
|
||||
kotlinx-collections-immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7"
|
||||
kotlinx-collections-immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8"
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
|
||||
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
|
||||
material3-pulltorefresh = "eu.bambooapps:compose-material3-pullrefresh:1.1.1"
|
||||
leakcanary-android-test = { module = "com.squareup.leakcanary:leakcanary-android-test", version.ref = "leakcanary" }
|
||||
napier = "io.github.aakira:napier:2.7.1"
|
||||
okhttp-bom = "com.squareup.okhttp3:okhttp-bom:4.12.0"
|
||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp" }
|
||||
okhttp-loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor" }
|
||||
okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver" }
|
||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
retrofit-kotlinxSerializationConverter = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" }
|
||||
sentry-android = { module = "io.sentry:sentry-android", version.ref = "sentry-sdk" }
|
||||
sentry-bom = { module = "io.sentry:sentry-bom", version.ref = "sentry-sdk" }
|
||||
sentry-okhttp = { module = "io.sentry:sentry-okhttp", version.ref = "sentry-sdk" }
|
||||
slack-compose-lints = "com.slack.lint.compose:compose-lint-checks:1.3.1"
|
||||
slack-lints = "com.slack.lint:slack-lint-checks:0.7.0"
|
||||
slack-lints = "com.slack.lint:slack-lint-checks:0.7.4"
|
||||
sqldelight-androidDriver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||
sqldelight-dialect338 = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" }
|
||||
sqldelight-extensions-coroutines = { module = "app.cash.sqldelight:coroutines-extensions-jvm", version.ref = "sqldelight" }
|
||||
sqldelight-jvmDriver = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" }
|
||||
sqldelight-primitiveAdapters = { module = "app.cash.sqldelight:primitive-adapters", version.ref = "sqldelight" }
|
||||
sqlite-android = "com.github.requery:sqlite-android:3.45.0"
|
||||
store = { module = "org.mobilenativefoundation.store:store5", version.ref = "store" }
|
||||
swipe = "me.saket.swipe:swipe:1.3.0"
|
||||
truth = "com.google.truth:truth:1.4.2"
|
||||
unfurl = "me.saket.unfurl:unfurl:1.7.0"
|
||||
truth = "com.google.truth:truth:1.4.4"
|
||||
unfurl = "me.saket.unfurl:unfurl:2.2.0"
|
||||
|
||||
[plugins]
|
||||
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibraries" }
|
||||
android-lint = { id = "com.android.lint", version.ref = "agp" }
|
||||
android-junit5 = { id = "de.mannodermaus.android-junit5", version.ref = "android-junit5" }
|
||||
android-test = { id = "com.android.test", version.ref = "agp" }
|
||||
anvil = "com.squareup.anvil:2.5.0-beta04"
|
||||
anvil = "com.squareup.anvil:2.5.0-beta11"
|
||||
baselineprofile = { id = "androidx.baselineprofile", version.ref = "benchmark" }
|
||||
dependencyAnalysis = "com.autonomousapps.dependency-analysis:2.0.2"
|
||||
invert = "com.squareup.invert:0.0.1-dev"
|
||||
kotlin-composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
ksp = "com.google.devtools.ksp:1.9.23-1.0.19"
|
||||
licensee = "app.cash.licensee:1.9.1"
|
||||
modulegraphassert = "com.jraska.module.graph.assertion:2.5.0"
|
||||
poko = "dev.drewhamilton.poko:0.15.2"
|
||||
ksp = "com.google.devtools.ksp:2.0.20-1.0.25"
|
||||
licensee = "app.cash.licensee:1.11.0"
|
||||
modulegraphassert = "com.jraska.module.graph.assertion:2.7.1"
|
||||
navigation-safeargs = { id = "androidx.navigation.safeargs.kotlin", version.ref = "navigation" }
|
||||
poko = "dev.drewhamilton.poko:0.17.1"
|
||||
screenshot = "com.android.compose.screenshot:0.0.1-alpha07"
|
||||
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
|
||||
tracelog = "dev.msfjarvis.tracelog:0.1.3"
|
||||
whetstone = { id = "dev.msfjarvis.whetstone", version.ref = "whetstone" }
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-2
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
distributionSha256Sum=1541fa36599e12857140465f3c91a97409b4512501c26f9631fb113e392c5bd1
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -84,7 +86,8 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
Vendored
+2
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
|
||||
@@ -12,12 +12,14 @@ appId: dev.msfjarvis.claw.android
|
||||
- waitForAnimationToEnd
|
||||
- takeScreenshot: "SavedPosts"
|
||||
- tapOn: "Search posts"
|
||||
- inputText: "Nix"
|
||||
- inputText: "Rust"
|
||||
- pressKey: Enter
|
||||
- hideKeyboard
|
||||
- waitForAnimationToEnd
|
||||
- takeScreenshot: "SearchPage"
|
||||
- back
|
||||
# TODO: figure out why do I need to press back twice
|
||||
- back
|
||||
- tapOn: "Settings"
|
||||
- tapOn: "Export"
|
||||
- takeScreenshot: "SettingsPage"
|
||||
|
||||
@@ -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.
|
||||
@@ -12,9 +12,10 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
compileOnly(libs.konvert.annotations)
|
||||
api(projects.database.core)
|
||||
implementation(libs.konvert.api)
|
||||
implementation(projects.database.core)
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
|
||||
ksp(libs.konvert.processor)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 format="6" by="lint 8.4.2" type="baseline" client="gradle" dependencies="true" name="AGP (8.4.2)" variant="all" version="8.4.2">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -29,5 +29,6 @@ class LobstersPost(
|
||||
val commentCount: Int,
|
||||
val commentsUrl: String,
|
||||
@SerialName("submitter_user") val submitter: String,
|
||||
@SerialName("user_is_author") val userIsAuthor: Boolean,
|
||||
val tags: List<String>,
|
||||
)
|
||||
|
||||
@@ -27,8 +27,7 @@ data class UIPost(
|
||||
@SerialName("submitter_user") val submitter: String,
|
||||
val tags: List<String>,
|
||||
val comments: List<Comment> = emptyList(),
|
||||
val isSaved: Boolean = false,
|
||||
val isRead: Boolean = false,
|
||||
val userIsAuthor: Boolean = false,
|
||||
) {
|
||||
@KonvertFrom(
|
||||
value = SavedPost::class,
|
||||
@@ -36,7 +35,6 @@ data class UIPost(
|
||||
[
|
||||
Mapping(target = "submitter", expression = "it.submitterName"),
|
||||
Mapping(target = "commentCount", expression = "it.commentCount ?: 0"),
|
||||
Mapping(target = "isSaved", expression = "true"),
|
||||
],
|
||||
)
|
||||
companion object
|
||||
|
||||
+19
-10
@@ -14,8 +14,9 @@ pluginManagement {
|
||||
includeGroup("androidx.baselineprofile")
|
||||
includeGroup("androidx.benchmark")
|
||||
includeGroup("androidx.databinding")
|
||||
includeGroupByRegex("com.android.*")
|
||||
includeGroupAndSubgroups("androidx.navigation")
|
||||
includeGroup("com.google.testing.platform")
|
||||
includeGroupAndSubgroups("com.android")
|
||||
}
|
||||
}
|
||||
exclusiveContent {
|
||||
@@ -35,8 +36,8 @@ pluginManagement {
|
||||
"com.jraska.module.graph.assertion",
|
||||
"com.jraska.module.graph.assertion.gradle.plugin",
|
||||
)
|
||||
includeModule("com.gradle", "gradle-enterprise-gradle-plugin")
|
||||
includeModule("com.gradle.enterprise", "com.gradle.enterprise.gradle.plugin")
|
||||
includeModule("com.gradle", "develocity-gradle-plugin")
|
||||
includeModule("com.gradle.develocity", "com.gradle.develocity.gradle.plugin")
|
||||
includeModule("com.jraska.module.graph.assertion", "plugin")
|
||||
includeModule(
|
||||
"org.gradle.toolchains.foojay-resolver-convention",
|
||||
@@ -49,6 +50,12 @@ pluginManagement {
|
||||
maven("https://oss.sonatype.org/content/repositories/snapshots/") {
|
||||
name = "Sonatype Snapshots"
|
||||
content { includeGroup("dev.msfjarvis.whetstone") }
|
||||
content { includeGroup("dev.drewhamilton.poko") }
|
||||
mavenContent { snapshotsOnly() }
|
||||
}
|
||||
maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") {
|
||||
name = "Sonatype S01 Snapshots"
|
||||
content { includeGroup("com.squareup.invert") }
|
||||
mavenContent { snapshotsOnly() }
|
||||
}
|
||||
mavenCentral { mavenContent { releasesOnly() } }
|
||||
@@ -57,14 +64,14 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
||||
id("com.gradle.enterprise") version "3.16.2"
|
||||
id("com.gradle.develocity") version "3.18.1"
|
||||
}
|
||||
|
||||
gradleEnterprise {
|
||||
develocity {
|
||||
buildScan {
|
||||
termsOfServiceUrl = "https://gradle.com/terms-of-service"
|
||||
termsOfServiceAgree = if (System.getenv("GITHUB_WORKFLOW").isNullOrEmpty()) "no" else "yes"
|
||||
publishOnFailureIf(!System.getenv("GITHUB_WORKFLOW").isNullOrEmpty())
|
||||
termsOfUseUrl = "https://gradle.com/help/legal-terms-of-use"
|
||||
termsOfUseAgree = if (System.getenv("GITHUB_WORKFLOW").isNullOrEmpty()) "no" else "yes"
|
||||
publishing.onlyIf { !System.getenv("GITHUB_WORKFLOW").isNullOrEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +80,8 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("androidx.*")
|
||||
includeGroupByRegex("com.android.*")
|
||||
includeGroupAndSubgroups("androidx")
|
||||
includeGroupAndSubgroups("com.android")
|
||||
includeGroup("com.google.android.gms")
|
||||
includeModule("com.google.android.material", "material")
|
||||
includeGroup("com.google.testing.platform")
|
||||
@@ -91,6 +98,7 @@ dependencyResolutionManagement {
|
||||
maven("https://oss.sonatype.org/content/repositories/snapshots/") {
|
||||
name = "Sonatype Snapshots"
|
||||
content { includeGroup("dev.msfjarvis.whetstone") }
|
||||
content { includeGroup("dev.drewhamilton.poko") }
|
||||
mavenContent { snapshotsOnly() }
|
||||
}
|
||||
mavenCentral { mavenContent { releasesOnly() } }
|
||||
@@ -110,5 +118,6 @@ include(
|
||||
"database:core",
|
||||
"database:impl",
|
||||
"model",
|
||||
"store",
|
||||
"web",
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
import dev.msfjarvis.claw.gradle.addTestDependencies
|
||||
|
||||
plugins {
|
||||
id("dev.msfjarvis.claw.android-library")
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.whetstone)
|
||||
}
|
||||
|
||||
android { namespace = "dev.msfjarvis.claw.data.store" }
|
||||
|
||||
anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
dependencies {
|
||||
implementation(libs.store)
|
||||
|
||||
addTestDependencies(project)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.4.2" type="baseline" client="gradle" dependencies="true" name="AGP (8.4.2)" variant="all" version="8.4.2">
|
||||
|
||||
</issues>
|
||||
@@ -9,23 +9,26 @@
|
||||
plugins {
|
||||
id("dev.msfjarvis.claw.android-library")
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
alias(libs.plugins.kotlin.composeCompiler)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.google.accompanist.web"
|
||||
buildFeatures.compose = true
|
||||
composeOptions.kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
kotlin.explicitApi()
|
||||
// Don't quite care
|
||||
lint.disable += "DeprecatedCall"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core)
|
||||
api(libs.androidx.compose.runtime)
|
||||
api(libs.androidx.compose.ui)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
api(projects.core)
|
||||
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.ui.util)
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.foundation)
|
||||
implementation(libs.androidx.compose.ui.unit)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 format="6" by="lint 8.4.2" type="baseline" client="gradle" dependencies="true" name="AGP (8.4.2)" variant="all" version="8.4.2">
|
||||
|
||||
</issues>
|
||||
|
||||
Reference in New Issue
Block a user