Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffe63bbce0 | ||
|
|
75d8a2dd2b | ||
|
|
2c0c97d6cc | ||
|
|
61307bdf03 | ||
|
|
2ec8fb03aa | ||
|
|
f8611efdc7 | ||
|
|
ae06a768fe | ||
|
|
feed18c33e | ||
|
|
8340228637 | ||
|
|
c5117e8669 | ||
|
|
9fb8e17cf6 | ||
|
|
d5a1413799 | ||
|
|
a7f6658b85 | ||
|
|
1885859d2f | ||
|
|
988cf117c2 | ||
|
|
d4a6009b92 | ||
|
|
a187752659 | ||
|
|
050007ff1b | ||
|
|
1441804e70 | ||
|
|
990c293ac2 | ||
|
|
de1bda0034 | ||
|
|
949df2c71f | ||
|
|
012376d2aa | ||
|
|
88224b765a | ||
|
|
3cfa48835c | ||
|
|
54a6c9767c | ||
|
|
05c7a32532 | ||
|
|
c5a3562608 | ||
|
|
a23037ce70 | ||
|
|
f4469c8393 | ||
|
|
d27474e910 | ||
|
|
7399dc9939 | ||
|
|
352f028659 | ||
|
|
966749d172 | ||
|
|
653dbf736e | ||
|
|
80be2aac8d | ||
|
|
6c3d21a5f4 | ||
|
|
c8f5ec5120 | ||
|
|
e171154a79 | ||
|
|
5217eb411d | ||
|
|
3112dc7c95 | ||
|
|
3e23ef40aa | ||
|
|
1b1aaf1032 | ||
|
|
39707842b1 | ||
|
|
2a0ec3d47e | ||
|
|
3c2022315e | ||
|
|
9a63ff2809 | ||
|
|
ee3b0edcf5 | ||
|
|
940ff167bf | ||
|
|
9a300aaf21 | ||
|
|
4924e3eebe | ||
|
|
a49c7136cd | ||
|
|
bc433bde83 | ||
|
|
bc19205e0a | ||
|
|
10f802c4aa | ||
|
|
b5b8a864cd | ||
|
|
11be868d43 | ||
|
|
0c63920cdf | ||
|
|
882c2a36b3 | ||
|
|
8270d5c05d | ||
|
|
9f6c7a3e7b | ||
|
|
55231eaf08 | ||
|
|
1af4c237ad | ||
|
|
b5d2636cbf | ||
|
|
a4d9bf87ef | ||
|
|
697eb1f639 | ||
|
|
0c9b823273 | ||
|
|
06d45d6b29 | ||
|
|
37869e52b6 | ||
|
|
6169311561 | ||
|
|
3e58d46c98 | ||
|
|
48601a6508 | ||
|
|
5fc4d8495e | ||
|
|
3f45d588e3 | ||
|
|
7bfc16ae22 | ||
|
|
1d31c10625 | ||
|
|
00af2995e0 | ||
|
|
fd042460ee | ||
|
|
c3810de709 | ||
|
|
b39d705ea5 | ||
|
|
91a13a400c | ||
|
|
b9003f8280 | ||
|
|
c91315ea01 | ||
|
|
03f8f7d64f | ||
|
|
3a43996939 | ||
|
|
2b12ab1501 | ||
|
|
6221419af6 | ||
|
|
05735ef769 | ||
|
|
8e47906bbe | ||
|
|
53f0656353 | ||
|
|
8e50cb3e77 | ||
|
|
35fc839573 | ||
|
|
9039a2de58 | ||
|
|
8263d1c34e | ||
|
|
338ad07db7 | ||
|
|
311f7c0574 | ||
|
|
ca91918707 | ||
|
|
5bcfe0a85c | ||
|
|
6084e5bb30 | ||
|
|
1d5302f26c | ||
|
|
1221daa780 | ||
|
|
b7d79fcb5f | ||
|
|
b466579c22 | ||
|
|
ed8353664b | ||
|
|
25609d9fd4 | ||
|
|
cf3859b591 | ||
|
|
8be80fde71 | ||
|
|
336de41849 | ||
|
|
306236b6e3 | ||
|
|
416ef47fb1 | ||
|
|
c9ff083c0a | ||
|
|
66c22aa3b8 | ||
|
|
b2a942c179 | ||
|
|
e9cbe3cdcb | ||
|
|
0000db67c4 | ||
|
|
89827405d7 | ||
|
|
e6914ac018 | ||
|
|
e162120943 | ||
|
|
333fb2aa7e | ||
|
|
84ee08e75c | ||
|
|
f682d12bd0 | ||
|
|
10adef15a8 | ||
|
|
fc769349d5 | ||
|
|
5152a9054b | ||
|
|
783d830c68 | ||
|
|
c25dfabdbf | ||
|
|
c7e94db3d2 | ||
|
|
2f0be7a8a6 | ||
|
|
22e55b142c | ||
|
|
6dda2f5917 | ||
|
|
838c86d1f8 | ||
|
|
a1a9e712b8 | ||
|
|
35469825c7 | ||
|
|
f565bc41d7 | ||
|
|
9446f09f22 | ||
|
|
925158b149 | ||
|
|
6f1c1b3629 | ||
|
|
cb6e71c8bf | ||
|
|
17258ea4d7 | ||
|
|
035d33fcea | ||
|
|
de46f1b159 | ||
|
|
4b4d133e8b | ||
|
|
5a1cdc73c5 | ||
|
|
c5fe4f5325 | ||
|
|
e2fa605db6 | ||
|
|
55580bc51c | ||
|
|
fe78f97f6a | ||
|
|
9d46e6f460 | ||
|
|
21f9f6f476 | ||
|
|
2ba8eb9430 | ||
|
|
6ea194dd0e | ||
|
|
1d3d4f7455 | ||
|
|
fdb79d4a6d | ||
|
|
89fce2010c | ||
|
|
fb878824e8 | ||
|
|
aff5af4771 | ||
|
|
dd06c93935 | ||
|
|
d9e664c440 | ||
|
|
0a492a169e | ||
|
|
6ae71adbca | ||
|
|
cbaa098005 | ||
|
|
741e82a425 | ||
|
|
c694b57e08 | ||
|
|
2c7bc2d668 | ||
|
|
765f83a2ae | ||
|
|
58f6607404 | ||
|
|
9b27783886 | ||
|
|
c6de087615 | ||
|
|
86f7b54b87 | ||
|
|
defe305ad7 | ||
|
|
cc210a3597 | ||
|
|
f7c7bb293b | ||
|
|
ab1db27dc7 | ||
|
|
71a86fa9aa | ||
|
|
fcc20d2f1e | ||
|
|
8922142ade | ||
|
|
eb28fe0120 | ||
|
|
d73a755cf2 | ||
|
|
7a648a77b0 | ||
|
|
c721b8b1da | ||
|
|
ce62671276 | ||
|
|
ba4cd1b0c6 | ||
|
|
a18ee3d6e5 | ||
|
|
15fa833d8e | ||
|
|
06edbeb903 | ||
|
|
082dc9cc82 | ||
|
|
0b3286cd1e | ||
|
|
f17fb4487e | ||
|
|
339162d595 | ||
|
|
da6fefb1b0 | ||
|
|
8d3ed24d65 | ||
|
|
ba219af1ac | ||
|
|
4218beac9d | ||
|
|
d5d71c1c82 | ||
|
|
348b3a7c65 | ||
|
|
f636b56ee3 | ||
|
|
ed81ceaa94 | ||
|
|
7a78c77b6a | ||
|
|
337cd259a6 | ||
|
|
e7781517aa | ||
|
|
ce3411e956 | ||
|
|
a58ca2c419 | ||
|
|
608d855cae | ||
|
|
eb529be286 | ||
|
|
68dbe97442 | ||
|
|
5374410c67 | ||
|
|
cbb697216c | ||
|
|
25a11bde59 | ||
|
|
62bffc33fd | ||
|
|
45f384d048 | ||
|
|
b9d92ecefa | ||
|
|
01fd8bee0c | ||
|
|
9048d253e6 | ||
|
|
16b660ad0e | ||
|
|
2273768556 | ||
|
|
ab01554b82 | ||
|
|
cce12fb138 | ||
|
|
6f4c1a0fbd | ||
|
|
3663ca1ec1 | ||
|
|
f39bf0b043 | ||
|
|
f55d89b17e | ||
|
|
f6185a3dc5 | ||
|
|
be7bd38341 | ||
|
|
b3277e2367 | ||
|
|
f560bb6fbf | ||
|
|
013138afb8 | ||
|
|
d6d639d059 | ||
|
|
31b2d66b9b | ||
|
|
4b278d68e6 | ||
|
|
e81687bf53 | ||
|
|
a0ea6d6e1c | ||
|
|
17eb9a4c45 | ||
|
|
2e8ad58979 | ||
|
|
680d43f78c | ||
|
|
0d1fb26b52 | ||
|
|
7540430e1d | ||
|
|
095ec1ed04 | ||
|
|
ce62a7b1fc | ||
|
|
4b89775f8a | ||
|
|
a5af23e330 | ||
|
|
46cc0910bf | ||
|
|
37e0fdb0a7 | ||
|
|
a3e7e76931 | ||
|
|
cfdab8ddcd | ||
|
|
389924a544 | ||
|
|
b3634a0762 | ||
|
|
985bc1361a | ||
|
|
d3d3a6af09 | ||
|
|
3883160234 | ||
|
|
d04ad16aee | ||
|
|
0067418ffb | ||
|
|
deb900f8f8 | ||
|
|
e6f6ed75f1 | ||
|
|
6226c3b0c8 | ||
|
|
1a525d6e5d | ||
|
|
bc213fe3c9 | ||
|
|
731666936c | ||
|
|
67ec7e92b5 | ||
|
|
40bfcb07b6 | ||
|
|
e5f09a7aef | ||
|
|
341bdb0eda | ||
|
|
27d68f2774 | ||
|
|
d259cd8eb5 | ||
|
|
c9e5ded43a | ||
|
|
a8e03e22bf | ||
|
|
7094016e7e | ||
|
|
1bfa1f3981 | ||
|
|
655745e049 | ||
|
|
f83a22b310 | ||
|
|
025c24a9d9 | ||
|
|
c8c3e83e7b | ||
|
|
7d8327af18 | ||
|
|
58bab2f538 | ||
|
|
cf93eda6c1 | ||
|
|
a3b7af0f79 | ||
|
|
91bf5e5555 | ||
|
|
61a72b330d | ||
|
|
6a1164363c | ||
|
|
df9a82482c | ||
|
|
778d98c7f2 | ||
|
|
cae759a09e | ||
|
|
52126b2248 | ||
|
|
5782521824 | ||
|
|
3522dc529d | ||
|
|
8602e68b60 | ||
|
|
fb4514d70e | ||
|
|
35d86bff11 | ||
|
|
193730e16c | ||
|
|
ed32186984 | ||
|
|
eb449740eb | ||
|
|
327fd43dd6 | ||
|
|
bc2345f415 | ||
|
|
fd1bdc79f8 | ||
|
|
22a2419185 | ||
|
|
de921bf786 | ||
|
|
3cf6b22d95 | ||
|
|
82ce45b8e4 | ||
|
|
dfe95a646e | ||
|
|
7b0b206905 | ||
|
|
1b1984064c | ||
|
|
7e03e2571f | ||
|
|
23f0b32634 | ||
|
|
f844062980 | ||
|
|
84f982b20e | ||
|
|
c6739487a4 | ||
|
|
be0f46371e | ||
|
|
85351d5b1c | ||
|
|
23366e4536 | ||
|
|
0d1f69d36e | ||
|
|
d77db45f37 | ||
|
|
20c5b6c4a5 | ||
|
|
62f512e319 | ||
|
|
e740ddb7cc | ||
|
|
68924d6f35 | ||
|
|
94db512518 | ||
|
|
f39e9889f3 | ||
|
|
b0ffbcd07d | ||
|
|
19412cc963 | ||
|
|
45c7dbb3f7 | ||
|
|
3b0d0e55dc | ||
|
|
ae21ebe80d | ||
|
|
cca14765ce | ||
|
|
725718d18c | ||
|
|
61b000dc48 | ||
|
|
cf850071e5 | ||
|
|
fe473e70a3 | ||
|
|
ce6540a6f3 | ||
|
|
ac3e60053a | ||
|
|
205ae8d9c7 | ||
|
|
772d368caa | ||
|
|
d2fa330491 | ||
|
|
f85856a6d9 | ||
|
|
4ed5988aa8 | ||
|
|
ac4bf7f4dd | ||
|
|
7b5cfa97d7 | ||
|
|
93292c3d5a | ||
|
|
62ebb43e6f | ||
|
|
5dabebf579 | ||
|
|
96728a3834 | ||
|
|
10aa416e94 | ||
|
|
e283584999 | ||
|
|
edea0c6ab1 | ||
|
|
1cb3eb6472 | ||
|
|
ddfa62f4fb | ||
|
|
88a119d66b | ||
|
|
de16ff52af | ||
|
|
8dc3a034c1 | ||
|
|
f9b2581464 | ||
|
|
2f2d2e694e | ||
|
|
25382acf0e | ||
|
|
d4672ae8bd | ||
|
|
4c87b644c0 | ||
|
|
f546f98227 | ||
|
|
e878df3d60 | ||
|
|
e49a2c2cbc | ||
|
|
f371c435fd | ||
|
|
19e7d65006 | ||
|
|
60506b4725 | ||
|
|
9cfa4d8ac4 | ||
|
|
8195a15a01 | ||
|
|
721b74fedc | ||
|
|
62ea7cd456 | ||
|
|
b60adaf87d | ||
|
|
0852f197be | ||
|
|
ef6359274d | ||
|
|
b31a3755bb | ||
|
|
c87f6a4239 | ||
|
|
c279765386 | ||
|
|
ac3dd45ee6 | ||
|
|
6db7880178 | ||
|
|
0c29a6626d | ||
|
|
183e1606c5 | ||
|
|
d5a4d7f91a | ||
|
|
5a491429db | ||
|
|
6265c50caf | ||
|
|
4b4f00ad8e | ||
|
|
ed071a0fff | ||
|
|
db393d4534 | ||
|
|
9dcf26b5a4 | ||
|
|
43402f45d8 | ||
|
|
76b5395fd2 | ||
|
|
f9fa30a58d | ||
|
|
b7660a93b5 | ||
|
|
5858466318 | ||
|
|
4d5ce37982 | ||
|
|
d84e6c8e7c | ||
|
|
9768ffa8ff | ||
|
|
5cb4af40cd | ||
|
|
b43d238dd1 | ||
|
|
3c8a23e91e | ||
|
|
63d74ae065 | ||
|
|
f0d4395da6 | ||
|
|
41fe784641 | ||
|
|
7c07e90d0e | ||
|
|
6731453c41 | ||
|
|
932b474763 | ||
|
|
1600b7a200 | ||
|
|
0d23225ec7 | ||
|
|
5214355339 | ||
|
|
81b96cf21d | ||
|
|
492eff8cde | ||
|
|
49c5029256 | ||
|
|
fcd85dc8e1 | ||
|
|
bc3a1266f4 | ||
|
|
bc2365b08b | ||
|
|
9c32d6721b | ||
|
|
1394bc74c4 | ||
|
|
cbf0707aa6 | ||
|
|
67e621160f | ||
|
|
e06a8ba236 | ||
|
|
5bd37a837c | ||
|
|
828fb74240 | ||
|
|
8241a6d901 | ||
|
|
edadcc6744 | ||
|
|
44438da292 | ||
|
|
591c68a6e4 | ||
|
|
892d68581d | ||
|
|
a4b39ce00e | ||
|
|
9ce0d3a8b4 | ||
|
|
23b07c4bad | ||
|
|
f3e28d2e58 | ||
|
|
7a5da2af9f | ||
|
|
4c28da30ae | ||
|
|
4961af8a20 | ||
|
|
1e396bc7ff | ||
|
|
52deb7ca5f | ||
|
|
8891667c92 | ||
|
|
e7e51f38d4 | ||
|
|
3de6a94ea0 | ||
|
|
ef0db7d5ae | ||
|
|
45701f414b | ||
|
|
14007c6e8f | ||
|
|
6ca500066f | ||
|
|
69f63cced4 | ||
|
|
1e0cc062d5 | ||
|
|
f6e9b3a83f | ||
|
|
e0eb748318 | ||
|
|
d65b8b6799 | ||
|
|
d41a1f444f | ||
|
|
786f674044 | ||
|
|
432a438815 | ||
|
|
bae671ad23 | ||
|
|
45b6f3c4d5 | ||
|
|
79a2a060c7 | ||
|
|
80736d6538 | ||
|
|
cb77a3ae85 | ||
|
|
80ab034493 | ||
|
|
3bb5dfffbe | ||
|
|
d9ab751dbd | ||
|
|
e3627ca89d | ||
|
|
543edf36e8 | ||
|
|
fb5721beda | ||
|
|
6e67a3de5f | ||
|
|
6d944967f0 | ||
|
|
2628bfc2cd | ||
|
|
d77fa13903 | ||
|
|
f71aa07949 | ||
|
|
a41b465648 | ||
|
|
a7038600b3 | ||
|
|
3143556c63 | ||
|
|
5e2d9a2355 | ||
|
|
b4adfede90 | ||
|
|
ed85ba905d | ||
|
|
c29e4065d6 | ||
|
|
bcf4f02a01 | ||
|
|
72b9409383 | ||
|
|
2c762428af | ||
|
|
58e427c13a | ||
|
|
974f03ee89 | ||
|
|
f5e6b25ec6 | ||
|
|
be5f073acf | ||
|
|
ca6b941cbb | ||
|
|
1e5f830652 | ||
|
|
e72a5fda5c | ||
|
|
5c2fe6e630 | ||
|
|
5907c2a09a | ||
|
|
e3b381c665 | ||
|
|
f1d30e59b0 | ||
|
|
7669a85f43 | ||
|
|
c05db765c8 |
@@ -6,3 +6,6 @@ cbdfa67d075ec1f8ceec0f3559498cbca01783a0
|
||||
|
||||
# Renamed Renovate config file
|
||||
f304304bc5b64edf189aaa473b05df912067a8bd
|
||||
|
||||
# Refactored database module into core and impl
|
||||
7b0b206905b8dc1f824a2d2a10e3d94c6b9dd11c
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
**/dependencies/** linguist-generated
|
||||
**/clawicons/** linguist-generated
|
||||
android/src/main/baseline-prof.txt linguist-generated -diff
|
||||
android/src/main/generated/** linguist-generated -diff
|
||||
|
||||
|
Before Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 261 KiB |
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"androidx.compose.compiler:compiler",
|
||||
"com.squareup.anvil",
|
||||
"com.google.devtools.ksp",
|
||||
"dev.drewhamilton.poko",
|
||||
"org.jetbrains.kotlin:kotlin-gradle-plugin",
|
||||
"org.jetbrains.kotlin.plugin.serialization",
|
||||
@@ -33,20 +33,22 @@
|
||||
"matchPackagePatterns": [
|
||||
"^androidx.*",
|
||||
],
|
||||
"groupName": "androidx"
|
||||
"matchPackageNames": [
|
||||
],
|
||||
"groupName": "AndroidX"
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^io.sentry",
|
||||
"^sentry",
|
||||
],
|
||||
"groupName": "sentry"
|
||||
"groupName": "Sentry"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"com.android.tools:r8",
|
||||
"matchPackagePatterns": [
|
||||
"^com.squareup.anvil"
|
||||
],
|
||||
"enabled": false,
|
||||
"allowedVersions": "!/.*-1-8$/",
|
||||
},
|
||||
],
|
||||
"regexManagers": [
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
name: Baseline profile generation
|
||||
|
||||
on:
|
||||
# every day at 00:43
|
||||
schedule:
|
||||
- cron: '43 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
baseline-profile:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
TERM: dumb
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Set up Git author
|
||||
shell: bash
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email noreply@github.com
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$ENCRYPT_KEY"
|
||||
env:
|
||||
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
|
||||
|
||||
# This allows us to build most of what we need without the emulator running
|
||||
# and using resources
|
||||
- name: Build app and benchmark
|
||||
run: ./gradlew :benchmark:assembleBenchmark :android:assembleBenchmark
|
||||
|
||||
- name: Clear unused Gradle Managed Devices
|
||||
run: ./gradlew cleanManagedDevices --unused-only
|
||||
|
||||
- name: Run benchmark on Gradle Managed Device
|
||||
run: |
|
||||
./gradlew api31BenchmarkAndroidTest \
|
||||
-Dorg.gradle.workers.max=1 \
|
||||
-Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile \
|
||||
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
||||
|
||||
# If we're on main branch, copy over the baseline profile and
|
||||
# commit it to the repository (if changed)
|
||||
- name: Commit baseline profile into main
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
# Pull down any changes which may have been committed while this workflow has been running
|
||||
git pull
|
||||
# Sort the baseline profile, output to android/
|
||||
sort -o android/src/main/baseline-prof.txt benchmark/build/outputs/managed_device_android_test_additional_output/benchmark/api31/BaselineProfileGenerator_generateBaselineProfile-baseline-prof.txt
|
||||
# If the baseline profile has changed, commit it
|
||||
if [[ $(git diff --stat android/src/main/baseline-prof.txt) != '' ]]; then
|
||||
git add android/src/main/baseline-prof.txt
|
||||
git commit -m "chore(android): refresh baseline profile" && git push
|
||||
fi
|
||||
|
||||
# Upload the entire output folder and attach it to the CI run
|
||||
- name: Attach baseline profile
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: Baseline profile output
|
||||
path: benchmark/build/outputs/managed_device_android_test_additional_output
|
||||
|
||||
- name: Clean secrets
|
||||
if: always()
|
||||
run: scripts/signing-cleanup.sh
|
||||
@@ -12,24 +12,25 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Run unit tests
|
||||
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
|
||||
uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1
|
||||
with:
|
||||
arguments: --no-configuration-cache --stacktrace check -PslimTests
|
||||
gradle-home-cache-cleanup: true
|
||||
@@ -38,21 +39,50 @@ jobs:
|
||||
|
||||
- name: (Fail-only) Upload test report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
|
||||
with:
|
||||
name: Test report
|
||||
path: android/build/reports
|
||||
path: '**/build/reports/tests/**'
|
||||
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@4cd9eb2d23752464a87e00499c30d256a59a01b4 # v4
|
||||
with:
|
||||
base-ref: refs/heads/main
|
||||
head-ref: ${{ github.ref }}
|
||||
|
||||
validate-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Check changelog format
|
||||
uses: mindsers/changelog-reader-action@b97ce03a10d9bdbb07beb491c76a5a01d78cd3ef # v2.2.2
|
||||
with:
|
||||
path: ./CHANGELOG.md
|
||||
validation_level: error
|
||||
|
||||
deploy-release-snapshot:
|
||||
runs-on: ubuntu-latest
|
||||
if: "github.event_name == 'push' && github.event.ref == 'refs/heads/main'"
|
||||
needs: [ "check" ]
|
||||
needs:
|
||||
- check
|
||||
- validate-changelog
|
||||
- dependency-review
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
@@ -60,12 +90,12 @@ jobs:
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$ENCRYPT_KEY"
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
env:
|
||||
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
|
||||
- name: Build release app
|
||||
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
|
||||
uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
name: Code quality analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- renovate/**
|
||||
schedule:
|
||||
- cron: '31 7 * * 3'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
name: CodeQL
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1
|
||||
with:
|
||||
languages: java
|
||||
tools: latest
|
||||
queries: +security-extended
|
||||
|
||||
- name: Build project
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew assembleDebug assembleRelease
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1
|
||||
with:
|
||||
category: "/language:java"
|
||||
mobsfscan:
|
||||
name: MobSF
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Run mobsfscan
|
||||
uses: MobSF/mobsfscan@e29e85c36fa1aa950d7f1b1064345d94a70b2a28
|
||||
with:
|
||||
args: . --sarif --output results.sarif || true
|
||||
|
||||
- name: Upload mobsfscan report
|
||||
uses: github/codeql-action/upload-sarif@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -1,50 +0,0 @@
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '31 7 * * 3'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0
|
||||
with:
|
||||
languages: java
|
||||
tools: latest
|
||||
queries: +security-extended
|
||||
|
||||
- name: Build project
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew assemble -PslimTests
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0
|
||||
with:
|
||||
category: "/language:java"
|
||||
@@ -1,6 +1,12 @@
|
||||
name: Release to Google Play
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
inAppUpdatePriority:
|
||||
description: 'In app update priority (0-5)'
|
||||
type: number
|
||||
default: "0"
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -10,13 +16,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
@@ -28,7 +34,7 @@ jobs:
|
||||
git config user.email noreply@github.com
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
|
||||
uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
@@ -36,9 +42,9 @@ jobs:
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$ENCRYPT_KEY"
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
env:
|
||||
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
|
||||
- name: Get current version
|
||||
shell: bash
|
||||
@@ -82,11 +88,10 @@ jobs:
|
||||
CHANGELOG: ${{ steps.changelog_reader.outputs.changes }}
|
||||
run: |
|
||||
mkdir -p distribution/whatsnew
|
||||
printf '## Changelog\n\n' > distribution/whatsnew/whatsnew-en-GB
|
||||
printf '%s' "${CHANGELOG}" >> distribution/whatsnew/whatsnew-en-GB
|
||||
|
||||
- name: Publish bundle to Google Play
|
||||
uses: r0adkll/upload-google-play@70c7397e868b00eccd390222f492ab3e30520d09 # v1.1.1
|
||||
uses: r0adkll/upload-google-play@9cf8fd30c9e616ec761db3b9abb2b0f15842ce58 # v1.1.2
|
||||
with:
|
||||
mappingFile: android/bundle/mapping.txt
|
||||
packageName: dev.msfjarvis.claw.android
|
||||
@@ -95,6 +100,7 @@ jobs:
|
||||
status: completed
|
||||
track: production
|
||||
whatsNewDirectory: distribution/whatsnew
|
||||
inAppUpdatePriority: ${{ inputs.inAppUpdatePriority }}
|
||||
|
||||
- name: Post-release work
|
||||
shell: bash
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
*.json
|
||||
baseline-prof.txt
|
||||
startup-prof.txt
|
||||
**/test/resources/**
|
||||
@@ -7,6 +7,88 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [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 January releases
|
||||
|
||||
## [1.38.0] - 2023-11-20
|
||||
|
||||
### Changed
|
||||
|
||||
* Add brand new icon by dzuk
|
||||
* Fix a bug that caused the app to fetch the same 20 posts over and over
|
||||
* Significantly improve scrolling performance when there are a lot of saved posts
|
||||
* Move search feature to its own screen
|
||||
* Make top app and bottom system bars use the same color
|
||||
* Reduce unnecessary API calls in search screen
|
||||
* Upgrade to Compose November releases
|
||||
* Enable logging of SQLite queries
|
||||
|
||||
## [1.37.0] - 2023-10-06
|
||||
|
||||
### Changed
|
||||
|
||||
* Fixed a crash that infrequently happened in the comments page
|
||||
* Fixed a crash when trying to list saved posts
|
||||
* Fixed a crash in home screen widget when user had less than 50 saved posts
|
||||
* Upgraded to Compose 1.6.0-alpha07
|
||||
|
||||
## [1.36.0] - 2023-10-04
|
||||
|
||||
### Added
|
||||
|
||||
* Introduce an initial attempt at a home screen widget
|
||||
|
||||
### Changed
|
||||
|
||||
* Fixed a crash triggered when swiping items
|
||||
* Adjusted pull to refresh component to match Material You theme
|
||||
|
||||
## [1.35.0] - 2023-09-19
|
||||
|
||||
### Changed
|
||||
|
||||
* Fixed missing vertical spacing between post tags
|
||||
* Fixed user profile links not displaying correctly in-app
|
||||
* Update to Compose October release
|
||||
|
||||
## [1.34.0] - 2023-08-30
|
||||
|
||||
### Added
|
||||
|
||||
* Add a swipe action on each story to open the comments page on `lobste.rs`
|
||||
* Automatically mark posts as "read" and visually distinguish between them
|
||||
|
||||
### Changed
|
||||
|
||||
* Tweak list items to reduce vertical size and adjust colors
|
||||
* Drop bogus workarounds for native library crashes
|
||||
* Updated Jetpack Compose and SQLite
|
||||
|
||||
### Fixed
|
||||
|
||||
* Rework how comments are displayed to adapt to lobste.rs API change
|
||||
|
||||
## [1.33.0] - 2023-07-31
|
||||
|
||||
### Changed
|
||||
|
||||
* Yet another attempt at fixing native library crashes
|
||||
* Rework how baseline profiles are generated
|
||||
|
||||
## [1.32.0] - 2023-07-27
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to AndroidX July 26 release
|
||||
* Revert selectable text feature due to sporadic crashes
|
||||
|
||||
## [1.31.0] - 2023-07-25
|
||||
|
||||
### Added
|
||||
@@ -262,7 +344,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
* Initial Play Store release
|
||||
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.31.0...HEAD
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.39.0...HEAD
|
||||
|
||||
[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
|
||||
|
||||
|
||||
@@ -10,33 +10,14 @@ Unofficial Android app for read-only access to [lobste.rs](https://lobste.rs), b
|
||||
|
||||
> Snapshots from the development branch can be obtained [here](https://github.com/msfjarvis/compose-lobsters/releases/tag/nightly).
|
||||
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature.webp"
|
||||
alt="Side by side screenshots of the app's main page in dark and light UI modes"
|
||||
height="550" />
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature_light.webp#gh-light-mode-only"
|
||||
alt="A grid of screenshots from the app, in clockwise order: the main screen, the comments page, the search page and the saved posts page"
|
||||
width="550" />
|
||||
|
||||
## Dependency Diagram
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature_dark.webp#gh-dark-mode-only"
|
||||
alt="A grid of screenshots from the app, in clockwise order: the main screen, the comments page, the search page and the saved posts page"
|
||||
width="550" />
|
||||
|
||||
```mermaid
|
||||
%%{
|
||||
init: {
|
||||
'theme': 'neutral'
|
||||
}
|
||||
}%%
|
||||
|
||||
graph LR
|
||||
|
||||
benchmark --> android
|
||||
android --> api
|
||||
android --> common
|
||||
android --> core
|
||||
android --> database
|
||||
android --> model
|
||||
api --> model
|
||||
common --> core
|
||||
common --> database
|
||||
common --> model
|
||||
|
||||
```
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
-dontobfuscate
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -15,29 +15,41 @@ plugins {
|
||||
id("dev.msfjarvis.claw.kotlin-kapt")
|
||||
id("dev.msfjarvis.claw.sentry")
|
||||
id("dev.msfjarvis.claw.versioning-plugin")
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.modulegraphassert)
|
||||
alias(libs.plugins.whetstone)
|
||||
alias(libs.plugins.baselineprofile)
|
||||
alias(libs.plugins.licensee)
|
||||
alias(libs.plugins.tracelog)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.msfjarvis.claw.android"
|
||||
defaultConfig { applicationId = "dev.msfjarvis.claw.android" }
|
||||
buildFeatures { compose = true }
|
||||
defaultConfig.applicationId = "dev.msfjarvis.claw.android"
|
||||
buildFeatures.compose = true
|
||||
composeOptions {
|
||||
useLiveLiterals = false
|
||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
}
|
||||
buildTypes {
|
||||
create("benchmark") {
|
||||
signingConfig = signingConfigs["debug"]
|
||||
matchingFallbacks += "release"
|
||||
isDebuggable = false
|
||||
proguardFile("benchmark-rules.pro")
|
||||
applicationIdSuffix = ".benchmark"
|
||||
}
|
||||
// kotlinx.serialization 1.6.0-RC regression
|
||||
packagingOptions.resources.pickFirsts += "**/*.bin"
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
mergeIntoMain = true
|
||||
saveInSrc = true
|
||||
from(projects.benchmark.dependencyProject)
|
||||
}
|
||||
|
||||
licensee {
|
||||
allow("Apache-2.0")
|
||||
allow("MIT")
|
||||
ignoreDependencies("org.commonmark") { because("Commonmark is BSD licensed") }
|
||||
allowUrl("https://jsoup.org/license") { because("Jsoup is MIT licensed") }
|
||||
allowUrl("https://opensource.org/licenses/isc-license.txt") {
|
||||
because("kotlin-result is ISC licensed")
|
||||
}
|
||||
packaging { jniLibs { useLegacyPackaging = true } }
|
||||
}
|
||||
|
||||
moduleGraphAssert {
|
||||
@@ -56,14 +68,14 @@ whetstone {
|
||||
dependencies {
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.accompanist.sysuicontroller)
|
||||
implementation(libs.aboutLibraries.m3)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.material)
|
||||
implementation(libs.androidx.compose.glance)
|
||||
implementation(libs.androidx.compose.glance.m3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material3.window.size)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.lifecycle.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
@@ -75,15 +87,17 @@ dependencies {
|
||||
implementation(libs.jsoup)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.material3.pulltorefresh)
|
||||
implementation(libs.napier)
|
||||
implementation(libs.soloader)
|
||||
implementation(libs.sqldelight.extensions.coroutines)
|
||||
implementation(libs.swipe)
|
||||
implementation(libs.unfurl)
|
||||
implementation(projects.api)
|
||||
implementation(projects.common)
|
||||
implementation(projects.core)
|
||||
implementation(projects.database)
|
||||
implementation(projects.database.impl)
|
||||
implementation(projects.model)
|
||||
implementation(projects.web)
|
||||
|
||||
kapt(libs.dagger.compiler)
|
||||
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
|
||||
<issue
|
||||
id="VectorPath"
|
||||
message="Very long vector path (918 characters), which is bad for performance. Considering reducing precision, removing minor details or rasterizing vector."
|
||||
errorLine1=" android:pathData="M431.3,341.2C435.9,337 580.3,236 710.5,223.8C747.3,220.3 790.7,254.7 833.2,312.5C875.9,370.7 892.9,433.4 875.9,443C863.2,450.2 838.6,407.4 758.8,396.3C743.4,394.2 730.4,393.4 719.4,393.5C738.5,423.4 745.6,451.1 737.2,458C733.1,461.4 673.6,438.7 612.9,436.5C570.1,435 535.2,440.9 506.7,451.3C496.9,454.9 486,453.8 477.2,448.3C444.5,427.9 424.2,406.2 418.4,376.2C415.8,362.8 420.9,349.4 431.3,341.2ZM274.7,394.8C285.8,370.1 312.2,325.9 346.3,305.4C383.6,282.9 335,390.3 427.3,489.8C519.5,589.3 586.5,623.8 655.1,581.5C723.7,539.2 746.5,518.1 753,518.7C764,519.6 772.9,536.1 764.4,589.5C763,598.2 761.3,606.5 759.3,614.2C828.7,575 871.7,516.3 889,520C912,524.8 918.3,617.8 852.1,721C811.8,784 768.8,830.7 741.4,857.5C724.3,874 699.7,880 677,873.1C512,822.6 315.5,733.8 202.9,652C193.8,645.2 186.8,635.9 183,625.1C171.8,593.7 161.3,548.1 168.5,495.3C179.5,414.2 222.7,352.9 245.2,335.9C261.9,323.2 267.1,362.4 274.7,394.8Z""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_launcher_foreground.xml"
|
||||
line="17"
|
||||
column="27"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Copyright © 2021-2024 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
@@ -25,6 +25,8 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="colorMode|density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -132,6 +134,19 @@
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".SearchActivity"
|
||||
android:configChanges="colorMode|density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
|
||||
android:exported="true"
|
||||
android:label="Claw Search"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/Theme.Claw">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
@@ -146,23 +161,34 @@
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
<!-- https://issuetracker.google.com/issues/258619948 -->
|
||||
<receiver
|
||||
android:name="androidx.profileinstaller.ProfileInstallReceiver"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:name=".glance.WidgetReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION" />
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/saved_posts_widget_info" />
|
||||
</receiver>
|
||||
<!-- Required: set your sentry.io project identifier (DSN) -->
|
||||
<meta-data android:name="io.sentry.dsn" android:value="${sentryDsn}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.dsn"
|
||||
android:value="${sentryDsn}" />
|
||||
<!-- enable automatic breadcrumbs for user interactions (clicks, swipes, scrolls) -->
|
||||
<meta-data android:name="io.sentry.traces.user-interaction.enable" android:value="${enableSentry}" />
|
||||
<meta-data android:name="io.sentry.breadcrumbs.user-interaction" android:value="${enableSentry}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.traces.user-interaction.enable"
|
||||
android:value="${enableSentry}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.breadcrumbs.user-interaction"
|
||||
android:value="${enableSentry}" />
|
||||
<!-- enable screenshot for crashes -->
|
||||
<meta-data android:name="io.sentry.attach-screenshot" android:value="${enableSentry}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-screenshot"
|
||||
android:value="${enableSentry}" />
|
||||
<!-- enable view hierarchy for crashes -->
|
||||
<meta-data android:name="io.sentry.attach-view-hierarchy" android:value="${enableSentry}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-view-hierarchy"
|
||||
android:value="${enableSentry}" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android
|
||||
|
||||
import android.app.assist.AssistContent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import javax.inject.Inject
|
||||
|
||||
/** A base class that encapsulates all activities used by Claw. */
|
||||
abstract class BaseActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var urlLauncher: UrlLauncher
|
||||
@Inject lateinit var htmlConverter: HTMLConverter
|
||||
@Inject lateinit var viewModel: ClawViewModel
|
||||
var webUri: String? = null
|
||||
|
||||
/** Entrypoint to show a [Composable] as this [ComponentActivity]'s view. */
|
||||
@Composable abstract fun Content()
|
||||
|
||||
/** Overrideable method to call stuff in [onCreate]. */
|
||||
open fun preLaunch() {}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
preLaunch()
|
||||
Whetstone.inject(this)
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||
navigationBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||
)
|
||||
setContent {
|
||||
LobstersTheme(
|
||||
dynamicColor = true,
|
||||
providedValues = arrayOf(LocalUriHandler provides urlLauncher),
|
||||
) {
|
||||
Content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent?) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
if (outContent != null) {
|
||||
if (webUri != null) {
|
||||
outContent.webUri = Uri.parse(webUri)
|
||||
} else {
|
||||
outContent.webUri = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import androidx.work.WorkManager
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.app.ApplicationComponentOwner
|
||||
import com.deliveryhero.whetstone.app.ContributesAppInjector
|
||||
import com.facebook.soloader.SoLoader
|
||||
import dev.msfjarvis.claw.android.work.SavedPostUpdaterWorker
|
||||
import dev.msfjarvis.claw.core.injection.AppPlugin
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -32,7 +31,6 @@ class ClawApplication : Application(), ApplicationComponentOwner {
|
||||
override fun onCreate() {
|
||||
Whetstone.inject(this)
|
||||
super.onCreate()
|
||||
SoLoader.init(this, false)
|
||||
plugins.forEach { plugin -> plugin.apply(this) }
|
||||
val postUpdateWorkRequest =
|
||||
PeriodicWorkRequestBuilder<SavedPostUpdaterWorker>(POST_REFRESH_PERIOD, TimeUnit.HOURS)
|
||||
|
||||
@@ -1,60 +1,39 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android
|
||||
|
||||
import android.app.assist.AssistContent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
|
||||
import dev.msfjarvis.claw.android.ui.LobstersApp
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import javax.inject.Inject
|
||||
import dev.msfjarvis.claw.android.ui.screens.LobstersPostsScreen
|
||||
|
||||
@ContributesActivityInjector
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var urlLauncher: UrlLauncher
|
||||
@Inject lateinit var htmlConverter: HTMLConverter
|
||||
private var webUri: String? = null
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
installSplashScreen()
|
||||
Whetstone.inject(this)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
setContent {
|
||||
val windowSizeClass = calculateWindowSizeClass(this)
|
||||
|
||||
LobstersApp(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
windowSizeClass = windowSizeClass,
|
||||
setWebUri = { url -> webUri = url },
|
||||
)
|
||||
}
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val windowSizeClass = calculateWindowSizeClass(this)
|
||||
LobstersPostsScreen(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
windowSizeClass = windowSizeClass,
|
||||
setWebUri = { url -> webUri = url },
|
||||
)
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent?) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
if (outContent != null) {
|
||||
if (webUri != null) {
|
||||
outContent.webUri = Uri.parse(webUri)
|
||||
} else {
|
||||
outContent.webUri = null
|
||||
}
|
||||
}
|
||||
override fun preLaunch() {
|
||||
super.preLaunch()
|
||||
installSplashScreen()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NAVIGATION_KEY = "postId"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
|
||||
import dev.msfjarvis.claw.android.ui.screens.SearchScreen
|
||||
|
||||
@ContributesActivityInjector
|
||||
class SearchActivity : BaseActivity() {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
SearchScreen(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
setWebUri = { webUri = it },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.appWidgetBackground
|
||||
import androidx.glance.appwidget.lazy.LazyColumn
|
||||
import androidx.glance.appwidget.lazy.items
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.background
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.material3.ColorProviders
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import dev.msfjarvis.claw.common.theme.DarkThemeColors
|
||||
import dev.msfjarvis.claw.common.theme.LightThemeColors
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
|
||||
class SavedPostsWidget(private val posts: List<SavedPost>) : GlanceAppWidget() {
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
provideContent {
|
||||
GlanceTheme(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) GlanceTheme.colors
|
||||
else ColorProviders(light = LightThemeColors, dark = DarkThemeColors)
|
||||
) {
|
||||
WidgetHost()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WidgetHost() {
|
||||
LazyColumn(
|
||||
modifier =
|
||||
GlanceModifier.fillMaxSize()
|
||||
.background(GlanceTheme.colors.background)
|
||||
.appWidgetBackground(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
item {
|
||||
val style = MaterialTheme.typography.titleLarge
|
||||
Text(
|
||||
text = "Saved posts",
|
||||
style =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onBackground,
|
||||
fontSize = style.fontSize,
|
||||
fontWeight = style.fontWeight.toGlance(),
|
||||
fontStyle = style.fontStyle.toGlance(),
|
||||
),
|
||||
modifier = GlanceModifier.padding(vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
items(posts) { post -> WidgetListEntry(post = post) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceComposable
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.action.ActionParameters.Key
|
||||
import androidx.glance.action.actionParametersOf
|
||||
import androidx.glance.action.actionStartActivity
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.action.actionStartActivity
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.background
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.MainActivity.Companion.NAVIGATION_KEY
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
|
||||
private val destinationKey = Key<String>(NAVIGATION_KEY)
|
||||
|
||||
@Composable
|
||||
@GlanceComposable
|
||||
fun WidgetListEntry(post: SavedPost, modifier: GlanceModifier = GlanceModifier) {
|
||||
val titleStyle = MaterialTheme.typography.titleMedium
|
||||
val commentsAction =
|
||||
actionStartActivity<MainActivity>(actionParametersOf(destinationKey to post.shortId))
|
||||
val postAction =
|
||||
if (post.url.isNotEmpty()) actionStartActivity(Intent(Intent.ACTION_VIEW, Uri.parse(post.url)))
|
||||
else commentsAction
|
||||
Box(modifier.padding(8.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
GlanceModifier.fillMaxWidth()
|
||||
.background(GlanceTheme.colors.surfaceVariant)
|
||||
.cornerRadius(4.dp)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = post.title,
|
||||
modifier = GlanceModifier.defaultWeight().padding(start = 4.dp).clickable(postAction),
|
||||
style =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onSurfaceVariant,
|
||||
fontSize = titleStyle.fontSize,
|
||||
fontWeight = titleStyle.fontWeight.toGlance(),
|
||||
fontStyle = titleStyle.fontStyle.toGlance(),
|
||||
),
|
||||
)
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.ic_comment),
|
||||
contentDescription = "${post.commentCount ?: 0} comments",
|
||||
modifier = GlanceModifier.padding(end = 4.dp).clickable(commentsAction),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.broadcastreceiver.ContributesBroadcastReceiverInjector
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@ContributesBroadcastReceiverInjector
|
||||
class WidgetReceiver : GlanceAppWidgetReceiver() {
|
||||
|
||||
@Inject lateinit var viewModel: ClawViewModel
|
||||
|
||||
override val glanceAppWidget: GlanceAppWidget
|
||||
get() = SavedPostsWidget(runBlocking { viewModel.savedPosts.first().take(50) })
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Whetstone.inject(this, context)
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
fun FontWeight?.toGlance(): androidx.glance.text.FontWeight? {
|
||||
return when (this) {
|
||||
FontWeight.Normal -> androidx.glance.text.FontWeight.Normal
|
||||
FontWeight.Medium -> androidx.glance.text.FontWeight.Medium
|
||||
FontWeight.Bold -> androidx.glance.text.FontWeight.Bold
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun FontStyle?.toGlance(): androidx.glance.text.FontStyle? {
|
||||
return when (this) {
|
||||
FontStyle.Normal -> androidx.glance.text.FontStyle.Normal
|
||||
FontStyle.Italic -> androidx.glance.text.FontStyle.Italic
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import com.deliveryhero.whetstone.ForScope
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dev.msfjarvis.claw.core.coroutines.DispatcherProvider
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
class DataStoreModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideLoginDatastore(
|
||||
@ForScope(ApplicationScope::class) context: Context,
|
||||
dispatcherProvider: DispatcherProvider,
|
||||
): DataStore<Preferences> {
|
||||
return PreferenceDataStoreFactory.create(
|
||||
corruptionHandler = null,
|
||||
migrations = emptyList(),
|
||||
scope = CoroutineScope(dispatcherProvider.io() + SupervisorJob()),
|
||||
) {
|
||||
context.preferencesDataStoreFile("login")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,5 +16,5 @@ import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
interface HTMLConverterModule {
|
||||
@Binds fun HTMLConverterImpl.bind(): HTMLConverter
|
||||
@Binds fun bindHTMLConverter(impl: HTMLConverterImpl): HTMLConverter
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -32,13 +32,7 @@ object MetadataExtractorModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideUnfurler(
|
||||
okHttpClient: OkHttpClient,
|
||||
logger: UnfurlLogger,
|
||||
): Unfurler {
|
||||
return Unfurler(
|
||||
httpClient = okHttpClient,
|
||||
logger = logger,
|
||||
)
|
||||
fun provideUnfurler(okHttpClient: OkHttpClient, logger: UnfurlLogger): Unfurler {
|
||||
return Unfurler(httpClient = okHttpClient, logger = logger)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 Harsh Shandilya.
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -7,9 +7,9 @@
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
|
||||
import android.content.Context
|
||||
import com.deliveryhero.whetstone.ForScope
|
||||
import com.deliveryhero.whetstone.activity.ActivityScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import com.squareup.anvil.annotations.optional.ForScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -17,18 +17,14 @@ import dev.msfjarvis.claw.core.injection.AppPlugin
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesMultibinding(ApplicationScope::class)
|
||||
class WorkManagerPlugin
|
||||
@Inject
|
||||
constructor(
|
||||
private val workerFactory: WorkerFactory,
|
||||
) : AppPlugin {
|
||||
class WorkManagerPlugin @Inject constructor(private val workerFactory: WorkerFactory) : AppPlugin {
|
||||
override fun apply(application: Application) {
|
||||
WorkManager.initialize(
|
||||
application,
|
||||
Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ 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.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import java.io.IOException
|
||||
@@ -21,15 +22,16 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Duplicate of [LobstersPagingSource] with an additional optimization to not continue loading
|
||||
* further pages if nothing is found in the current one. This is required to prevent the app from
|
||||
* hammering the search endpoint with up to 70 consecutive requests for the search results of a
|
||||
* blank query.
|
||||
* Specialized variant of [LobstersPagingSource] to prevent hammering the Lobsters search endpoint
|
||||
* with unnecessary requests. Rather than abstract out the API call this paging source takes in the
|
||||
* relevant parameters to short-circuit when there is no query specified in order to avoid calling
|
||||
* the API at all.
|
||||
*/
|
||||
class SearchPagingSource
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
|
||||
private val searchApi: LobstersSearchApi,
|
||||
@Assisted private val queryProvider: () -> String,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : PagingSource<Int, LobstersPost>() {
|
||||
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int? {
|
||||
@@ -39,8 +41,14 @@ constructor(
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
|
||||
val searchQuery = queryProvider()
|
||||
// If there is no query, we don't need to call the API at all.
|
||||
if (searchQuery.isEmpty()) {
|
||||
return LoadResult.Page(itemsBefore = 0, data = emptyList(), prevKey = null, nextKey = null)
|
||||
}
|
||||
val page = params.key ?: STARTING_PAGE_INDEX
|
||||
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
|
||||
val result = withContext(ioDispatcher) { searchApi.searchPosts(searchQuery, page) }
|
||||
return when (result) {
|
||||
is ApiResult.Success -> {
|
||||
// Optimization: prevent fetching more pages if we found no items, this means
|
||||
// there is no active search query.
|
||||
@@ -62,6 +70,6 @@ constructor(
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(remoteFetcher: RemoteFetcher<LobstersPost>): SearchPagingSource
|
||||
fun create(queryProvider: () -> String): SearchPagingSource
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.settings
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import javax.inject.Inject
|
||||
|
||||
class LoginSettings
|
||||
@Inject
|
||||
constructor(
|
||||
val dataStore: DataStore<Preferences>,
|
||||
) {
|
||||
companion object {
|
||||
val CSRF_TOKEN = stringPreferencesKey("csrf_token")
|
||||
val LOGIN_COOKIE = stringPreferencesKey("login_cookie")
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.ImportExport
|
||||
import androidx.compose.material.icons.filled.NewReleases
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Whatshot
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material.icons.outlined.Whatshot
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.android.ui.datatransfer.DataTransferScreen
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
|
||||
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
|
||||
import dev.msfjarvis.claw.android.ui.decorations.TransparentSystemBars
|
||||
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
|
||||
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
|
||||
import dev.msfjarvis.claw.android.ui.lists.SearchList
|
||||
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import dev.msfjarvis.claw.android.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.theme.LobstersTheme
|
||||
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.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LobstersApp(
|
||||
urlLauncher: UrlLauncher,
|
||||
htmlConverter: HTMLConverter,
|
||||
windowSizeClass: WindowSizeClass,
|
||||
setWebUri: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
) {
|
||||
val hottestListState = rememberLazyListState()
|
||||
val newestListState = rememberLazyListState()
|
||||
val savedListState = rememberLazyListState()
|
||||
val searchListState = rememberLazyListState()
|
||||
val navController = rememberNavController()
|
||||
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 hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
|
||||
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
|
||||
val savedPosts by viewModel.savedPosts.collectAsState(persistentMapOf())
|
||||
|
||||
val navigationType = ClawNavigationType.fromSize(windowSizeClass.widthSizeClass)
|
||||
|
||||
LobstersTheme(
|
||||
dynamicColor = true,
|
||||
providedValues = arrayOf(LocalUriHandler provides urlLauncher),
|
||||
) {
|
||||
val navItems =
|
||||
persistentListOf(
|
||||
NavigationItem(
|
||||
label = "Hottest",
|
||||
route = Destinations.Hottest.route,
|
||||
icon = Icons.Outlined.Whatshot,
|
||||
selectedIcon = Icons.Filled.Whatshot,
|
||||
) {
|
||||
coroutineScope.launch { hottestListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Newest",
|
||||
route = Destinations.Newest.route,
|
||||
icon = Icons.Outlined.NewReleases,
|
||||
selectedIcon = Icons.Filled.NewReleases,
|
||||
) {
|
||||
coroutineScope.launch { newestListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Saved",
|
||||
route = Destinations.Saved.route,
|
||||
icon = Icons.Outlined.FavoriteBorder,
|
||||
selectedIcon = Icons.Filled.Favorite,
|
||||
) {
|
||||
coroutineScope.launch { savedListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Search",
|
||||
route = Destinations.Search.route,
|
||||
icon = Icons.Outlined.Search,
|
||||
selectedIcon = Icons.Filled.Search,
|
||||
) {
|
||||
coroutineScope.launch { searchListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
)
|
||||
|
||||
TransparentSystemBars()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (currentDestination != Destinations.Search.route) {
|
||||
ClawAppBar(
|
||||
navigationIcon = {
|
||||
if (
|
||||
navController.previousBackStackEntry != null &&
|
||||
navItems.none { it.route == currentDestination }
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = "Go back to previous screen",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
IconButton(onClick = { navController.navigate(Destinations.DataTransfer.route) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ImportExport,
|
||||
contentDescription = "Data transfer options",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
|
||||
ClawNavigationBar(
|
||||
navController = navController,
|
||||
items = navItems,
|
||||
isVisible = navItems.any { it.route == currentDestination },
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
modifier = modifier.semantics { testTagsAsResourceId = true },
|
||||
) { paddingValues ->
|
||||
Row(modifier = Modifier.padding(paddingValues)) {
|
||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.NAVIGATION_RAIL) {
|
||||
ClawNavigationRail(
|
||||
navController = navController,
|
||||
items = navItems,
|
||||
isVisible = navItems.any { it.route == currentDestination },
|
||||
)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Destinations.startDestination.route,
|
||||
// Make animations 2x faster than default specs
|
||||
enterTransition = { fadeIn(animationSpec = tween(350)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(350)) },
|
||||
) {
|
||||
val uri = LobstersApi.BASE_URL
|
||||
composable(route = Destinations.Hottest.route) {
|
||||
setWebUri("https://lobste.rs/")
|
||||
NetworkPosts(
|
||||
lazyPagingItems = hottestPosts,
|
||||
listState = hottestListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.Newest.route) {
|
||||
setWebUri("https://lobste.rs/")
|
||||
NetworkPosts(
|
||||
lazyPagingItems = newestPosts,
|
||||
listState = newestListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.Saved.route) {
|
||||
setWebUri(null)
|
||||
DatabasePosts(
|
||||
items = savedPosts,
|
||||
listState = savedListState,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
composable(Destinations.Search.route) {
|
||||
setWebUri("https://lobste.rs/search")
|
||||
SearchList(
|
||||
items = viewModel.searchResults,
|
||||
listState = searchListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
postActions = postActions,
|
||||
searchQuery = viewModel.searchQuery,
|
||||
setSearchQuery = { query -> viewModel.searchQuery = query },
|
||||
)
|
||||
}
|
||||
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"))
|
||||
setWebUri("https://lobste.rs/s/$postId")
|
||||
CommentsPage(
|
||||
postId = postId,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
)
|
||||
}
|
||||
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"))
|
||||
setWebUri("https://lobste.rs/u/$username")
|
||||
UserProfile(
|
||||
username = username,
|
||||
getProfile = viewModel::getUserProfile,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.DataTransfer.route) {
|
||||
DataTransferScreen(
|
||||
context = context,
|
||||
importPosts = viewModel::importPosts,
|
||||
exportPosts = viewModel::exportPosts,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.datatransfer
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.activity.result.contract.ActivityResultContracts.GetContent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.Upload
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val MIME_TYPE = "application/json"
|
||||
|
||||
@Composable
|
||||
fun DataTransferScreen(
|
||||
context: Context,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
exportPosts: suspend (OutputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Column(modifier = modifier) {
|
||||
ImportOption(context, coroutineScope, importPosts, snackbarHostState)
|
||||
ExportOption(context, coroutineScope, exportPosts, snackbarHostState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImportOption(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val importAction =
|
||||
rememberLauncherForActivityResult(GetContent()) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
importPosts(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully imported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsActionItem(
|
||||
title = "Import saved posts",
|
||||
description = "Import saved posts from a previously generated export",
|
||||
icon = Icons.Filled.Download,
|
||||
) {
|
||||
importAction.launch(MIME_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExportOption(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
exportPosts: suspend (OutputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val exportAction =
|
||||
rememberLauncherForActivityResult(CreateDocument(MIME_TYPE)) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openOutputStream(uri)?.use { stream ->
|
||||
exportPosts(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsActionItem(
|
||||
title = "Export posts to file",
|
||||
description = "Write all saved posts into a JSON file that can be imported at a later date",
|
||||
icon = Icons.Filled.Upload,
|
||||
) {
|
||||
exportAction.launch("claw-export.json")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsActionItem(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
description: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(title) },
|
||||
supportingContent = { description?.let { Text(it) } },
|
||||
leadingContent = {
|
||||
icon?.let {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = modifier.clickable { onClick?.invoke() },
|
||||
)
|
||||
}
|
||||
|
||||
/** Shows a Snackbar but dismisses any existing ones first. */
|
||||
private suspend fun SnackbarHostState.showSnackbarDismissing(
|
||||
text: String,
|
||||
) {
|
||||
currentSnackbarData?.dismiss()
|
||||
showSnackbar(text)
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.ui.decorations
|
||||
package dev.msfjarvis.claw.android.ui.decorations
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -19,6 +20,7 @@ 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
|
||||
import dev.msfjarvis.claw.common.ui.surfaceColorAtNavigationBarElevation
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -31,7 +33,10 @@ fun ClawAppBar(
|
||||
TopAppBar(
|
||||
title = title,
|
||||
modifier = modifier.shadow(8.dp),
|
||||
colors = TopAppBarDefaults.topAppBarColors(),
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtNavigationBarElevation()
|
||||
),
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actions,
|
||||
)
|
||||
@@ -41,9 +46,5 @@ fun ClawAppBar(
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun ClawAppBarPreview() {
|
||||
LobstersTheme {
|
||||
ClawAppBar(
|
||||
title = { Text("Claw", fontWeight = FontWeight.Bold) },
|
||||
)
|
||||
}
|
||||
LobstersTheme { ClawAppBar(title = { Text("Claw", fontWeight = FontWeight.Bold) }) }
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.decorations
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
|
||||
@Composable
|
||||
fun TransparentSystemBars() {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val useDarkIcons = !isSystemInDarkTheme()
|
||||
|
||||
DisposableEffect(systemUiController, useDarkIcons) {
|
||||
systemUiController.setSystemBarsColor(color = Color.Transparent, darkIcons = useDarkIcons)
|
||||
|
||||
onDispose {}
|
||||
}
|
||||
}
|
||||
@@ -36,14 +36,16 @@ fun rememberPostActions(
|
||||
): PostActions {
|
||||
return remember {
|
||||
object : PostActions {
|
||||
override fun viewPost(postUrl: String, commentsUrl: String) {
|
||||
override fun viewPost(postId: String, postUrl: String, commentsUrl: String) {
|
||||
viewModel.markPostAsRead(postId)
|
||||
urlLauncher.openUri(postUrl.ifEmpty { commentsUrl })
|
||||
}
|
||||
|
||||
override fun viewComments(postId: String) {
|
||||
viewModel.markPostAsRead(postId)
|
||||
val currentRoute = navController.currentDestination?.route
|
||||
val newRoute =
|
||||
Destinations.Comments.route.replace(Destinations.Comments.placeholder, postId)
|
||||
Destinations.Comments.route.replace(Destinations.Comments.PLACEHOLDER, postId)
|
||||
if (currentRoute != Destinations.Comments.route) navController.navigate(newRoute)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -17,7 +17,7 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Inbox
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -28,13 +28,12 @@ import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.ui.decorations.MonthHeader
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import java.time.Month
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun DatabasePosts(
|
||||
items: ImmutableMap<Month, List<SavedPost>>,
|
||||
items: ImmutableMap<String, List<SavedPost>>,
|
||||
listState: LazyListState,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -53,18 +52,15 @@ fun DatabasePosts(
|
||||
} else {
|
||||
LazyColumn(state = listState) {
|
||||
items.forEach { (month, posts) ->
|
||||
stickyHeader(contentType = "month-header") { MonthHeader(month = month) }
|
||||
items(
|
||||
items = posts,
|
||||
key = { it.shortId },
|
||||
contentType = { "LobstersItem" },
|
||||
) { item ->
|
||||
ListItem(
|
||||
stickyHeader(contentType = "month-header") { MonthHeader(label = month) }
|
||||
items(items = posts, key = { it.shortId }, contentType = { "LobstersItem" }) { item ->
|
||||
LobstersListItem(
|
||||
item = item,
|
||||
isSaved = { true },
|
||||
isRead = { false },
|
||||
postActions = postActions,
|
||||
)
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.lists
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.msfjarvis.claw.common.posts.LobstersCard
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
|
||||
@Composable
|
||||
fun ListItem(
|
||||
item: SavedPost,
|
||||
isSaved: suspend (SavedPost) -> Boolean,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val saved by produceState(false, item) { value = isSaved(item) }
|
||||
LobstersCard(
|
||||
post = item,
|
||||
isSaved = saved,
|
||||
postActions = postActions,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.lists
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Reply
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import dev.msfjarvis.claw.common.posts.LobstersCard
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import me.saket.swipe.SwipeAction
|
||||
import me.saket.swipe.SwipeableActionsBox
|
||||
|
||||
@Composable
|
||||
fun LobstersListItem(
|
||||
item: SavedPost,
|
||||
isSaved: (SavedPost) -> Boolean,
|
||||
isRead: suspend (String) -> Boolean,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val read by produceState(false, item.shortId) { value = isRead(item.shortId) }
|
||||
val commentsAction =
|
||||
SwipeAction(
|
||||
icon = rememberVectorPainter(Icons.AutoMirrored.Filled.Reply),
|
||||
background = MaterialTheme.colorScheme.tertiary,
|
||||
onSwipe = { postActions.viewCommentsPage(item.commentsUrl) },
|
||||
)
|
||||
SwipeableActionsBox(endActions = listOf(commentsAction)) {
|
||||
LobstersCard(
|
||||
post = item,
|
||||
isSaved = isSaved(item),
|
||||
isRead = read,
|
||||
postActions = postActions,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -14,13 +14,8 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -30,18 +25,22 @@ import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.itemContentType
|
||||
import androidx.paging.compose.itemKey
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.posts.toDbModel
|
||||
import dev.msfjarvis.claw.common.ui.NetworkError
|
||||
import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import eu.bambooapps.material3.pullrefresh.PullRefreshIndicator
|
||||
import eu.bambooapps.material3.pullrefresh.pullRefresh
|
||||
import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NetworkPosts(
|
||||
lazyPagingItems: LazyPagingItems<LobstersPost>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: suspend (SavedPost) -> Boolean,
|
||||
isPostSaved: (SavedPost) -> Boolean,
|
||||
isPostRead: suspend (String) -> Boolean,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -57,9 +56,7 @@ fun NetworkPosts(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
) {
|
||||
LazyColumn(state = listState) {
|
||||
items(
|
||||
count = lazyPagingItems.itemCount,
|
||||
key = lazyPagingItems.itemKey { it.shortId },
|
||||
@@ -67,14 +64,15 @@ fun NetworkPosts(
|
||||
) { index ->
|
||||
val item = lazyPagingItems[index]
|
||||
if (item != null) {
|
||||
val dbModel = item.toDbModel()
|
||||
ListItem(
|
||||
val dbModel = item.toSavedPost()
|
||||
LobstersListItem(
|
||||
item = dbModel,
|
||||
isSaved = isPostSaved,
|
||||
isRead = isPostRead,
|
||||
postActions = postActions,
|
||||
)
|
||||
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
if (lazyPagingItems.loadState.append == LoadState.Loading) {
|
||||
@@ -83,7 +81,7 @@ fun NetworkPosts(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
.padding(vertical = 16.dp),
|
||||
.padding(vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -93,8 +91,7 @@ fun NetworkPosts(
|
||||
refreshing = isRefreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
backgroundColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = contentColorFor(MaterialTheme.colorScheme.surface),
|
||||
shadowElevation = 6.dp, // From M2 implementation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,26 +6,13 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.lists
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SearchOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.isTraversalGroup
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -42,7 +29,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
fun SearchList(
|
||||
items: Flow<PagingData<LobstersPost>>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: suspend (SavedPost) -> Boolean,
|
||||
isPostSaved: (SavedPost) -> Boolean,
|
||||
postActions: PostActions,
|
||||
searchQuery: String,
|
||||
setSearchQuery: (String) -> Unit,
|
||||
@@ -53,41 +40,19 @@ fun SearchList(
|
||||
setSearchQuery(query)
|
||||
lazyPagingItems.refresh()
|
||||
}
|
||||
DisposableEffect(true) {
|
||||
// Clear search field when navigating away
|
||||
onDispose { setSearchQuery("") }
|
||||
}
|
||||
|
||||
var searchActive by remember { mutableStateOf(false) }
|
||||
Column(modifier = Modifier.semantics { isTraversalGroup = true }.zIndex(1f).fillMaxWidth()) {
|
||||
Column(modifier = modifier.semantics { isTraversalGroup = true }.zIndex(1f).fillMaxWidth()) {
|
||||
SearchBar(
|
||||
value = searchQuery,
|
||||
onValueChange = setSearchQuery,
|
||||
onSearch = {
|
||||
triggerSearch(it)
|
||||
searchActive = true
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
|
||||
onSearch = { triggerSearch(it) },
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp).testTag("search_bar"),
|
||||
)
|
||||
NetworkPosts(
|
||||
lazyPagingItems = lazyPagingItems,
|
||||
listState = listState,
|
||||
isPostSaved = isPostSaved,
|
||||
isPostRead = { false },
|
||||
postActions = postActions,
|
||||
)
|
||||
if (searchActive) {
|
||||
NetworkPosts(
|
||||
lazyPagingItems = lazyPagingItems,
|
||||
listState = listState,
|
||||
isPostSaved = isPostSaved,
|
||||
postActions = postActions,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.SearchOff,
|
||||
contentDescription = "No search results",
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally).size(36.dp),
|
||||
)
|
||||
Text(text = "Nothing to see here", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.login
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.google.accompanist.web.AccompanistWebViewClient
|
||||
import com.google.accompanist.web.WebView
|
||||
import com.google.accompanist.web.rememberWebViewState
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import io.github.aakira.napier.Napier
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
|
||||
@Suppress("Unused")
|
||||
@Composable
|
||||
fun LobstersLoginWebView(modifier: Modifier = Modifier) {
|
||||
val state = rememberWebViewState(url = "https://lobste.rs/login")
|
||||
val webClient = remember {
|
||||
object : AccompanistWebViewClient() {
|
||||
override fun onPageFinished(view: WebView, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
Napier.d(tag = "WebView") { "onPageFinished(url=$url)" }
|
||||
if (url != null && url.toHttpUrl() == LobstersApi.BASE_URL.toHttpUrl()) {
|
||||
val cookies = CookieManager.getInstance().getCookie(LobstersApi.BASE_URL)
|
||||
val trap =
|
||||
cookies
|
||||
.split(";")
|
||||
.associate {
|
||||
val split = it.split("=")
|
||||
split[0] to split[1]
|
||||
}["lobsters_trap"]
|
||||
Napier.d(tag = "WebView") { "lobsters_trap=${trap}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WebView(state = state, client = webClient, modifier = modifier, captureBackPresses = false)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -9,34 +9,38 @@ package dev.msfjarvis.claw.android.ui.navigation
|
||||
sealed class Destinations {
|
||||
abstract val route: String
|
||||
|
||||
object Hottest : Destinations() {
|
||||
data object Hottest : Destinations() {
|
||||
override val route = "hottest"
|
||||
}
|
||||
|
||||
object Newest : Destinations() {
|
||||
data object Newest : Destinations() {
|
||||
override val route = "newest"
|
||||
}
|
||||
|
||||
object Saved : Destinations() {
|
||||
data object Saved : Destinations() {
|
||||
override val route = "saved"
|
||||
}
|
||||
|
||||
object Search : Destinations() {
|
||||
data object Comments : Destinations() {
|
||||
const val PLACEHOLDER = "{postId}"
|
||||
override val route = "comments/$PLACEHOLDER"
|
||||
}
|
||||
|
||||
data object User : Destinations() {
|
||||
const val PLACEHOLDER = "{username}"
|
||||
override val route = "user/$PLACEHOLDER"
|
||||
}
|
||||
|
||||
data object Search : Destinations() {
|
||||
override val route = "search"
|
||||
}
|
||||
|
||||
object Comments : Destinations() {
|
||||
const val placeholder = "{postId}"
|
||||
override val route = "comments/$placeholder"
|
||||
data object Settings : Destinations() {
|
||||
override val route = "settings"
|
||||
}
|
||||
|
||||
object User : Destinations() {
|
||||
const val placeholder = "{username}"
|
||||
override val route = "user/$placeholder"
|
||||
}
|
||||
|
||||
object DataTransfer : Destinations() {
|
||||
override val route: String = "datatransfer"
|
||||
data object AboutLibraries : Destinations() {
|
||||
override val route = "about_libraries"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* 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.screens
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.NewReleases
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Tune
|
||||
import androidx.compose.material.icons.filled.Whatshot
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.Whatshot
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.android.SearchActivity
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawAppBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
|
||||
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
|
||||
import dev.msfjarvis.claw.android.ui.getActivity
|
||||
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
|
||||
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
|
||||
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import dev.msfjarvis.claw.android.ui.rememberPostActions
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import dev.msfjarvis.claw.common.user.UserProfile
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LobstersPostsScreen(
|
||||
urlLauncher: UrlLauncher,
|
||||
htmlConverter: HTMLConverter,
|
||||
windowSizeClass: WindowSizeClass,
|
||||
setWebUri: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
) {
|
||||
val hottestListState = rememberLazyListState()
|
||||
val newestListState = rememberLazyListState()
|
||||
val savedListState = rememberLazyListState()
|
||||
val navController = rememberNavController()
|
||||
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 hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
|
||||
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
|
||||
val savedPosts by viewModel.savedPostsByMonth.collectAsState(persistentMapOf())
|
||||
|
||||
val navigationType = ClawNavigationType.fromSize(windowSizeClass.widthSizeClass)
|
||||
|
||||
val postIdOverride = context.getActivity()?.intent?.extras?.getString(MainActivity.NAVIGATION_KEY)
|
||||
LaunchedEffect(false) {
|
||||
if (postIdOverride != null) {
|
||||
navController.navigate(
|
||||
Destinations.Comments.route.replace(Destinations.Comments.PLACEHOLDER, postIdOverride)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val navItems =
|
||||
persistentListOf(
|
||||
NavigationItem(
|
||||
label = "Hottest",
|
||||
route = Destinations.Hottest.route,
|
||||
icon = Icons.Outlined.Whatshot,
|
||||
selectedIcon = Icons.Filled.Whatshot,
|
||||
) {
|
||||
coroutineScope.launch { hottestListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Newest",
|
||||
route = Destinations.Newest.route,
|
||||
icon = Icons.Outlined.NewReleases,
|
||||
selectedIcon = Icons.Filled.NewReleases,
|
||||
) {
|
||||
coroutineScope.launch { newestListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Saved",
|
||||
route = Destinations.Saved.route,
|
||||
icon = Icons.Outlined.FavoriteBorder,
|
||||
selectedIcon = Icons.Filled.Favorite,
|
||||
) {
|
||||
coroutineScope.launch { savedListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
ClawAppBar(
|
||||
navigationIcon = {
|
||||
if (
|
||||
navController.previousBackStackEntry != null &&
|
||||
navItems.none { it.route == currentDestination }
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Go back to previous screen",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
IconButton(
|
||||
onClick = { context.startActivity(Intent(context, SearchActivity::class.java)) }
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Search, contentDescription = "Search posts")
|
||||
}
|
||||
IconButton(onClick = { navController.navigate(Destinations.Settings.route) }) {
|
||||
Icon(imageVector = Icons.Filled.Tune, contentDescription = "Settings")
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
|
||||
ClawNavigationBar(
|
||||
navController = navController,
|
||||
items = navItems,
|
||||
isVisible = navItems.any { it.route == currentDestination },
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
modifier = modifier.semantics { testTagsAsResourceId = true },
|
||||
) { paddingValues ->
|
||||
Row(modifier = Modifier.padding(paddingValues)) {
|
||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.NAVIGATION_RAIL) {
|
||||
ClawNavigationRail(
|
||||
navController = navController,
|
||||
items = navItems,
|
||||
isVisible = navItems.any { it.route == currentDestination },
|
||||
)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Destinations.startDestination.route,
|
||||
// Make animations 2x faster than default specs
|
||||
enterTransition = { fadeIn(animationSpec = tween(350)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(350)) },
|
||||
) {
|
||||
val uri = LobstersApi.BASE_URL
|
||||
composable(route = Destinations.Hottest.route) {
|
||||
setWebUri("https://lobste.rs/")
|
||||
NetworkPosts(
|
||||
lazyPagingItems = hottestPosts,
|
||||
listState = hottestListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
isPostRead = viewModel::isPostRead,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.Newest.route) {
|
||||
setWebUri("https://lobste.rs/")
|
||||
NetworkPosts(
|
||||
lazyPagingItems = newestPosts,
|
||||
listState = newestListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
isPostRead = viewModel::isPostRead,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.Saved.route) {
|
||||
setWebUri(null)
|
||||
DatabasePosts(items = savedPosts, listState = savedListState, postActions = postActions)
|
||||
}
|
||||
composable(
|
||||
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"
|
||||
}
|
||||
setWebUri("https://lobste.rs/s/$postId")
|
||||
CommentsPage(
|
||||
postId = postId,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
)
|
||||
}
|
||||
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"
|
||||
}
|
||||
setWebUri("https://lobste.rs/u/$username")
|
||||
UserProfile(username = username, getProfile = viewModel::getUserProfile)
|
||||
}
|
||||
composable(route = Destinations.Settings.route) {
|
||||
SettingsScreen(
|
||||
context = context,
|
||||
openLibrariesScreen = { navController.navigate(Destinations.AboutLibraries.route) },
|
||||
importPosts = viewModel::importPosts,
|
||||
exportPostsAsJson = viewModel::exportPostsAsJson,
|
||||
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.AboutLibraries.route) {
|
||||
LibrariesContainer(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||
import dev.msfjarvis.claw.android.ui.lists.SearchList
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import dev.msfjarvis.claw.android.ui.rememberPostActions
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
urlLauncher: UrlLauncher,
|
||||
htmlConverter: HTMLConverter,
|
||||
setWebUri: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val postActions = rememberPostActions(urlLauncher, navController, viewModel)
|
||||
val listState = rememberLazyListState()
|
||||
Scaffold(modifier = modifier) { paddingValues ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Destinations.Search.route,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
) {
|
||||
composable(route = Destinations.Search.route) {
|
||||
setWebUri("https://lobste.rs/search")
|
||||
SearchList(
|
||||
items = viewModel.searchResults,
|
||||
listState = listState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
postActions = postActions,
|
||||
searchQuery = viewModel.searchQuery,
|
||||
setSearchQuery = { query -> viewModel.searchQuery = query },
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Destinations.Comments.route,
|
||||
arguments = listOf(navArgument("postId") { type = NavType.StringType }),
|
||||
) { backStackEntry ->
|
||||
val postId =
|
||||
requireNotNull(backStackEntry.arguments?.getString("postId")) {
|
||||
"Navigating to ${Destinations.Comments.route} without necessary 'postId' argument"
|
||||
}
|
||||
setWebUri("https://lobste.rs/s/$postId")
|
||||
CommentsPage(
|
||||
postId = postId,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright © 2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
||||
import androidx.compose.material.icons.filled.Bookmarks
|
||||
import androidx.compose.material.icons.filled.Code
|
||||
import androidx.compose.material.icons.filled.ImportExport
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val JSON_MIME_TYPE = "application/json"
|
||||
private const val HTML_MIME_TYPE = "application/html"
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
context: Context,
|
||||
openLibrariesScreen: () -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
exportPostsAsJson: suspend (OutputStream) -> Unit,
|
||||
exportPostsAsHtml: suspend (OutputStream) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
Column {
|
||||
ListItem(
|
||||
headlineContent = { Text("Libraries") },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.LibraryBooks,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = openLibrariesScreen),
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text("Data transfer") },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ImportExport,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ImportPosts(context, coroutineScope, snackbarHostState, importPosts)
|
||||
ExportPosts(
|
||||
context,
|
||||
coroutineScope,
|
||||
snackbarHostState,
|
||||
exportPostsAsJson,
|
||||
exportPostsAsHtml,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.ImportPosts(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val importAction =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
importPosts(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully imported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { importAction.launch(JSON_MIME_TYPE) },
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
modifier = modifier.fillMaxWidth(0.30f).weight(0.5f),
|
||||
) {
|
||||
Text(text = "Import")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.ExportPosts(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
exportPostsAsJson: suspend (OutputStream) -> Unit,
|
||||
exportPostsAsHtml: suspend (OutputStream) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val jsonExportAction =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(JSON_MIME_TYPE)) { uri
|
||||
->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openOutputStream(uri)?.use { stream ->
|
||||
exportPostsAsJson(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
val htmlExportAction =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(HTML_MIME_TYPE)) { uri
|
||||
->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openOutputStream(uri)?.use { stream ->
|
||||
exportPostsAsHtml(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
OutlinedButton(
|
||||
onClick = { expanded = true },
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
modifier = modifier.fillMaxWidth(0.30f).weight(0.5f),
|
||||
) {
|
||||
Text(text = "Export")
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("JSON") },
|
||||
onClick = {
|
||||
expanded = false
|
||||
jsonExportAction.launch("claw-export.json")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Filled.Code, contentDescription = "Export as JSON")
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Bookmarks") },
|
||||
onClick = {
|
||||
expanded = false
|
||||
htmlExportAction.launch("claw-export.html")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Bookmarks,
|
||||
contentDescription = "Export as browser bookmarks",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows a Snackbar but dismisses any existing ones first. */
|
||||
private suspend fun SnackbarHostState.showSnackbarDismissing(text: String) {
|
||||
currentSnackbarData?.dismiss()
|
||||
showSnackbar(text)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.api.injection.BaseUrl
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
/** Helper for extracting CSRF token for authenticated requests to https://lobste.rs. */
|
||||
class CSRFRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
@IODispatcher private val dispatcher: CoroutineDispatcher,
|
||||
@BaseUrl private val url: String,
|
||||
) {
|
||||
suspend fun extractToken(): String? {
|
||||
val request = Request.Builder().url(url).build()
|
||||
return withContext(dispatcher) {
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
val doc = Jsoup.parse(response.body?.string() ?: return@use null)
|
||||
val element = doc.select("meta[name=\"csrf-token\"]").first() ?: return@use null
|
||||
return@use element.attr("content")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,36 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
|
||||
import com.slack.eithernet.ApiResult.Failure
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import com.squareup.anvil.annotations.optional.ForScope
|
||||
import dev.msfjarvis.claw.android.glance.SavedPostsWidget
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
|
||||
import dev.msfjarvis.claw.android.paging.SearchPagingSource
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.core.injection.MainDispatcher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import java.io.IOException
|
||||
@@ -30,15 +38,18 @@ import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.time.LocalDateTime
|
||||
import java.time.Month
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@ContributesViewModel
|
||||
@@ -46,15 +57,17 @@ class ClawViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val api: LobstersApi,
|
||||
private val searchApi: LobstersSearchApi,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val commentsRepository: CommentsRepository,
|
||||
private val readPostsRepository: ReadPostsRepository,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val linkMetadataRepository: LinkMetadataRepository,
|
||||
private val dataTransferRepository: DataTransferRepository,
|
||||
private val pagingSourceFactory: LobstersPagingSource.Factory,
|
||||
private val searchPagingSourceFactory: SearchPagingSource.Factory,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
|
||||
@ForScope(ApplicationScope::class) context: Context,
|
||||
) : AndroidViewModel(context as Application) {
|
||||
private val hottestPostsPager =
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
pagingSourceFactory.create(api::getHottestPosts)
|
||||
@@ -65,7 +78,7 @@ constructor(
|
||||
}
|
||||
private val searchResultsPager =
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
searchPagingSourceFactory.create { searchApi.searchPosts(searchQuery, it) }
|
||||
searchPagingSourceFactory.create { searchQuery }
|
||||
}
|
||||
|
||||
val hottestPosts
|
||||
@@ -74,24 +87,51 @@ constructor(
|
||||
val newestPosts
|
||||
get() = newestPostsPager.flow
|
||||
|
||||
private val savedPostsFlow
|
||||
val savedPosts
|
||||
get() = savedPostsRepository.savedPosts
|
||||
|
||||
val savedPosts
|
||||
get() = savedPostsFlow.map(::mapSavedPosts)
|
||||
val savedPostsByMonth
|
||||
get() = savedPosts.map(::mapSavedPosts)
|
||||
|
||||
val searchResults
|
||||
get() = searchResultsPager.flow
|
||||
|
||||
var searchQuery by mutableStateOf("")
|
||||
|
||||
private fun mapSavedPosts(items: List<SavedPost>): ImmutableMap<Month, List<SavedPost>> {
|
||||
val sorted = items.sortedByDescending { post -> post.createdAt.toLocalDateTime() }
|
||||
return sorted.groupBy { post -> post.createdAt.toLocalDateTime().month }.toImmutableMap()
|
||||
private val _savedPostsMutex = Mutex()
|
||||
private var _savedPosts = emptyList<String>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
savedPosts.collectLatest {
|
||||
_savedPostsMutex.withLock { _savedPosts = it.map(SavedPost::shortId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun isPostSaved(post: SavedPost): Boolean {
|
||||
return savedPostsFlow.first().any { savedPost -> savedPost.shortId == post.shortId }
|
||||
private fun mapSavedPosts(items: List<SavedPost>): ImmutableMap<String, List<SavedPost>> {
|
||||
val sorted =
|
||||
items.sortedWith { post1, post2 ->
|
||||
val post1Date = post1.createdAt.toLocalDateTime()
|
||||
val post2Date = post2.createdAt.toLocalDateTime()
|
||||
if (post2Date.isBefore(post1Date)) {
|
||||
-1
|
||||
} else if (post2Date.isAfter(post1Date)) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
return sorted
|
||||
.groupBy { post ->
|
||||
val time = post.createdAt.toLocalDateTime()
|
||||
"${time.month.name.lowercase().capitalize(Locale.current)} ${time.year}"
|
||||
}
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
fun isPostSaved(post: SavedPost): Boolean {
|
||||
return _savedPosts.contains(post.shortId)
|
||||
}
|
||||
|
||||
fun toggleSave(post: SavedPost) {
|
||||
@@ -102,6 +142,10 @@ constructor(
|
||||
} else {
|
||||
savedPostsRepository.savePost(post)
|
||||
}
|
||||
val newPosts = savedPosts.first()
|
||||
withContext(mainDispatcher) {
|
||||
SavedPostsWidget(newPosts.take(50)).updateAll(getApplication())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +188,17 @@ constructor(
|
||||
|
||||
suspend fun importPosts(input: InputStream) = dataTransferRepository.importPosts(input)
|
||||
|
||||
suspend fun exportPosts(output: OutputStream) = dataTransferRepository.exportPosts(output)
|
||||
suspend fun exportPostsAsJson(output: OutputStream) =
|
||||
dataTransferRepository.exportPostsAsJson(output)
|
||||
|
||||
suspend fun exportPostsAsHtml(output: OutputStream) =
|
||||
dataTransferRepository.exportPostsAsHTML(output)
|
||||
|
||||
fun markPostAsRead(postId: String) {
|
||||
viewModelScope.launch { readPostsRepository.markRead(postId) }
|
||||
}
|
||||
|
||||
suspend fun isPostRead(postId: String) = readPostsRepository.isRead(postId)
|
||||
|
||||
/**
|
||||
* Parses a given [String] into a [LocalDateTime]. This method is only intended to be used for
|
||||
@@ -152,6 +206,7 @@ constructor(
|
||||
* solution.
|
||||
*/
|
||||
private fun String.toLocalDateTime(): LocalDateTime {
|
||||
if (isEmpty()) return LocalDateTime.now(ZoneId.systemDefault())
|
||||
return LocalDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(this))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.database.LobstersDatabase
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.database.local.PostCommentsQueries
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -17,10 +17,9 @@ import kotlinx.coroutines.withContext
|
||||
class CommentsRepository
|
||||
@Inject
|
||||
constructor(
|
||||
database: LobstersDatabase,
|
||||
private val postCommentsQueries: PostCommentsQueries,
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val postCommentsQueries = database.postCommentsQueries
|
||||
|
||||
suspend fun getSeenComments(postId: String) =
|
||||
withContext(dbDispatcher) { postCommentsQueries.getCommentIds(postId).executeAsOneOrNull() }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -8,11 +8,13 @@ package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.database.LobstersDatabase
|
||||
import dev.msfjarvis.claw.database.SavedPostSerializer
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.database.local.SavedPostQueries
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -26,29 +28,72 @@ import kotlinx.serialization.json.encodeToStream
|
||||
class DataTransferRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val database: LobstersDatabase,
|
||||
private val json: Json,
|
||||
private val savedPostQueries: SavedPostQueries,
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val serializer = ListSerializer(SavedPostSerializer)
|
||||
private val savedPostQueries = database.savedPostQueries
|
||||
|
||||
suspend fun importPosts(input: InputStream) {
|
||||
val posts: List<SavedPost> =
|
||||
withContext(ioDispatcher) {
|
||||
json.decodeFromStream(
|
||||
serializer,
|
||||
input,
|
||||
)
|
||||
}
|
||||
withContext(ioDispatcher) { json.decodeFromStream(serializer, input) }
|
||||
withContext(dbDispatcher) {
|
||||
database.transaction { posts.forEach(savedPostQueries::insertOrReplacePost) }
|
||||
savedPostQueries.transaction { posts.forEach(savedPostQueries::insertOrReplacePost) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun exportPosts(output: OutputStream) {
|
||||
suspend fun exportPostsAsJson(output: OutputStream) {
|
||||
val posts = withContext(dbDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
|
||||
withContext(ioDispatcher) { json.encodeToStream(serializer, posts, output) }
|
||||
}
|
||||
|
||||
suspend fun exportPostsAsHTML(output: OutputStream) {
|
||||
fun computeTimestamp(post: SavedPost): Long {
|
||||
val temporal = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(post.createdAt)
|
||||
val instant = Instant.from(temporal)
|
||||
return instant.toEpochMilli()
|
||||
}
|
||||
|
||||
val posts = withContext(dbDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
|
||||
val header =
|
||||
"""
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<!-- This is an automatically generated file.
|
||||
It will be read and overwritten.
|
||||
DO NOT EDIT! -->
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
"""
|
||||
.trimIndent()
|
||||
val html = buildString {
|
||||
append(header)
|
||||
append("<DD><p>\n")
|
||||
for (post in posts) {
|
||||
append(
|
||||
"""
|
||||
<DT><A HREF="${post.url.ifEmpty { post.commentsUrl }}" ADD_DATE="${computeTimestamp(post)}" PRIVATE="0" TAGS="${post.tags.joinToString(",")}">${post.title}</A>
|
||||
<DD>${post.title}
|
||||
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
append(
|
||||
"""
|
||||
<DT><A HREF="https://example.com/" ADD_DATE="0" PRIVATE="0" TAGS="delete,me,pls">Padding post</A>
|
||||
<DD>Linkding ignores the last entry so this pads the difference for imports
|
||||
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
append("</DD></p>\n")
|
||||
}
|
||||
withContext(ioDispatcher) {
|
||||
val writer = output.bufferedWriter()
|
||||
writer.write(html)
|
||||
writer.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.database.local.ReadPostsQueries
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ReadPostsRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val readPostsQueries: ReadPostsQueries,
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
|
||||
suspend fun markRead(postId: String) {
|
||||
withContext(dbDispatcher) { readPostsQueries.markRead(postId) }
|
||||
}
|
||||
|
||||
suspend fun isRead(postId: String): Boolean =
|
||||
withContext(dbDispatcher) { readPostsQueries.isRead(postId).executeAsOneOrNull() != null }
|
||||
}
|
||||
@@ -9,8 +9,8 @@ package dev.msfjarvis.claw.android.viewmodel
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.database.LobstersDatabase
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.database.local.SavedPostQueries
|
||||
import io.github.aakira.napier.Napier
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -19,10 +19,9 @@ import kotlinx.coroutines.withContext
|
||||
class SavedPostsRepository
|
||||
@Inject
|
||||
constructor(
|
||||
database: LobstersDatabase,
|
||||
private val savedPostQueries: SavedPostQueries,
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val savedPostQueries = database.savedPostQueries
|
||||
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
|
||||
|
||||
suspend fun savePost(post: SavedPost) {
|
||||
|
||||
@@ -7,16 +7,18 @@
|
||||
package dev.msfjarvis.claw.android.work
|
||||
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.deliveryhero.whetstone.ForScope
|
||||
import com.deliveryhero.whetstone.worker.ContributesWorker
|
||||
import com.deliveryhero.whetstone.worker.WorkerScope
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import com.squareup.anvil.annotations.optional.ForScope
|
||||
import dev.msfjarvis.claw.android.glance.SavedPostsWidget
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.common.posts.toDbModel
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
@@ -40,8 +42,9 @@ constructor(
|
||||
.first()
|
||||
.map { post -> lobstersApi.getPostDetails(post.shortId) }
|
||||
.filterIsInstance<Success<LobstersPostDetails>>()
|
||||
.map { result -> result.value.toDbModel() }
|
||||
.map { result -> result.value.toSavedPost() }
|
||||
.let { savedPostsRepository.savePosts(it) }
|
||||
SavedPostsWidget(savedPostsRepository.savedPosts.first().take(50)).updateAll(applicationContext)
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--
|
||||
~ Copyright © 2024 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:colorControlNormal"
|
||||
android:alpha="0.6"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21.99,4c0,-1.1 -0.89,-2 -1.99,-2L4,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h14l4,4 -0.01,-18zM20,4v13.17L18.83,16L4,16L4,4h16zM6,12h12v2L6,14zM6,9h12v2L6,11zM6,6h12v2L6,8z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,20 @@
|
||||
<!--
|
||||
~ Copyright © 2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1080"
|
||||
android:viewportHeight="1080">
|
||||
<group android:scaleX="0.7"
|
||||
android:scaleY="0.7"
|
||||
android:translateX="162"
|
||||
android:translateY="162">
|
||||
<path
|
||||
android:pathData="M431.3,341.2C435.9,337 580.3,236 710.5,223.8C747.3,220.3 790.7,254.7 833.2,312.5C875.9,370.7 892.9,433.4 875.9,443C863.2,450.2 838.6,407.4 758.8,396.3C743.4,394.2 730.4,393.4 719.4,393.5C738.5,423.4 745.6,451.1 737.2,458C733.1,461.4 673.6,438.7 612.9,436.5C570.1,435 535.2,440.9 506.7,451.3C496.9,454.9 486,453.8 477.2,448.3C444.5,427.9 424.2,406.2 418.4,376.2C415.8,362.8 420.9,349.4 431.3,341.2ZM274.7,394.8C285.8,370.1 312.2,325.9 346.3,305.4C383.6,282.9 335,390.3 427.3,489.8C519.5,589.3 586.5,623.8 655.1,581.5C723.7,539.2 746.5,518.1 753,518.7C764,519.6 772.9,536.1 764.4,589.5C763,598.2 761.3,606.5 759.3,614.2C828.7,575 871.7,516.3 889,520C912,524.8 918.3,617.8 852.1,721C811.8,784 768.8,830.7 741.4,857.5C724.3,874 699.7,880 677,873.1C512,822.6 315.5,733.8 202.9,652C193.8,645.2 186.8,635.9 183,625.1C171.8,593.7 161.3,548.1 168.5,495.3C179.5,414.2 222.7,352.9 245.2,335.9C261.9,323.2 267.1,362.4 274.7,394.8Z"
|
||||
android:fillColor="#FF4529"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -5,7 +5,7 @@
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 44 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 44 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 46 B |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 52 B |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 52 B |
|
Before Width: | Height: | Size: 6.1 KiB |
@@ -5,5 +5,5 @@
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
<bool name="use_light_status_bar">false</bool>
|
||||
<color name="ic_launcher_background">#540C00</color>
|
||||
</resources>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Copyright © 2021-2024 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
@@ -7,19 +7,11 @@
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Claw.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">#FF6C0000</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_foreground</item>
|
||||
<item name="windowSplashScreenBackground">@color/ic_launcher_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
||||
<item name="windowSplashScreenAnimationDuration">1000</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.Claw</item>
|
||||
</style>
|
||||
|
||||
<style name="Platform.Theme.Claw" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
|
||||
<style name="Base.Theme.Claw" parent="Platform.Theme.Claw">
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Claw" parent="Base.Theme.Claw">
|
||||
<item name="android:windowLightStatusBar">@bool/use_light_status_bar</item>
|
||||
</style>
|
||||
<style name="Theme.Claw" parent="android:Theme.Material.NoActionBar" />
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
~ Copyright © 2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="349dp"
|
||||
android:minHeight="110dp"
|
||||
android:minResizeWidth="349dp"
|
||||
android:minResizeHeight="110dp"
|
||||
android:resizeMode="vertical|horizontal"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen" />
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class CSRFRepositoryTest {
|
||||
@Test
|
||||
fun `correctly extracts CSRF token`() = runTest {
|
||||
val repo =
|
||||
CSRFRepository(
|
||||
OkHttpClient.Builder().build(),
|
||||
Dispatchers.Default,
|
||||
server.url("/").toString(),
|
||||
)
|
||||
assertThat(repo.extractToken())
|
||||
.isEqualTo(
|
||||
"OZWykgFemPVeOSNmB53-ccKXe458X7xCInO1-qzFU6nk_9RCSrSQqS9OPmA5_pyy8qD3IYAIZ7XfAM3gdhJpkQ"
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val server = MockWebServer()
|
||||
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setup() {
|
||||
val dispatcher =
|
||||
object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
return when (val path = request.path) {
|
||||
"/" ->
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody(
|
||||
javaClass.classLoader!!
|
||||
.getResourceAsStream("csrf_page.html")
|
||||
.readAllBytes()
|
||||
.decodeToString(),
|
||||
)
|
||||
else -> error("Invalid path: $path")
|
||||
}
|
||||
}
|
||||
}
|
||||
server.dispatcher = dispatcher
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
|
||||
#
|
||||
versioning-plugin.versionCode=13100
|
||||
versioning-plugin.versionName=1.31.0
|
||||
versioning-plugin.versionCode=13900
|
||||
versioning-plugin.versionName=1.39.0
|
||||
|
||||
@@ -29,5 +29,6 @@ dependencies {
|
||||
testImplementation(testFixtures(libs.eithernet))
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.kotlinx.serialization.json)
|
||||
testImplementation(libs.retrofit.kotlinxSerializationConverter)
|
||||
addTestDependencies(project)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.api
|
||||
|
||||
@JvmInline value class CSRFToken(val value: String)
|
||||
@@ -12,23 +12,24 @@ import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
/** Simple interface defining an API for lobste.rs */
|
||||
interface LobstersApi {
|
||||
|
||||
@GET("hottest.json")
|
||||
suspend fun getHottestPosts(@Query("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
@GET("page/{page}.json")
|
||||
suspend fun getHottestPosts(@Path("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
|
||||
@GET("newest.json")
|
||||
suspend fun getNewestPosts(@Query("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
@GET("newest/page/{page}.json")
|
||||
suspend fun getNewestPosts(@Path("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
|
||||
@GET("s/{postId}.json")
|
||||
suspend fun getPostDetails(@Path("postId") postId: String): ApiResult<LobstersPostDetails, Unit>
|
||||
|
||||
@GET("u/{username}.json")
|
||||
@GET("~{username}.json")
|
||||
suspend fun getUser(@Path("username") username: String): ApiResult<User, Unit>
|
||||
|
||||
@GET("/") suspend fun getCSRFToken(): ApiResult<CSRFToken, Unit>
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://lobste.rs"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.api.converters
|
||||
|
||||
import dev.msfjarvis.claw.api.CSRFToken
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import java.lang.reflect.Type
|
||||
import okhttp3.ResponseBody
|
||||
import org.jsoup.Jsoup
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
|
||||
object CSRFTokenConverter : Converter<ResponseBody, CSRFToken> {
|
||||
override fun convert(value: ResponseBody): CSRFToken {
|
||||
val token =
|
||||
Jsoup.parse(value.string(), LobstersApi.BASE_URL)
|
||||
.select("meta[name=\"csrf-token\"]")
|
||||
.first()!!
|
||||
.attr("content")
|
||||
return CSRFToken(token)
|
||||
}
|
||||
|
||||
object Factory : Converter.Factory() {
|
||||
override fun responseBodyConverter(
|
||||
type: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit,
|
||||
): Converter<ResponseBody, CSRFToken> {
|
||||
return CSRFTokenConverter
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -51,7 +51,7 @@ object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
|
||||
private fun getCommentsData(elem: Elements): Pair<Int, String> {
|
||||
val linkElement = elem.select("a")
|
||||
val countString = linkElement.text().trimStart().substringBefore(" ")
|
||||
val commentsUrl = linkElement.attr("href")
|
||||
val commentsUrl = LobstersApi.BASE_URL + linkElement.attr("href")
|
||||
return (countString.toIntOrNull() ?: 0) to commentsUrl
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
|
||||
override fun responseBodyConverter(
|
||||
type: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
retrofit: Retrofit,
|
||||
): Converter<ResponseBody, List<LobstersPost>> {
|
||||
return SearchConverter
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -16,7 +16,9 @@ import dagger.multibindings.IntKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.api.converters.CSRFTokenConverter
|
||||
import dev.msfjarvis.claw.api.converters.SearchConverter
|
||||
import javax.inject.Named
|
||||
import javax.inject.Qualifier
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.CallAdapter
|
||||
@@ -39,7 +41,7 @@ object RetrofitModule {
|
||||
client: OkHttpClient,
|
||||
converterFactories: Map<Int, @JvmSuppressWildcards Converter.Factory>,
|
||||
callAdapterFactories: Map<Int, @JvmSuppressWildcards CallAdapter.Factory>,
|
||||
@BaseUrl baseUrl: String,
|
||||
@Named("LobstersURL") baseUrl: String,
|
||||
): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
@@ -55,7 +57,7 @@ object RetrofitModule {
|
||||
client: OkHttpClient,
|
||||
@SearchApi converterFactories: List<@JvmSuppressWildcards Converter.Factory>,
|
||||
callAdapterFactories: Map<Int, @JvmSuppressWildcards CallAdapter.Factory>,
|
||||
@BaseUrl baseUrl: String,
|
||||
@Named("LobstersURL") baseUrl: String,
|
||||
): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
@@ -81,18 +83,18 @@ object RetrofitModule {
|
||||
@IntoMap
|
||||
fun provideApiResultCallAdapter(): CallAdapter.Factory = ApiResultCallAdapterFactory
|
||||
|
||||
@Provides
|
||||
@IntKey(Int.MAX_VALUE)
|
||||
@IntoMap
|
||||
fun provideCSRFTokenConverter(): Converter.Factory = CSRFTokenConverter.Factory
|
||||
|
||||
@Provides
|
||||
@SearchApi
|
||||
fun provideConverters(): List<@JvmSuppressWildcards Converter.Factory> =
|
||||
listOf(
|
||||
ApiResultConverterFactory,
|
||||
SearchConverter.Factory,
|
||||
)
|
||||
listOf(ApiResultConverterFactory, SearchConverter.Factory)
|
||||
|
||||
@Provides @BaseUrl fun provideBaseUrl(): String = LobstersApi.BASE_URL
|
||||
@Provides @Named("LobstersURL") fun provideLobstersUrl(): String = LobstersApi.BASE_URL
|
||||
}
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class BaseUrl
|
||||
|
||||
/** Internal Dagger [Qualifier] to identify dependencies exclusive to [LobstersSearchApi]. */
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) private annotation class SearchApi
|
||||
|
||||
@@ -40,7 +40,7 @@ class ApiTest {
|
||||
fun `post details with comments`() = runTest {
|
||||
val postDetails = api.getPostDetails("tdfoqh")
|
||||
assertIs<Success<LobstersPostDetails>>(postDetails)
|
||||
assertThat(postDetails.value.comments).hasSize(7)
|
||||
assertThat(postDetails.value.comments).hasSize(10)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -49,4 +49,14 @@ class ApiTest {
|
||||
assertIs<Success<User>>(user)
|
||||
assertThat(user.value.username).isEqualTo("msfjarvis")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retrieve CSRF token`() = runTest {
|
||||
val token = api.getCSRFToken()
|
||||
assertIs<Success<CSRFToken>>(token)
|
||||
assertThat(token.value.value)
|
||||
.isEqualTo(
|
||||
"OZWykgFemPVeOSNmB53-ccKXe458X7xCInO1-qzFU6nk_9RCSrSQqS9OPmA5_pyy8qD3IYAIZ7XfAM3gdhJpkQ"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ package dev.msfjarvis.claw.api
|
||||
import com.slack.eithernet.ApiResult.Companion.success
|
||||
import com.slack.eithernet.test.EitherNetController
|
||||
import com.slack.eithernet.test.enqueue
|
||||
import dev.msfjarvis.claw.api.converters.CSRFTokenConverter
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.User
|
||||
@@ -16,6 +17,8 @@ import dev.msfjarvis.claw.util.TestUtils.getResource
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.ResponseBody
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
class ApiWrapper(controller: EitherNetController<LobstersApi>) {
|
||||
@@ -35,5 +38,12 @@ class ApiWrapper(controller: EitherNetController<LobstersApi>) {
|
||||
controller.enqueue(LobstersApi::getHottestPosts) { success(hottest) }
|
||||
controller.enqueue(LobstersApi::getPostDetails) { success(postDetails) }
|
||||
controller.enqueue(LobstersApi::getUser) { success(user) }
|
||||
controller.enqueue(LobstersApi::getCSRFToken) {
|
||||
success(
|
||||
CSRFTokenConverter.convert(
|
||||
ResponseBody.create(MediaType.get("text/html"), getResource("csrf_page.html"))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class SearchApiTest {
|
||||
url = "https://xenodium.com/chatgpt-visits-the-emacs-doctor/",
|
||||
createdAt = "",
|
||||
commentCount = 3,
|
||||
commentsUrl = "/s/gjlsdg/chatgpt_visits_emacs_doctor",
|
||||
commentsUrl = "https://lobste.rs/s/gjlsdg/chatgpt_visits_emacs_doctor",
|
||||
submitter = User("xenodium", "", null, "/avatars/xenodium-16.png", ""),
|
||||
tags = listOf("ai", "emacs"),
|
||||
description = "",
|
||||
@@ -49,7 +49,7 @@ class SearchApiTest {
|
||||
url = "https://mstack.nl/blog/20230623-chatgpt-question-pdf-document/",
|
||||
createdAt = "",
|
||||
commentCount = 0,
|
||||
commentsUrl = "/s/astcqf/implementing_question_answering_system",
|
||||
commentsUrl = "https://lobste.rs/s/astcqf/implementing_question_answering_system",
|
||||
submitter = User("asteroid", "", null, "/avatars/asteroid-16.png", ""),
|
||||
tags = listOf("ai"),
|
||||
description = "",
|
||||
|
||||
@@ -20,9 +20,8 @@ object TestUtils {
|
||||
return String(file.readBytes())
|
||||
}
|
||||
|
||||
inline fun <reified T> assertIs(value: Any?): T {
|
||||
inline fun <reified T> assertIs(value: Any?) {
|
||||
contract { returns() implies (value is T) }
|
||||
assertThat(value).isInstanceOf(T::class.java)
|
||||
return value as T
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,15 @@
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.test)
|
||||
id("dev.msfjarvis.claw.android-common")
|
||||
id(libs.plugins.android.test.get().pluginId)
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
alias(libs.plugins.baselineprofile)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.msfjarvis.claw.benchmark"
|
||||
|
||||
compileSdk = 34
|
||||
defaultConfig {
|
||||
minSdk = 28
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -30,23 +31,22 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
managedDevices {
|
||||
devices {
|
||||
create<ManagedVirtualDevice>("api31") {
|
||||
device = "Pixel 6"
|
||||
apiLevel = 31
|
||||
systemImageSource = "aosp"
|
||||
}
|
||||
}
|
||||
testOptions.managedDevices.devices {
|
||||
create<ManagedVirtualDevice>("api31") {
|
||||
device = "Pixel 6"
|
||||
apiLevel = 31
|
||||
systemImageSource = "aosp"
|
||||
}
|
||||
}
|
||||
|
||||
targetProjectPath = ":android"
|
||||
experimentalProperties["android.experimental.self-instrumenting"] = true
|
||||
}
|
||||
|
||||
androidComponents { beforeVariants(selector().all()) { it.enable = it.buildType == "benchmark" } }
|
||||
baselineProfile {
|
||||
managedDevices += "api31"
|
||||
useConnectedDevices = false
|
||||
enableEmulatorDisplay = false
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.benchmark.macro.junit4)
|
||||
|
||||
@@ -28,6 +28,7 @@ class BaselineProfileGenerator {
|
||||
baselineProfileRule.collect(
|
||||
packageName = PACKAGE_NAME,
|
||||
maxIterations = 8,
|
||||
includeInStartupProfile = true,
|
||||
) {
|
||||
device.executeShellCommand("pm clear $PACKAGE_NAME")
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import androidx.test.uiautomator.UiDevice
|
||||
import androidx.test.uiautomator.UiObject2
|
||||
import androidx.test.uiautomator.Until
|
||||
|
||||
const val PACKAGE_NAME = "dev.msfjarvis.claw.android.benchmark"
|
||||
const val PACKAGE_NAME = "dev.msfjarvis.claw.android"
|
||||
private const val AWAIT_TIMEOUT = 10_000L
|
||||
private const val SAVE_BUTTON_ID = "save_button"
|
||||
private const val NAV_ID_HOTTEST = "HOTTEST"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
plugins { `kotlin-dsl` }
|
||||
|
||||
kotlin.jvmToolchain { languageVersion.set(JavaLanguageVersion.of(17)) }
|
||||
kotlin.jvmToolchain(17)
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
@@ -54,10 +54,6 @@ gradlePlugin {
|
||||
id = "dev.msfjarvis.claw.versioning-plugin"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.versioning.VersioningPlugin"
|
||||
}
|
||||
register("versions") {
|
||||
id = "dev.msfjarvis.claw.versions"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.DependencyUpdatesPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +61,10 @@ dependencies {
|
||||
implementation(libs.build.agp)
|
||||
implementation(libs.build.cachefix)
|
||||
implementation(libs.build.kotlin.gradle)
|
||||
implementation(libs.build.r8)
|
||||
implementation(libs.build.semver)
|
||||
implementation(libs.build.sentry)
|
||||
implementation(libs.build.spotless)
|
||||
implementation(libs.build.vcu)
|
||||
implementation(libs.build.versions)
|
||||
|
||||
// Expose the generated version catalog API to the plugin.
|
||||
implementation(files(libs::class.java.superclass.protectionDomain.codeSource.location))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -8,31 +8,33 @@
|
||||
|
||||
rootProject.name = "build-logic"
|
||||
|
||||
pluginManagement {
|
||||
plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" }
|
||||
repositories {
|
||||
exclusiveContent {
|
||||
forRepository { gradlePluginPortal() }
|
||||
filter {
|
||||
includeModule(
|
||||
"org.gradle.toolchains.foojay-resolver-convention",
|
||||
"org.gradle.toolchains.foojay-resolver-convention.gradle.plugin",
|
||||
)
|
||||
includeModule("org.gradle.toolchains", "foojay-resolver")
|
||||
includeModule("org.gradle.kotlin.kotlin-dsl", "org.gradle.kotlin.kotlin-dsl.gradle.plugin")
|
||||
includeModule("org.gradle.kotlin", "gradle-kotlin-dsl-plugins")
|
||||
}
|
||||
}
|
||||
mavenCentral { mavenContent { releasesOnly() } }
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
exclusiveContent {
|
||||
forRepository(::google)
|
||||
filter {
|
||||
includeGroup("androidx.databinding")
|
||||
includeGroup("com.android")
|
||||
includeGroup("com.android.tools.analytics-library")
|
||||
includeGroup("com.android.tools.build")
|
||||
includeGroup("com.android.tools.build.jetifier")
|
||||
includeGroup("com.android.databinding")
|
||||
includeGroup("com.android.tools.ddms")
|
||||
includeGroup("com.android.tools.layoutlib")
|
||||
includeGroup("com.android.tools.lint")
|
||||
includeGroup("com.android.tools.utp")
|
||||
includeGroupByRegex("com.android.*")
|
||||
includeGroup("com.google.testing.platform")
|
||||
includeModule("com.android.tools", "annotations")
|
||||
includeModule("com.android.tools", "common")
|
||||
includeModule("com.android.tools", "desugar_jdk_libs")
|
||||
includeModule("com.android.tools", "desugar_jdk_libs_configuration")
|
||||
includeModule("com.android.tools", "dvlib")
|
||||
includeModule("com.android.tools", "play-sdk-proto")
|
||||
includeModule("com.android.tools", "repository")
|
||||
includeModule("com.android.tools", "sdklib")
|
||||
includeModule("com.android.tools", "sdk-common")
|
||||
}
|
||||
}
|
||||
exclusiveContent {
|
||||
@@ -43,11 +45,7 @@ dependencyResolutionManagement {
|
||||
includeModule("gradle.plugin.org.gradle.android", "android-cache-fix-gradle-plugin")
|
||||
}
|
||||
}
|
||||
exclusiveContent {
|
||||
forRepository { maven("https://storage.googleapis.com/r8-releases/raw/main") }
|
||||
filter { includeModule("com.android.tools", "r8") }
|
||||
}
|
||||
mavenCentral()
|
||||
mavenCentral { mavenContent { releasesOnly() } }
|
||||
}
|
||||
versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } }
|
||||
}
|
||||
|
||||
@@ -15,14 +15,13 @@ import com.android.build.api.variant.HasUnitTestBuilder
|
||||
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
||||
import com.android.build.gradle.BaseExtension
|
||||
import dev.msfjarvis.claw.gradle.LintConfig.configureLint
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.gradle.android.AndroidCacheFixPlugin
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.findByType
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
class AndroidCommonPlugin : Plugin<Project> {
|
||||
|
||||
@@ -59,9 +58,9 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
}
|
||||
project.extensions.findByType<ApplicationExtension>()?.lint?.configureLint(project)
|
||||
project.extensions.findByType<LibraryExtension>()?.lint?.configureLint(project)
|
||||
val catalog = project.extensions.getByType<VersionCatalogsExtension>()
|
||||
val libs = catalog.named("libs")
|
||||
project.dependencies.addProvider("lintChecks", libs.findLibrary("slack-compose-lints").get())
|
||||
val libs = project.extensions.getByName("libs") as LibrariesForLibs
|
||||
project.dependencies.addProvider("lintChecks", libs.slack.compose.lints)
|
||||
project.dependencies.addProvider("lintChecks", libs.slack.lints)
|
||||
}
|
||||
|
||||
private fun Project.configureSlimTests() {
|
||||
@@ -73,7 +72,7 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
beforeVariants(selector().all()) {
|
||||
if (it.name == "debug") return@beforeVariants
|
||||
(it as HasUnitTestBuilder).enableUnitTest = false
|
||||
it.enableAndroidTest = false
|
||||
it.androidTest.enable = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +81,7 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
beforeVariants(selector().all()) {
|
||||
if (it.name == "debug") return@beforeVariants
|
||||
(it as HasUnitTestBuilder).enableUnitTest = false
|
||||
it.enableAndroidTest = false
|
||||
it.androidTest.enable = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||