Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f00f185f6d | ||
|
|
a08ffb903c | ||
|
|
4eaf25e189 | ||
|
|
109176768c | ||
|
|
cf0ef021d8 | ||
|
|
59ba98174c | ||
|
|
afe78aa82f | ||
|
|
0a70924cab | ||
|
|
ae362c2e03 | ||
|
|
83ca0e6167 | ||
|
|
71977c5b2c | ||
|
|
047d6badb0 | ||
|
|
5c0b5bb4f7 | ||
|
|
7762733da2 | ||
|
|
9f705ca02a | ||
|
|
6c5b54587e | ||
|
|
e318ea3a35 | ||
|
|
5ba0c9a544 | ||
|
|
85fc5cac21 | ||
|
|
86a40d2e84 | ||
|
|
557407347b | ||
|
|
b0467aa36c | ||
|
|
d29b4e2ee2 | ||
|
|
64fb1235d5 | ||
|
|
d46993aaf6 | ||
|
|
310db93ff0 | ||
|
|
f07a5682a1 | ||
|
|
bcde4b9719 | ||
|
|
e2e2f4f574 | ||
|
|
a42139ae1e | ||
|
|
5d0e56f115 | ||
|
|
fa31e300e9 | ||
|
|
f54d790da4 | ||
|
|
96c8a0450e | ||
|
|
886cd65a32 | ||
|
|
8c8dd93136 | ||
|
|
9110244236 | ||
|
|
56f0ceddb6 | ||
|
|
c07752df4a | ||
|
|
3f18160dd0 | ||
|
|
0c3dc00137 | ||
|
|
4fb66975fb | ||
|
|
dc584a300d | ||
|
|
30121d6d88 | ||
|
|
2a97576e37 | ||
|
|
32a59d0104 | ||
|
|
a161769cbd | ||
|
|
9a6d7d267a | ||
|
|
9298fa9ea0 | ||
|
|
f0bd943607 | ||
|
|
30474377fc | ||
|
|
6d04230dc6 | ||
|
|
19f408b6cd | ||
|
|
ad528792fa | ||
|
|
78e34a0042 | ||
|
|
83ba17dea3 | ||
|
|
fcca85c51a | ||
|
|
17c7313f41 | ||
|
|
97126da808 | ||
|
|
fdf891c8eb | ||
|
|
af7c54203f | ||
|
|
334cf11b82 | ||
|
|
a9ba948d12 | ||
|
|
1e29a866a0 | ||
|
|
693713eb8d | ||
|
|
1b6c6af3c4 | ||
|
|
61b0ac83f0 | ||
|
|
bce435e3d2 | ||
|
|
833b3cd927 | ||
|
|
a6a3b2a0e4 | ||
|
|
73e0b4927f | ||
|
|
b68d58c19d | ||
|
|
3bccc8748d | ||
|
|
f115fd8dda | ||
|
|
954ef90d99 | ||
|
|
65f7013e78 | ||
|
|
af799e0165 | ||
|
|
c4fb53e09d | ||
|
|
982497a83f | ||
|
|
165c0eaaed | ||
|
|
2c62515746 | ||
|
|
376c2f138c | ||
|
|
d271dd3733 | ||
|
|
0c36875502 | ||
|
|
b31521affa | ||
|
|
488880ee35 | ||
|
|
f2065d897b | ||
|
|
e83b87be6c | ||
|
|
993b45034e | ||
|
|
9d6d497185 | ||
|
|
719d317263 | ||
|
|
8d78e1910a | ||
|
|
62f643135b | ||
|
|
595a8f7dfe | ||
|
|
1d7be62857 | ||
|
|
f86cdb0e62 | ||
|
|
ffc525a1ad | ||
|
|
de6b13fbfc | ||
|
|
e949ee0bb6 | ||
|
|
9f4caa7f62 | ||
|
|
a334eebe74 | ||
|
|
fd30320ed6 | ||
|
|
c293fced8a | ||
|
|
fa57bc9695 | ||
|
|
1d1c7a36b6 | ||
|
|
682377f6c5 | ||
|
|
a8d637e589 | ||
|
|
9e2564f352 | ||
|
|
eb17c20be8 | ||
|
|
e878699e2a | ||
|
|
9dc0b5cd13 | ||
|
|
875453cce5 | ||
|
|
82a1ba9ac3 | ||
|
|
ff4a09e41b | ||
|
|
6b670ad3e5 | ||
|
|
2bf7ccc952 | ||
|
|
d25476be11 | ||
|
|
6d7e2bc7aa | ||
|
|
b1148679e3 | ||
|
|
6a9cd40eaf | ||
|
|
bd16709a81 | ||
|
|
cbd7f2fca4 | ||
|
|
89b821ebf1 | ||
|
|
3fc0f891d4 | ||
|
|
74a7835a53 | ||
|
|
051b7ab2bb | ||
|
|
38f71460f0 | ||
|
|
1ab1520c60 | ||
|
|
0e5caf591a | ||
|
|
ad2582a28e | ||
|
|
c9d89f493d | ||
|
|
8669e3f780 | ||
|
|
e7d5676cb5 | ||
|
|
800bffbf24 | ||
|
|
17c02c2a25 | ||
|
|
89db5e3b3f | ||
|
|
1524eb23d4 | ||
|
|
d7dd2eb611 | ||
|
|
a3e150fdd9 | ||
|
|
fa2868edd6 | ||
|
|
e3e62b62ad | ||
|
|
a071a65ecc | ||
|
|
65e033cc80 | ||
|
|
ffe63bbce0 | ||
|
|
75d8a2dd2b | ||
|
|
2c0c97d6cc | ||
|
|
61307bdf03 | ||
|
|
2ec8fb03aa | ||
|
|
f8611efdc7 | ||
|
|
ae06a768fe | ||
|
|
feed18c33e | ||
|
|
8340228637 | ||
|
|
c5117e8669 | ||
|
|
9fb8e17cf6 | ||
|
|
d5a1413799 | ||
|
|
a7f6658b85 | ||
|
|
1885859d2f | ||
|
|
988cf117c2 | ||
|
|
d4a6009b92 | ||
|
|
a187752659 | ||
|
|
050007ff1b | ||
|
|
1441804e70 | ||
|
|
990c293ac2 | ||
|
|
de1bda0034 | ||
|
|
949df2c71f | ||
|
|
012376d2aa | ||
|
|
88224b765a | ||
|
|
3cfa48835c | ||
|
|
54a6c9767c | ||
|
|
05c7a32532 | ||
|
|
c5a3562608 | ||
|
|
a23037ce70 | ||
|
|
f4469c8393 | ||
|
|
d27474e910 | ||
|
|
7399dc9939 | ||
|
|
352f028659 | ||
|
|
966749d172 | ||
|
|
653dbf736e | ||
|
|
80be2aac8d | ||
|
|
6c3d21a5f4 | ||
|
|
c8f5ec5120 | ||
|
|
e171154a79 | ||
|
|
5217eb411d | ||
|
|
3112dc7c95 | ||
|
|
3e23ef40aa | ||
|
|
1b1aaf1032 | ||
|
|
39707842b1 | ||
|
|
2a0ec3d47e | ||
|
|
3c2022315e | ||
|
|
9a63ff2809 | ||
|
|
ee3b0edcf5 | ||
|
|
940ff167bf | ||
|
|
9a300aaf21 | ||
|
|
4924e3eebe | ||
|
|
a49c7136cd | ||
|
|
bc433bde83 | ||
|
|
bc19205e0a | ||
|
|
10f802c4aa | ||
|
|
b5b8a864cd | ||
|
|
11be868d43 | ||
|
|
0c63920cdf | ||
|
|
882c2a36b3 | ||
|
|
8270d5c05d | ||
|
|
9f6c7a3e7b | ||
|
|
55231eaf08 | ||
|
|
1af4c237ad | ||
|
|
b5d2636cbf | ||
|
|
a4d9bf87ef | ||
|
|
697eb1f639 | ||
|
|
0c9b823273 | ||
|
|
06d45d6b29 | ||
|
|
37869e52b6 | ||
|
|
6169311561 | ||
|
|
3e58d46c98 | ||
|
|
48601a6508 | ||
|
|
5fc4d8495e | ||
|
|
3f45d588e3 | ||
|
|
7bfc16ae22 | ||
|
|
1d31c10625 | ||
|
|
00af2995e0 | ||
|
|
fd042460ee | ||
|
|
c3810de709 | ||
|
|
b39d705ea5 | ||
|
|
91a13a400c | ||
|
|
b9003f8280 | ||
|
|
c91315ea01 | ||
|
|
03f8f7d64f | ||
|
|
3a43996939 | ||
|
|
2b12ab1501 | ||
|
|
6221419af6 | ||
|
|
05735ef769 | ||
|
|
8e47906bbe | ||
|
|
53f0656353 | ||
|
|
8e50cb3e77 | ||
|
|
35fc839573 | ||
|
|
9039a2de58 | ||
|
|
8263d1c34e | ||
|
|
338ad07db7 | ||
|
|
311f7c0574 | ||
|
|
ca91918707 | ||
|
|
5bcfe0a85c | ||
|
|
6084e5bb30 | ||
|
|
1d5302f26c | ||
|
|
1221daa780 | ||
|
|
b7d79fcb5f | ||
|
|
b466579c22 | ||
|
|
ed8353664b | ||
|
|
25609d9fd4 | ||
|
|
cf3859b591 | ||
|
|
8be80fde71 | ||
|
|
336de41849 | ||
|
|
306236b6e3 | ||
|
|
416ef47fb1 | ||
|
|
c9ff083c0a | ||
|
|
66c22aa3b8 | ||
|
|
b2a942c179 | ||
|
|
e9cbe3cdcb | ||
|
|
0000db67c4 | ||
|
|
89827405d7 | ||
|
|
e6914ac018 | ||
|
|
e162120943 | ||
|
|
333fb2aa7e | ||
|
|
84ee08e75c | ||
|
|
f682d12bd0 | ||
|
|
10adef15a8 | ||
|
|
fc769349d5 | ||
|
|
5152a9054b | ||
|
|
783d830c68 | ||
|
|
c25dfabdbf | ||
|
|
c7e94db3d2 | ||
|
|
2f0be7a8a6 | ||
|
|
22e55b142c | ||
|
|
6dda2f5917 | ||
|
|
838c86d1f8 | ||
|
|
a1a9e712b8 |
Binary file not shown.
|
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 262 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 261 KiB |
+11
-7
@@ -33,22 +33,26 @@
|
||||
"matchPackagePatterns": [
|
||||
"^androidx.*",
|
||||
],
|
||||
"matchPackageNames": [
|
||||
],
|
||||
"groupName": "androidx"
|
||||
"groupName": "AndroidX"
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^io.sentry",
|
||||
"^sentry",
|
||||
],
|
||||
"groupName": "sentry"
|
||||
"groupName": "Sentry"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"com.squareup.anvil",
|
||||
"matchPackagePatterns": [
|
||||
"^com.squareup.anvil"
|
||||
],
|
||||
"enabled": false,
|
||||
"allowedVersions": "!/.*-1-8$/",
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^androidx.glance",
|
||||
],
|
||||
"allowedVersions": "<=1.0.0",
|
||||
},
|
||||
],
|
||||
"regexManagers": [
|
||||
|
||||
+20
-62
@@ -12,10 +12,11 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
@@ -23,28 +24,38 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Run unit tests
|
||||
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||
with:
|
||||
arguments: --no-configuration-cache --stacktrace check -PslimTests
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
gradle-home-cache-cleanup: true
|
||||
add-job-summary: always
|
||||
dependency-graph: generate-and-submit
|
||||
|
||||
- name: Check changelog format
|
||||
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2.2.3
|
||||
with:
|
||||
path: ./CHANGELOG.md
|
||||
validation_level: error
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew --no-configuration-cache --stacktrace check -PslimTests
|
||||
|
||||
- name: (Fail-only) Upload test report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: Test report
|
||||
path: '**/build/reports/tests/**'
|
||||
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-14
|
||||
if: "github.event_name == 'push' && github.event.ref != 'refs/heads/main'"
|
||||
needs:
|
||||
- check
|
||||
steps:
|
||||
@@ -52,60 +63,7 @@ jobs:
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@7bbfa034e752445ea40215fff1c3bf9597993d3f # v3
|
||||
uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # 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
|
||||
- validate-changelog
|
||||
- dependency-review
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
env:
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
|
||||
- name: Build release app
|
||||
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
with:
|
||||
arguments: --no-configuration-cache --stacktrace collectReleaseApks
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
- name: Clean secrets
|
||||
run: scripts/signing-cleanup.sh
|
||||
|
||||
- name: Deploy snapshot
|
||||
run: scripts/deploy-snapshot.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -19,25 +20,24 @@ permissions:
|
||||
jobs:
|
||||
codeql:
|
||||
name: CodeQL
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7
|
||||
uses: github/codeql-action/init@05963f47d870e2cb19a537396c1f668a348c7d8f # v3.24.8
|
||||
with:
|
||||
languages: java
|
||||
tools: latest
|
||||
@@ -46,10 +46,10 @@ jobs:
|
||||
- name: Build project
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew assembleDebug assembleRelease
|
||||
./gradlew assembleDebug assembleInternal
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7
|
||||
uses: github/codeql-action/analyze@05963f47d870e2cb19a537396c1f668a348c7d8f # v3.24.8
|
||||
with:
|
||||
category: "/language:java"
|
||||
mobsfscan:
|
||||
@@ -59,29 +59,17 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Run mobsfscan
|
||||
uses: MobSF/mobsfscan@b8503e0027d134f55cc3306582efbbbeaa96c7ba
|
||||
uses: MobSF/mobsfscan@e29e85c36fa1aa950d7f1b1064345d94a70b2a28
|
||||
with:
|
||||
args: . --sarif --output results.sarif || true
|
||||
|
||||
- name: Upload mobsfscan report
|
||||
uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2
|
||||
uses: github/codeql-action/upload-sarif@05963f47d870e2cb19a537396c1f668a348c7d8f # v3.24.8
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -13,7 +13,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
publish-google-play-release:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
@@ -22,23 +22,23 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- name: Set up Git author
|
||||
shell: bash
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email noreply@github.com
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
echo VERSION="${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Update changelog
|
||||
uses: msfjarvis/keep-a-changelog-new-release@bf43ae42f4e42f137572950af426efcf6e777207 # 2.1.0
|
||||
uses: msfjarvis/keep-a-changelog-new-release@78cddbe3549f1c9263a2d754446d3765e865c518 # v2.1.1
|
||||
with:
|
||||
tag: v${{ env.VERSION }}
|
||||
version: ${{ env.VERSION }}
|
||||
@@ -70,14 +70,14 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: |
|
||||
./gradlew --no-configuration-cache collectReleaseApks collectReleaseBundle -PsentryUploadMappings
|
||||
./gradlew --no-configuration-cache collectReleaseApks collectReleaseBundle -PenableSentry
|
||||
|
||||
- name: Clean secrets
|
||||
run: scripts/signing-cleanup.sh
|
||||
|
||||
- name: Get changelog entry
|
||||
id: changelog_reader
|
||||
uses: mindsers/changelog-reader-action@b97ce03a10d9bdbb07beb491c76a5a01d78cd3ef # v2.2.2
|
||||
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2.2.3
|
||||
with:
|
||||
version: ${{ env.VERSION }}
|
||||
path: ./CHANGELOG.md
|
||||
@@ -88,11 +88,10 @@ jobs:
|
||||
CHANGELOG: ${{ steps.changelog_reader.outputs.changes }}
|
||||
run: |
|
||||
mkdir -p distribution/whatsnew
|
||||
printf '## Changelog\n\n' > distribution/whatsnew/whatsnew-en-GB
|
||||
printf '%s' "${CHANGELOG}" >> distribution/whatsnew/whatsnew-en-GB
|
||||
|
||||
- name: Publish bundle to Google Play
|
||||
uses: r0adkll/upload-google-play@9cf8fd30c9e616ec761db3b9abb2b0f15842ce58 # v1.1.2
|
||||
uses: r0adkll/upload-google-play@935ef9c68bb393a8e6116b1575626a7f5be3a7fb # v1.1.3
|
||||
with:
|
||||
mappingFile: android/bundle/mapping.txt
|
||||
packageName: dev.msfjarvis.claw.android
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Android release build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
versionName:
|
||||
description: 'User-facing release version name'
|
||||
required: true
|
||||
default: ""
|
||||
versionCode:
|
||||
description: 'versionCode or build number'
|
||||
required: true
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
signed-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||
with:
|
||||
cache-read-only: true
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- name: Build release app
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
./gradlew --no-configuration-cache --stacktrace -PenableSentry collectReleaseBundle
|
||||
./scripts/signing-cleanup.sh
|
||||
env:
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
ORG_GRADLE_PROJECT_VERSION_NAME: ${{ github.event.inputs.versionName }}
|
||||
ORG_GRADLE_PROJECT_VERSION_CODE: ${{ github.event.inputs.versionCode }}
|
||||
|
||||
- name: Upload app bundle
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: release-aab
|
||||
path: android/build/outputs/bundle/release/android-release.aab
|
||||
+61
-1
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.44.0] - 2024-03-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed a bug in the database code that was triggering crashes for
|
||||
a subset of upgrading users
|
||||
|
||||
## [1.43.0] - 2024-03-17
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix crash when trying to open a comments-only post from
|
||||
the home screen widget
|
||||
|
||||
## [1.42.0] - 2024-03-16
|
||||
|
||||
### Fixed
|
||||
|
||||
* Adapt to changes in lobste.rs API
|
||||
|
||||
## [1.41.0] - 2024-03-07
|
||||
|
||||
### Fixed
|
||||
|
||||
* Downgrade dependency that was pulling in extraneous storage
|
||||
permissions which the app does not require.
|
||||
|
||||
## [1.40.0] - 2024-03-07
|
||||
|
||||
### Changed
|
||||
|
||||
* Update to Compose March releases
|
||||
* Improve performance of frequently invoked database queries
|
||||
* Refactor UI data model to improve state handling
|
||||
* Make saved posts update in the background more often
|
||||
* Consolidate widget display logic
|
||||
* Rework some theming logic to align with new Material guidelines
|
||||
|
||||
## [1.39.0] - 2024-01-23
|
||||
|
||||
### Changed
|
||||
|
||||
* Add HTML bookmarks as an export format
|
||||
* Rework widget item layout for consistent touch targets
|
||||
* Add attribution for libraries used in the app
|
||||
* Redesign settings screen
|
||||
* Update to Compose December releases
|
||||
|
||||
## [1.38.0] - 2023-11-20
|
||||
|
||||
### Changed
|
||||
@@ -334,7 +382,19 @@ 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.38.0...HEAD
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.44.0...HEAD
|
||||
|
||||
[1.44.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.43.0...v1.44.0
|
||||
|
||||
[1.43.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.42.0...v1.43.0
|
||||
|
||||
[1.42.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.41.0...v1.42.0
|
||||
|
||||
[1.41.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.40.0...v1.41.0
|
||||
|
||||
[1.40.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.39.0...v1.40.0
|
||||
|
||||
[1.39.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.38.0...v1.39.0
|
||||
|
||||
[1.38.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.37.0...v1.38.0
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ Unofficial Android app for read-only access to [lobste.rs](https://lobste.rs), b
|
||||
height="80" />
|
||||
</a>
|
||||
|
||||
> Snapshots from the development branch can be obtained [here](https://github.com/msfjarvis/compose-lobsters/releases/tag/nightly).
|
||||
> [!NOTE]
|
||||
> Daily builds are published to the Google Play 'Open Testing' channel (if there are any changes), which can be opted into via this link: [[Android]](https://play.google.com/store/apps/details?id=dev.msfjarvis.claw.android) [[Web]](https://play.google.com/apps/testing/dev.msfjarvis.claw.android)
|
||||
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature_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"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -15,6 +15,7 @@ plugins {
|
||||
id("dev.msfjarvis.claw.kotlin-kapt")
|
||||
id("dev.msfjarvis.claw.sentry")
|
||||
id("dev.msfjarvis.claw.versioning-plugin")
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.modulegraphassert)
|
||||
alias(libs.plugins.whetstone)
|
||||
@@ -31,8 +32,11 @@ android {
|
||||
useLiveLiterals = false
|
||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
}
|
||||
// kotlinx.serialization 1.6.0-RC regression
|
||||
packagingOptions.resources.pickFirsts += "**/*.bin"
|
||||
buildTypes.create("internal") {
|
||||
matchingFallbacks += "release"
|
||||
signingConfig = signingConfigs["debug"]
|
||||
applicationIdSuffix = ".internal"
|
||||
}
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
@@ -44,11 +48,9 @@ baselineProfile {
|
||||
licensee {
|
||||
allow("Apache-2.0")
|
||||
allow("MIT")
|
||||
ignoreDependencies("com.michael-bull.kotlin-result") { because("kotlin-result is ISC licensed") }
|
||||
ignoreDependencies("org.commonmark") { because("Commonmark is BSD licensed") }
|
||||
allowUrl("https://jsoup.org/license") { because("Jsoup is MIT licensed") }
|
||||
allowUrl("https://opensource.org/licenses/isc-license.txt") {
|
||||
because("kotlin-result is ISC licensed")
|
||||
}
|
||||
}
|
||||
|
||||
moduleGraphAssert {
|
||||
@@ -67,6 +69,7 @@ whetstone {
|
||||
dependencies {
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.aboutLibraries.m3)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.glance)
|
||||
implementation(libs.androidx.compose.glance.m3)
|
||||
@@ -95,6 +98,7 @@ dependencies {
|
||||
implementation(projects.core)
|
||||
implementation(projects.database.impl)
|
||||
implementation(projects.model)
|
||||
implementation(projects.web)
|
||||
|
||||
kapt(libs.dagger.compiler)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
<issue
|
||||
id="VectorPath"
|
||||
|
||||
Vendored
-20
@@ -1,22 +1,2 @@
|
||||
-dontobfuscate
|
||||
-keepattributes SourceFile, LineNumberTable
|
||||
|
||||
# From https://github.com/square/retrofit/blob/master/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro
|
||||
|
||||
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
|
||||
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
|
||||
-if interface * { @retrofit2.http.* <methods>; }
|
||||
-keep,allowobfuscation interface <1>
|
||||
|
||||
# Keep inherited services.
|
||||
-if interface * { @retrofit2.http.* <methods>; }
|
||||
-keep,allowobfuscation interface * extends <1>
|
||||
|
||||
# With R8 full mode generic signatures are stripped for classes that are not
|
||||
# kept. Suspend functions are wrapped in continuations where the type argument
|
||||
# is used.
|
||||
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
||||
|
||||
# R8 full mode strips generic signatures from return types if not kept.
|
||||
-if interface * { @retrofit2.http.* public *** *(...); }
|
||||
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<!--
|
||||
~ Copyright © 2021-2024 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">Claw 🤫</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Copyright © 2021-2024 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
@@ -25,6 +25,8 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="colorMode|density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -137,6 +139,8 @@
|
||||
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" />
|
||||
@@ -174,17 +178,19 @@
|
||||
<!-- enable automatic breadcrumbs for user interactions (clicks, swipes, scrolls) -->
|
||||
<meta-data
|
||||
android:name="io.sentry.traces.user-interaction.enable"
|
||||
android:value="${enableSentry}" />
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="io.sentry.breadcrumbs.user-interaction"
|
||||
android:value="${enableSentry}" />
|
||||
android:value="true" />
|
||||
<!-- enable screenshot for crashes -->
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-screenshot"
|
||||
android:value="${enableSentry}" />
|
||||
android:value="true" />
|
||||
<!-- enable view hierarchy for crashes -->
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-view-hierarchy"
|
||||
android:value="${enableSentry}" />
|
||||
android:value="true" />
|
||||
<!-- set the environment for this build -->
|
||||
<meta-data android:name="io.sentry.environment" android:value="${sentryEnvironment}" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
+5375
-9719
File diff suppressed because it is too large
Load Diff
+5375
-9719
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android
|
||||
|
||||
import android.app.assist.AssistContent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import javax.inject.Inject
|
||||
|
||||
/** A base class that encapsulates all activities used by Claw. */
|
||||
@Stable
|
||||
abstract class BaseActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var urlLauncher: UrlLauncher
|
||||
@Inject lateinit var htmlConverter: HTMLConverter
|
||||
@Inject lateinit var viewModel: ClawViewModel
|
||||
var webUri: String? = null
|
||||
|
||||
/** Entrypoint to show a [Composable] as this [ComponentActivity]'s view. */
|
||||
@Composable abstract fun Content()
|
||||
|
||||
/** Overrideable method to call stuff in [onCreate]. */
|
||||
open fun preLaunch() {}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
preLaunch()
|
||||
Whetstone.inject(this)
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||
navigationBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||
)
|
||||
setContent {
|
||||
LobstersTheme(
|
||||
dynamicColor = true,
|
||||
providedValues = arrayOf(LocalUriHandler provides urlLauncher),
|
||||
) {
|
||||
Content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent?) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
if (outContent != null) {
|
||||
if (webUri != null) {
|
||||
outContent.webUri = Uri.parse(webUri)
|
||||
} else {
|
||||
outContent.webUri = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -34,19 +34,12 @@ class ClawApplication : Application(), ApplicationComponentOwner {
|
||||
plugins.forEach { plugin -> plugin.apply(this) }
|
||||
val postUpdateWorkRequest =
|
||||
PeriodicWorkRequestBuilder<SavedPostUpdaterWorker>(POST_REFRESH_PERIOD, TimeUnit.HOURS)
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
.setRequiresCharging(false)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
)
|
||||
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
.build()
|
||||
WorkManager.getInstance(this)
|
||||
.enqueueUniquePeriodicWork(
|
||||
"updateSavedPosts",
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
postUpdateWorkRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,80 +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
|
||||
|
||||
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.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
|
||||
import dev.msfjarvis.claw.android.ui.screens.LobstersPostsScreen
|
||||
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
|
||||
|
||||
@ContributesActivityInjector
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var urlLauncher: UrlLauncher
|
||||
@Inject lateinit var htmlConverter: HTMLConverter
|
||||
private var webUri: String? = null
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
installSplashScreen()
|
||||
Whetstone.inject(this)
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle =
|
||||
SystemBarStyle.light(
|
||||
Color.TRANSPARENT,
|
||||
Color.TRANSPARENT,
|
||||
),
|
||||
navigationBarStyle =
|
||||
SystemBarStyle.light(
|
||||
Color.TRANSPARENT,
|
||||
Color.TRANSPARENT,
|
||||
),
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val windowSizeClass = calculateWindowSizeClass(this)
|
||||
LobstersPostsScreen(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
windowSizeClass = windowSizeClass,
|
||||
setWebUri = { url -> webUri = url },
|
||||
)
|
||||
setContent {
|
||||
val windowSizeClass = calculateWindowSizeClass(this)
|
||||
LobstersTheme(
|
||||
dynamicColor = true,
|
||||
providedValues = arrayOf(LocalUriHandler provides urlLauncher),
|
||||
) {
|
||||
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 {
|
||||
|
||||
@@ -1,70 +1,24 @@
|
||||
/*
|
||||
* 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.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.ui.platform.LocalUriHandler
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
|
||||
import dev.msfjarvis.claw.android.ui.screens.SearchScreen
|
||||
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
|
||||
|
||||
@ContributesActivityInjector
|
||||
class SearchActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var urlLauncher: UrlLauncher
|
||||
@Inject lateinit var htmlConverter: HTMLConverter
|
||||
@Inject lateinit var viewModel: ClawViewModel
|
||||
private var webUri: String? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Whetstone.inject(this)
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle =
|
||||
SystemBarStyle.light(
|
||||
Color.TRANSPARENT,
|
||||
Color.TRANSPARENT,
|
||||
),
|
||||
navigationBarStyle =
|
||||
SystemBarStyle.light(
|
||||
Color.TRANSPARENT,
|
||||
Color.TRANSPARENT,
|
||||
),
|
||||
class SearchActivity : BaseActivity() {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
SearchScreen(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
setWebUri = { webUri = it },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
setContent {
|
||||
LobstersTheme(
|
||||
dynamicColor = true,
|
||||
providedValues = arrayOf(LocalUriHandler provides urlLauncher),
|
||||
) {
|
||||
SearchScreen(urlLauncher, htmlConverter, { webUri = it })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent?) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
if (outContent != null) {
|
||||
if (webUri != null) {
|
||||
outContent.webUri = Uri.parse(webUri)
|
||||
} else {
|
||||
outContent.webUri = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 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.
|
||||
@@ -28,46 +28,47 @@ 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
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class SavedPostsWidget(
|
||||
private val posts: List<SavedPost>,
|
||||
) : GlanceAppWidget() {
|
||||
class SavedPostsWidget(private val posts: Flow<List<UIPost>>) : GlanceAppWidget() {
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val postWindow = posts.first().take(50).toImmutableList()
|
||||
provideContent {
|
||||
GlanceTheme(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) GlanceTheme.colors
|
||||
else ColorProviders(light = LightThemeColors, dark = DarkThemeColors)
|
||||
) {
|
||||
WidgetHost()
|
||||
WidgetHost(postWindow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WidgetHost(posts: ImmutableList<UIPost>, modifier: GlanceModifier = GlanceModifier) {
|
||||
LazyColumn(
|
||||
modifier =
|
||||
modifier.fillMaxSize().background(GlanceTheme.colors.background).appWidgetBackground(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
item {
|
||||
val style = MaterialTheme.typography.titleLarge
|
||||
Text(
|
||||
text = "Saved posts",
|
||||
style =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onBackground,
|
||||
fontSize = style.fontSize,
|
||||
fontWeight = style.fontWeight.toGlance(),
|
||||
fontStyle = style.fontStyle.toGlance(),
|
||||
),
|
||||
modifier = GlanceModifier.padding(vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
items(posts) { post -> WidgetListEntry(post = post) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -14,6 +14,8 @@ 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
|
||||
@@ -26,36 +28,32 @@ import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.wrapContentWidth
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.MainActivity.Companion.NAVIGATION_KEY
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
private val destinationKey = Key<String>(NAVIGATION_KEY)
|
||||
|
||||
@Composable
|
||||
@GlanceComposable
|
||||
fun WidgetListEntry(
|
||||
post: SavedPost,
|
||||
modifier: GlanceModifier = GlanceModifier,
|
||||
) {
|
||||
fun WidgetListEntry(post: UIPost, modifier: GlanceModifier = GlanceModifier) {
|
||||
val titleStyle = MaterialTheme.typography.titleMedium
|
||||
val subtitleStyle = MaterialTheme.typography.labelLarge
|
||||
val commentsAction =
|
||||
actionStartActivity<MainActivity>(actionParametersOf(destinationKey to post.shortId))
|
||||
// If the URL starts with a /, it's a relative URL and we should open the comments page directly.
|
||||
val postAction =
|
||||
if (post.url.isNotEmpty()) actionStartActivity(Intent(Intent.ACTION_VIEW, Uri.parse(post.url)))
|
||||
else commentsAction
|
||||
if (post.url.startsWith('/')) commentsAction
|
||||
else actionStartActivity(Intent(Intent.ACTION_VIEW, Uri.parse(post.url)))
|
||||
Box(modifier.padding(8.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
GlanceModifier.fillMaxWidth()
|
||||
.background(GlanceTheme.colors.surfaceVariant)
|
||||
.cornerRadius(16.dp)
|
||||
.cornerRadius(4.dp)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Text(
|
||||
@@ -67,23 +65,13 @@ fun WidgetListEntry(
|
||||
fontSize = titleStyle.fontSize,
|
||||
fontWeight = titleStyle.fontWeight.toGlance(),
|
||||
fontStyle = titleStyle.fontStyle.toGlance(),
|
||||
)
|
||||
),
|
||||
)
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.ic_comment),
|
||||
contentDescription = "${post.commentCount} comments",
|
||||
modifier = GlanceModifier.padding(end = 4.dp).clickable(commentsAction),
|
||||
)
|
||||
post.commentCount?.let { count ->
|
||||
Text(
|
||||
text = "$count comments",
|
||||
modifier =
|
||||
GlanceModifier.wrapContentWidth().padding(end = 4.dp).clickable(commentsAction),
|
||||
style =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onSurfaceVariant,
|
||||
fontSize = subtitleStyle.fontSize,
|
||||
fontWeight = subtitleStyle.fontWeight.toGlance(),
|
||||
fontStyle = subtitleStyle.fontStyle.toGlance(),
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -14,8 +14,6 @@ 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() {
|
||||
@@ -23,7 +21,7 @@ class WidgetReceiver : GlanceAppWidgetReceiver() {
|
||||
@Inject lateinit var viewModel: ClawViewModel
|
||||
|
||||
override val glanceAppWidget: GlanceAppWidget
|
||||
get() = SavedPostsWidget(runBlocking { viewModel.savedPosts.first().take(50) })
|
||||
get() = SavedPostsWidget(viewModel.savedPosts)
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Whetstone.inject(this, context)
|
||||
|
||||
+3
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -32,13 +32,7 @@ object MetadataExtractorModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideUnfurler(
|
||||
okHttpClient: OkHttpClient,
|
||||
logger: UnfurlLogger,
|
||||
): Unfurler {
|
||||
return Unfurler(
|
||||
httpClient = okHttpClient,
|
||||
logger = logger,
|
||||
)
|
||||
fun provideUnfurler(okHttpClient: OkHttpClient, logger: UnfurlLogger): Unfurler {
|
||||
return Unfurler(httpClient = okHttpClient, logger = logger)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -17,18 +17,14 @@ import dev.msfjarvis.claw.core.injection.AppPlugin
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesMultibinding(ApplicationScope::class)
|
||||
class WorkManagerPlugin
|
||||
@Inject
|
||||
constructor(
|
||||
private val workerFactory: WorkerFactory,
|
||||
) : AppPlugin {
|
||||
class WorkManagerPlugin @Inject constructor(private val workerFactory: WorkerFactory) : AppPlugin {
|
||||
override fun apply(application: Application) {
|
||||
WorkManager.initialize(
|
||||
application,
|
||||
Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -13,8 +13,12 @@ import com.slack.eithernet.ApiResult.Success
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dev.msfjarvis.claw.android.viewmodel.ReadPostsRepository
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.toUIPost
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -24,15 +28,25 @@ class LobstersPagingSource
|
||||
constructor(
|
||||
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : PagingSource<Int, LobstersPost>() {
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val readPostsRepository: ReadPostsRepository,
|
||||
) : PagingSource<Int, UIPost>() {
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UIPost> {
|
||||
val page = params.key ?: STARTING_PAGE_INDEX
|
||||
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
|
||||
is Success ->
|
||||
LoadResult.Page(
|
||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||
data = result.value,
|
||||
data =
|
||||
result.value.map {
|
||||
it
|
||||
.toUIPost()
|
||||
.copy(
|
||||
isSaved = savedPostsRepository.isPostSaved(it.shortId),
|
||||
isRead = readPostsRepository.isPostRead(it.shortId),
|
||||
)
|
||||
},
|
||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||
nextKey = page + 1,
|
||||
)
|
||||
@@ -43,7 +57,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int? {
|
||||
override fun getRefreshKey(state: PagingState<Int, UIPost>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -14,9 +14,12 @@ import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
|
||||
import dev.msfjarvis.claw.android.viewmodel.ReadPostsRepository
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.toUIPost
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -33,14 +36,16 @@ constructor(
|
||||
private val searchApi: LobstersSearchApi,
|
||||
@Assisted private val queryProvider: () -> String,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : PagingSource<Int, LobstersPost>() {
|
||||
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int? {
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val readPostsRepository: ReadPostsRepository,
|
||||
) : PagingSource<Int, UIPost>() {
|
||||
override fun getRefreshKey(state: PagingState<Int, UIPost>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UIPost> {
|
||||
val searchQuery = queryProvider()
|
||||
// If there is no query, we don't need to call the API at all.
|
||||
if (searchQuery.isEmpty()) {
|
||||
@@ -55,7 +60,15 @@ constructor(
|
||||
val nextKey = if (result.value.isEmpty()) null else page + 1
|
||||
LoadResult.Page(
|
||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||
data = result.value,
|
||||
data =
|
||||
result.value.map {
|
||||
it
|
||||
.toUIPost()
|
||||
.copy(
|
||||
isSaved = savedPostsRepository.isPostSaved(it.shortId),
|
||||
isRead = readPostsRepository.isPostRead(it.shortId),
|
||||
)
|
||||
},
|
||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||
nextKey = nextKey,
|
||||
)
|
||||
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.datatransfer
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.activity.result.contract.ActivityResultContracts.GetContent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.Upload
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val MIME_TYPE = "application/json"
|
||||
|
||||
@Composable
|
||||
fun DataTransferScreen(
|
||||
context: Context,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
exportPosts: suspend (OutputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Column(modifier = modifier) {
|
||||
ImportOption(context, coroutineScope, importPosts, snackbarHostState)
|
||||
ExportOption(context, coroutineScope, exportPosts, snackbarHostState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImportOption(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val importAction =
|
||||
rememberLauncherForActivityResult(GetContent()) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
importPosts(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully imported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsActionItem(
|
||||
title = "Import saved posts",
|
||||
description = "Import saved posts from a previously generated export",
|
||||
icon = Icons.Filled.Download,
|
||||
) {
|
||||
importAction.launch(MIME_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExportOption(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
exportPosts: suspend (OutputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val exportAction =
|
||||
rememberLauncherForActivityResult(CreateDocument(MIME_TYPE)) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openOutputStream(uri)?.use { stream ->
|
||||
exportPosts(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsActionItem(
|
||||
title = "Export posts to file",
|
||||
description = "Write all saved posts into a JSON file that can be imported at a later date",
|
||||
icon = Icons.Filled.Upload,
|
||||
) {
|
||||
exportAction.launch("claw-export.json")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsActionItem(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
description: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(title) },
|
||||
supportingContent = { description?.let { Text(it) } },
|
||||
leadingContent = {
|
||||
icon?.let {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = modifier.clickable { onClick?.invoke() },
|
||||
)
|
||||
}
|
||||
|
||||
/** Shows a Snackbar but dismisses any existing ones first. */
|
||||
private suspend fun SnackbarHostState.showSnackbarDismissing(
|
||||
text: String,
|
||||
) {
|
||||
currentSnackbarData?.dismiss()
|
||||
showSnackbar(text)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -16,9 +16,8 @@ import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
fun Context.getActivity(): ComponentActivity? {
|
||||
return when (this) {
|
||||
@@ -45,7 +44,7 @@ fun rememberPostActions(
|
||||
viewModel.markPostAsRead(postId)
|
||||
val currentRoute = navController.currentDestination?.route
|
||||
val newRoute =
|
||||
Destinations.Comments.route.replace(Destinations.Comments.placeholder, postId)
|
||||
Destinations.Comments.route.replace(Destinations.Comments.PLACEHOLDER, postId)
|
||||
if (currentRoute != Destinations.Comments.route) navController.navigate(newRoute)
|
||||
}
|
||||
|
||||
@@ -59,11 +58,11 @@ fun rememberPostActions(
|
||||
urlLauncher.openUri(commentsUrl.replaceAfterLast('/', "r"))
|
||||
}
|
||||
|
||||
override fun toggleSave(post: SavedPost) {
|
||||
override fun toggleSave(post: UIPost) {
|
||||
viewModel.toggleSave(post)
|
||||
}
|
||||
|
||||
override suspend fun getComments(postId: String): LobstersPostDetails {
|
||||
override suspend fun getComments(postId: String): UIPost {
|
||||
return viewModel.getPostComments(postId)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -27,14 +27,13 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.ui.decorations.MonthHeader
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import java.time.Month
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun DatabasePosts(
|
||||
items: ImmutableMap<Month, List<SavedPost>>,
|
||||
items: ImmutableMap<String, List<UIPost>>,
|
||||
listState: LazyListState,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -53,18 +52,9 @@ fun DatabasePosts(
|
||||
} else {
|
||||
LazyColumn(state = listState) {
|
||||
items.forEach { (month, posts) ->
|
||||
stickyHeader(contentType = "month-header") { MonthHeader(month = month) }
|
||||
items(
|
||||
items = posts,
|
||||
key = { it.shortId },
|
||||
contentType = { "LobstersItem" },
|
||||
) { item ->
|
||||
LobstersListItem(
|
||||
item = item,
|
||||
isSaved = { true },
|
||||
isRead = { false },
|
||||
postActions = postActions,
|
||||
)
|
||||
stickyHeader(contentType = "month-header") { MonthHeader(label = month) }
|
||||
items(items = posts, key = { it.shortId }, contentType = { "LobstersItem" }) { item ->
|
||||
LobstersListItem(item = item, refresh = {}, postActions = postActions)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -11,39 +11,28 @@ import androidx.compose.material.icons.automirrored.filled.Reply
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import dev.msfjarvis.claw.common.posts.LobstersCard
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import me.saket.swipe.SwipeAction
|
||||
import me.saket.swipe.SwipeableActionsBox
|
||||
|
||||
@Composable
|
||||
fun LobstersListItem(
|
||||
item: SavedPost,
|
||||
isSaved: (SavedPost) -> Boolean,
|
||||
isRead: suspend (String) -> Boolean,
|
||||
item: UIPost,
|
||||
postActions: PostActions,
|
||||
refresh: () -> Unit,
|
||||
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,
|
||||
)
|
||||
SwipeableActionsBox(endActions = listOf(commentsAction)) {
|
||||
LobstersCard(post = item, postActions = postActions, refresh = refresh, modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -27,9 +27,7 @@ import androidx.paging.compose.itemKey
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.ui.NetworkError
|
||||
import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import eu.bambooapps.material3.pullrefresh.PullRefreshIndicator
|
||||
import eu.bambooapps.material3.pullrefresh.pullRefresh
|
||||
import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
|
||||
@@ -37,16 +35,14 @@ import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NetworkPosts(
|
||||
lazyPagingItems: LazyPagingItems<LobstersPost>,
|
||||
lazyPagingItems: LazyPagingItems<UIPost>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: (SavedPost) -> Boolean,
|
||||
isPostRead: suspend (String) -> Boolean,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ReportDrawnWhen { lazyPagingItems.itemCount > 0 }
|
||||
val refreshLoadState = lazyPagingItems.loadState.refresh
|
||||
val isRefreshing = refreshLoadState == LoadState.Loading
|
||||
val isRefreshing = refreshLoadState == LoadState.Loading && lazyPagingItems.itemCount == 0
|
||||
val pullRefreshState = rememberPullRefreshState(isRefreshing, lazyPagingItems::refresh)
|
||||
Box(modifier = modifier.fillMaxSize().pullRefresh(pullRefreshState)) {
|
||||
if (lazyPagingItems.itemCount == 0 && refreshLoadState is LoadState.Error) {
|
||||
@@ -56,9 +52,7 @@ fun NetworkPosts(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
) {
|
||||
LazyColumn(state = listState) {
|
||||
items(
|
||||
count = lazyPagingItems.itemCount,
|
||||
key = lazyPagingItems.itemKey { it.shortId },
|
||||
@@ -66,14 +60,11 @@ fun NetworkPosts(
|
||||
) { index ->
|
||||
val item = lazyPagingItems[index]
|
||||
if (item != null) {
|
||||
val dbModel = item.toSavedPost()
|
||||
LobstersListItem(
|
||||
item = dbModel,
|
||||
isSaved = isPostSaved,
|
||||
isRead = isPostRead,
|
||||
item = item,
|
||||
postActions = postActions,
|
||||
refresh = { lazyPagingItems.refresh() },
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
@@ -83,7 +74,7 @@ fun NetworkPosts(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
.padding(vertical = 16.dp),
|
||||
.padding(vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -21,15 +21,13 @@ import androidx.paging.PagingData
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.ui.SearchBar
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Composable
|
||||
fun SearchList(
|
||||
items: Flow<PagingData<LobstersPost>>,
|
||||
items: Flow<PagingData<UIPost>>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: (SavedPost) -> Boolean,
|
||||
postActions: PostActions,
|
||||
searchQuery: String,
|
||||
setSearchQuery: (String) -> Unit,
|
||||
@@ -50,8 +48,6 @@ fun SearchList(
|
||||
NetworkPosts(
|
||||
lazyPagingItems = lazyPagingItems,
|
||||
listState = listState,
|
||||
isPostSaved = isPostSaved,
|
||||
isPostRead = { false },
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.login
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.google.accompanist.web.AccompanistWebViewClient
|
||||
import com.google.accompanist.web.WebView
|
||||
import com.google.accompanist.web.rememberWebViewState
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import io.github.aakira.napier.Napier
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
|
||||
@Suppress("Unused")
|
||||
@Composable
|
||||
fun LobstersLoginWebView(modifier: Modifier = Modifier) {
|
||||
val state = rememberWebViewState(url = "https://lobste.rs/login")
|
||||
val webClient = remember {
|
||||
object : AccompanistWebViewClient() {
|
||||
override fun onPageFinished(view: WebView, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
Napier.d(tag = "WebView") { "onPageFinished(url=$url)" }
|
||||
if (url != null && url.toHttpUrl() == LobstersApi.BASE_URL.toHttpUrl()) {
|
||||
val cookies = CookieManager.getInstance().getCookie(LobstersApi.BASE_URL)
|
||||
val trap =
|
||||
cookies
|
||||
.split(";")
|
||||
.associate {
|
||||
val split = it.split("=")
|
||||
split[0] to split[1]
|
||||
}["lobsters_trap"]
|
||||
Napier.d(tag = "WebView") { "lobsters_trap=${trap}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WebView(state = state, client = webClient, modifier = modifier, captureBackPresses = false)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -9,34 +9,38 @@ package dev.msfjarvis.claw.android.ui.navigation
|
||||
sealed class Destinations {
|
||||
abstract val route: String
|
||||
|
||||
object Hottest : Destinations() {
|
||||
data object Hottest : Destinations() {
|
||||
override val route = "hottest"
|
||||
}
|
||||
|
||||
object Newest : Destinations() {
|
||||
data object Newest : Destinations() {
|
||||
override val route = "newest"
|
||||
}
|
||||
|
||||
object Saved : Destinations() {
|
||||
data object Saved : Destinations() {
|
||||
override val route = "saved"
|
||||
}
|
||||
|
||||
object Comments : Destinations() {
|
||||
const val placeholder = "{postId}"
|
||||
override val route = "comments/$placeholder"
|
||||
data object Comments : Destinations() {
|
||||
const val PLACEHOLDER = "{postId}"
|
||||
override val route = "comments/$PLACEHOLDER"
|
||||
}
|
||||
|
||||
object User : Destinations() {
|
||||
const val placeholder = "{username}"
|
||||
override val route = "user/$placeholder"
|
||||
data object User : Destinations() {
|
||||
const val PLACEHOLDER = "{username}"
|
||||
override val route = "user/$PLACEHOLDER"
|
||||
}
|
||||
|
||||
object DataTransfer : Destinations() {
|
||||
override val route: String = "datatransfer"
|
||||
data object Search : Destinations() {
|
||||
override val route = "search"
|
||||
}
|
||||
|
||||
object Search : Destinations() {
|
||||
override val route: String = "search"
|
||||
data object Settings : Destinations() {
|
||||
override val route = "settings"
|
||||
}
|
||||
|
||||
data object AboutLibraries : Destinations() {
|
||||
override val route = "about_libraries"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+26
-61
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -13,22 +13,20 @@ import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.ImportExport
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
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.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -41,10 +39,8 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -65,11 +61,10 @@ 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.datatransfer.DataTransferScreen
|
||||
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
|
||||
@@ -83,6 +78,7 @@ import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.ui.decorations.ClawAppBar
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import dev.msfjarvis.claw.common.user.UserProfile
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -120,7 +116,7 @@ fun LobstersPostsScreen(
|
||||
LaunchedEffect(false) {
|
||||
if (postIdOverride != null) {
|
||||
navController.navigate(
|
||||
Destinations.Comments.route.replace(Destinations.Comments.placeholder, postIdOverride)
|
||||
Destinations.Comments.route.replace(Destinations.Comments.PLACEHOLDER, postIdOverride)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -162,7 +158,7 @@ fun LobstersPostsScreen(
|
||||
navItems.none { it.route == currentDestination }
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() },
|
||||
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
@@ -185,38 +181,14 @@ fun LobstersPostsScreen(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
IconButton(onClick = { expanded = true }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = "Extra options")
|
||||
IconButton(
|
||||
onClick = { context.startActivity(Intent(context, SearchActivity::class.java)) }
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Search, contentDescription = "Search posts")
|
||||
}
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Backup and restore") },
|
||||
onClick = {
|
||||
navController.navigate(Destinations.DataTransfer.route)
|
||||
expanded = false
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ImportExport,
|
||||
contentDescription = "Data transfer options",
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Search posts") },
|
||||
onClick = {
|
||||
context.startActivity(Intent(context, SearchActivity::class.java))
|
||||
expanded = false
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Search,
|
||||
contentDescription = "Search posts",
|
||||
)
|
||||
}
|
||||
)
|
||||
IconButton(onClick = { navController.navigate(Destinations.Settings.route) }) {
|
||||
Icon(imageVector = Icons.Filled.Tune, contentDescription = "Settings")
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -256,8 +228,6 @@ fun LobstersPostsScreen(
|
||||
NetworkPosts(
|
||||
lazyPagingItems = hottestPosts,
|
||||
listState = hottestListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
isPostRead = viewModel::isPostRead,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
@@ -266,26 +236,20 @@ fun LobstersPostsScreen(
|
||||
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,
|
||||
)
|
||||
DatabasePosts(items = savedPosts, listState = savedListState, postActions = postActions)
|
||||
}
|
||||
composable(
|
||||
route = Destinations.Comments.route,
|
||||
arguments = listOf(navArgument("postId") { type = NavType.StringType }),
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.placeholder}/.*" },
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.placeholder}" },
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.PLACEHOLDER}/.*" },
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.PLACEHOLDER}" },
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val postId =
|
||||
@@ -304,27 +268,28 @@ fun LobstersPostsScreen(
|
||||
composable(
|
||||
route = Destinations.User.route,
|
||||
arguments = listOf(navArgument("username") { type = NavType.StringType }),
|
||||
deepLinks =
|
||||
listOf(navDeepLink { uriPattern = "$uri/u/${Destinations.User.placeholder}" }),
|
||||
deepLinks = listOf(navDeepLink { uriPattern = "$uri/u/${Destinations.User.PLACEHOLDER}" }),
|
||||
) { backStackEntry ->
|
||||
val username =
|
||||
requireNotNull(backStackEntry.arguments?.getString("username")) {
|
||||
"Navigating to ${Destinations.User.route} without necessary 'username' argument"
|
||||
}
|
||||
setWebUri("https://lobste.rs/u/$username")
|
||||
UserProfile(
|
||||
username = username,
|
||||
getProfile = viewModel::getUserProfile,
|
||||
)
|
||||
UserProfile(username = username, getProfile = viewModel::getUserProfile)
|
||||
}
|
||||
composable(route = Destinations.DataTransfer.route) {
|
||||
DataTransferScreen(
|
||||
composable(route = Destinations.Settings.route) {
|
||||
SettingsScreen(
|
||||
context = context,
|
||||
openLibrariesScreen = { navController.navigate(Destinations.AboutLibraries.route) },
|
||||
importPosts = viewModel::importPosts,
|
||||
exportPosts = viewModel::exportPosts,
|
||||
exportPostsAsJson = viewModel::exportPostsAsJson,
|
||||
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.AboutLibraries.route) {
|
||||
LibrariesContainer(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -47,7 +47,6 @@ fun SearchScreen(
|
||||
SearchList(
|
||||
items = viewModel.searchResults,
|
||||
listState = listState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
postActions = postActions,
|
||||
searchQuery = viewModel.searchQuery,
|
||||
setSearchQuery = { query -> viewModel.searchQuery = query },
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -11,11 +11,14 @@ import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
|
||||
import com.slack.eithernet.ApiResult.Failure
|
||||
@@ -29,14 +32,15 @@ import dev.msfjarvis.claw.android.paging.SearchPagingSource
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
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 dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.fromSavedPost
|
||||
import dev.msfjarvis.claw.model.toUIPost
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.time.LocalDateTime
|
||||
import java.time.Month
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
@@ -44,11 +48,8 @@ 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
|
||||
@@ -67,81 +68,75 @@ constructor(
|
||||
@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)
|
||||
}
|
||||
private val newestPostsPager =
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
pagingSourceFactory.create(api::getNewestPosts)
|
||||
}
|
||||
private val searchResultsPager =
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
searchPagingSourceFactory.create { searchQuery }
|
||||
}
|
||||
|
||||
val hottestPosts
|
||||
get() = hottestPostsPager.flow
|
||||
|
||||
val newestPosts
|
||||
get() = newestPostsPager.flow
|
||||
|
||||
val savedPosts
|
||||
get() =
|
||||
savedPostsRepository.savedPosts.map {
|
||||
it.sortedByDescending { post -> post.createdAt.toLocalDateTime() }
|
||||
}
|
||||
|
||||
val hottestPosts =
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = PAGE_SIZE),
|
||||
initialKey = STARTING_PAGE_INDEX,
|
||||
pagingSourceFactory = { pagingSourceFactory.create(api::getHottestPosts) },
|
||||
)
|
||||
.flow
|
||||
.cachedIn(viewModelScope)
|
||||
val newestPosts =
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = PAGE_SIZE),
|
||||
initialKey = STARTING_PAGE_INDEX,
|
||||
pagingSourceFactory = { pagingSourceFactory.create(api::getNewestPosts) },
|
||||
)
|
||||
.flow
|
||||
.cachedIn(viewModelScope)
|
||||
val searchResults =
|
||||
Pager(
|
||||
PagingConfig(pageSize = PAGE_SIZE),
|
||||
initialKey = STARTING_PAGE_INDEX,
|
||||
pagingSourceFactory = { searchPagingSourceFactory.create { searchQuery } },
|
||||
)
|
||||
.flow
|
||||
val savedPosts = savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) }
|
||||
val savedPostsByMonth
|
||||
get() = savedPosts.map(::mapSavedPosts)
|
||||
|
||||
val searchResults
|
||||
get() = searchResultsPager.flow
|
||||
get() = savedPosts.map(::groupSavedPosts)
|
||||
|
||||
var searchQuery by mutableStateOf("")
|
||||
|
||||
private val _savedPostsMutex = Mutex()
|
||||
private var _readPosts = emptyList<String>()
|
||||
private var _savedPosts = emptyList<String>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
savedPosts.collectLatest {
|
||||
_savedPostsMutex.withLock { _savedPosts = it.map(SavedPost::shortId) }
|
||||
}
|
||||
}
|
||||
viewModelScope.launch { savedPosts.collectLatest { _savedPosts = it.map(UIPost::shortId) } }
|
||||
viewModelScope.launch { readPostsRepository.readPosts.collectLatest { _readPosts = it } }
|
||||
}
|
||||
|
||||
private fun mapSavedPosts(items: List<SavedPost>): ImmutableMap<Month, List<SavedPost>> {
|
||||
private fun groupSavedPosts(items: List<UIPost>): ImmutableMap<String, List<UIPost>> {
|
||||
val sorted =
|
||||
items.sortedWith { post1, post2 ->
|
||||
post2.createdAt.toLocalDateTime().compareTo(post1.createdAt.toLocalDateTime())
|
||||
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 -> post.createdAt.toLocalDateTime().month }.toImmutableMap()
|
||||
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) {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
val saved = isPostSaved(post)
|
||||
if (saved) {
|
||||
savedPostsRepository.removePost(post)
|
||||
} else {
|
||||
savedPostsRepository.savePost(post)
|
||||
}
|
||||
val newPosts = savedPosts.first()
|
||||
withContext(mainDispatcher) {
|
||||
SavedPostsWidget(newPosts.take(50)).updateAll(getApplication())
|
||||
}
|
||||
fun toggleSave(post: UIPost) {
|
||||
viewModelScope.launch {
|
||||
savedPostsRepository.toggleSave(post)
|
||||
withContext(mainDispatcher) { SavedPostsWidget(savedPosts).updateAll(getApplication()) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getPostComments(postId: String) =
|
||||
withContext(ioDispatcher) {
|
||||
when (val result = api.getPostDetails(postId)) {
|
||||
is Success -> result.value
|
||||
is Success -> result.value.toUIPost()
|
||||
is Failure.NetworkFailure -> throw result.error
|
||||
is Failure.UnknownFailure -> throw result.error
|
||||
is Failure.HttpFailure -> {
|
||||
@@ -177,14 +172,16 @@ constructor(
|
||||
|
||||
suspend fun importPosts(input: InputStream) = dataTransferRepository.importPosts(input)
|
||||
|
||||
suspend fun exportPosts(output: OutputStream) = dataTransferRepository.exportPosts(output)
|
||||
suspend fun exportPostsAsJson(output: OutputStream) =
|
||||
dataTransferRepository.exportPostsAsJson(output)
|
||||
|
||||
suspend fun exportPostsAsHtml(output: OutputStream) =
|
||||
dataTransferRepository.exportPostsAsHTML(output)
|
||||
|
||||
fun markPostAsRead(postId: String) {
|
||||
viewModelScope.launch { readPostsRepository.markRead(postId) }
|
||||
}
|
||||
|
||||
suspend fun isPostRead(postId: String) = readPostsRepository.isRead(postId)
|
||||
|
||||
/**
|
||||
* Parses a given [String] into a [LocalDateTime]. This method is only intended to be used for
|
||||
* dates in the format returned by the Lobsters API, and is not a general purpose parsing
|
||||
|
||||
+58
-16
@@ -1,20 +1,21 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.database.SavedPostSerializer
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.database.local.SavedPostQueries
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
@@ -27,27 +28,68 @@ class DataTransferRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val json: Json,
|
||||
private val savedPostQueries: SavedPostQueries,
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val serializer = ListSerializer(SavedPostSerializer)
|
||||
|
||||
suspend fun importPosts(input: InputStream) {
|
||||
val posts: List<SavedPost> =
|
||||
withContext(ioDispatcher) {
|
||||
json.decodeFromStream(
|
||||
serializer,
|
||||
input,
|
||||
)
|
||||
}
|
||||
withContext(dbDispatcher) {
|
||||
savedPostQueries.transaction { posts.forEach(savedPostQueries::insertOrReplacePost) }
|
||||
}
|
||||
withContext(ioDispatcher) { json.decodeFromStream(serializer, input) }
|
||||
savedPostsRepository.savePosts(posts)
|
||||
}
|
||||
|
||||
suspend fun exportPosts(output: OutputStream) {
|
||||
val posts = withContext(dbDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
|
||||
suspend fun exportPostsAsJson(output: OutputStream) {
|
||||
val posts = savedPostsRepository.savedPosts.first()
|
||||
withContext(ioDispatcher) { json.encodeToStream(serializer, posts, output) }
|
||||
}
|
||||
|
||||
suspend fun exportPostsAsHTML(output: OutputStream) {
|
||||
fun computeTimestamp(post: SavedPost): Long {
|
||||
val temporal = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(post.createdAt)
|
||||
val instant = Instant.from(temporal)
|
||||
return instant.toEpochMilli()
|
||||
}
|
||||
|
||||
val posts = savedPostsRepository.savedPosts.first()
|
||||
val header =
|
||||
"""
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<!-- This is an automatically generated file.
|
||||
It will be read and overwritten.
|
||||
DO NOT EDIT! -->
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
"""
|
||||
.trimIndent()
|
||||
val html = buildString {
|
||||
append(header)
|
||||
append("<DD><p>\n")
|
||||
for (post in posts) {
|
||||
append(
|
||||
"""
|
||||
<DT><A HREF="${post.url.ifEmpty { post.commentsUrl }}" ADD_DATE="${computeTimestamp(post)}" PRIVATE="0" TAGS="${post.tags.joinToString(",")}">${post.title}</A>
|
||||
<DD>${post.title}
|
||||
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
append(
|
||||
"""
|
||||
<DT><A HREF="https://example.com/" ADD_DATE="0" PRIVATE="0" TAGS="delete,me,pls">Padding post</A>
|
||||
<DD>Linkding ignores the last entry so this pads the difference for imports
|
||||
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
append("</DD></p>\n")
|
||||
}
|
||||
withContext(ioDispatcher) {
|
||||
val writer = output.bufferedWriter()
|
||||
writer.write(html)
|
||||
writer.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.database.local.ReadPostsQueries
|
||||
import javax.inject.Inject
|
||||
@@ -18,11 +20,13 @@ constructor(
|
||||
private val readPostsQueries: ReadPostsQueries,
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
val readPosts = readPostsQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
|
||||
|
||||
suspend fun markRead(postId: String) {
|
||||
withContext(dbDispatcher) { readPostsQueries.markRead(postId) }
|
||||
}
|
||||
|
||||
suspend fun isRead(postId: String): Boolean =
|
||||
withContext(dbDispatcher) { readPostsQueries.isRead(postId).executeAsOneOrNull() != null }
|
||||
suspend fun isPostRead(shortId: String): Boolean {
|
||||
return withContext(dbDispatcher) { readPostsQueries.isPostRead(shortId).executeAsOne() }
|
||||
}
|
||||
}
|
||||
|
||||
+13
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -11,6 +11,8 @@ import app.cash.sqldelight.coroutines.mapToList
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.database.local.SavedPostQueries
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import io.github.aakira.napier.Napier
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -24,9 +26,14 @@ constructor(
|
||||
) {
|
||||
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
|
||||
|
||||
suspend fun savePost(post: SavedPost) {
|
||||
Napier.d(tag = TAG) { "Saving post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.insertOrReplacePost(post) }
|
||||
suspend fun toggleSave(post: UIPost) {
|
||||
if (post.isSaved) {
|
||||
Napier.d(tag = TAG) { "Removing post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
|
||||
} else {
|
||||
Napier.d(tag = TAG) { "Saving post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.insertOrReplacePost(post.toSavedPost()) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun savePosts(posts: List<SavedPost>) {
|
||||
@@ -38,9 +45,8 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removePost(post: SavedPost) {
|
||||
Napier.d(tag = TAG) { "Removing post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
|
||||
suspend fun isPostSaved(postId: String): Boolean {
|
||||
return withContext(dbDispatcher) { savedPostQueries.isPostSaved(postId).executeAsOne() }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -18,9 +18,12 @@ import dev.msfjarvis.claw.android.glance.SavedPostsWidget
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.fromSavedPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* WorkManager-backed [CoroutineWorker] that gets all the posts from [SavedPostsRepository], fetches
|
||||
@@ -44,7 +47,10 @@ constructor(
|
||||
.filterIsInstance<Success<LobstersPostDetails>>()
|
||||
.map { result -> result.value.toSavedPost() }
|
||||
.let { savedPostsRepository.savePosts(it) }
|
||||
SavedPostsWidget(savedPostsRepository.savedPosts.first().take(50)).updateAll(applicationContext)
|
||||
SavedPostsWidget(
|
||||
savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) }
|
||||
)
|
||||
.updateAll(applicationContext)
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--
|
||||
~ Copyright © 2024 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:colorControlNormal"
|
||||
android:alpha="0.6"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21.99,4c0,-1.1 -0.89,-2 -1.99,-2L4,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h14l4,4 -0.01,-18zM20,4v13.17L18.83,16L4,16L4,4h16zM6,12h12v2L6,14zM6,9h12v2L6,11zM6,6h12v2L6,8z"/>
|
||||
</vector>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Copyright © 2021-2024 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
@@ -13,11 +13,5 @@
|
||||
<item name="postSplashScreenTheme">@style/Theme.Claw</item>
|
||||
</style>
|
||||
|
||||
<style name="Platform.Theme.Claw" parent="android:Theme.Material.NoActionBar" />
|
||||
|
||||
<style name="Base.Theme.Claw" parent="Platform.Theme.Claw">
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Claw" parent="Base.Theme.Claw" />
|
||||
<style name="Theme.Claw" parent="android:Theme.Material.NoActionBar" />
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
|
||||
#
|
||||
versioning-plugin.versionCode=13800
|
||||
versioning-plugin.versionName=1.38.0
|
||||
versioning-plugin.versionCode=14400
|
||||
versioning-plugin.versionName=1.44.0
|
||||
|
||||
@@ -27,7 +27,6 @@ dependencies {
|
||||
implementation(libs.jsoup)
|
||||
|
||||
testImplementation(testFixtures(libs.eithernet))
|
||||
testImplementation(libs.testcontainers)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.kotlinx.serialization.json)
|
||||
testImplementation(libs.retrofit.kotlinxSerializationConverter)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -1,53 +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.api
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.msfjarvis.claw.model.shiori.AuthRequest
|
||||
import dev.msfjarvis.claw.model.shiori.AuthResponse
|
||||
import dev.msfjarvis.claw.model.shiori.Bookmark
|
||||
import dev.msfjarvis.claw.model.shiori.BookmarkRequest
|
||||
import dev.msfjarvis.claw.model.shiori.BookmarksResponse
|
||||
import dev.msfjarvis.claw.model.shiori.EditedBookmark
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.HTTP
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
|
||||
private const val SESSION_ID_HEADER = "X-Session-Id"
|
||||
|
||||
interface ShioriApi {
|
||||
@POST("/api/login") suspend fun login(@Body body: AuthRequest): ApiResult<AuthResponse, Unit>
|
||||
|
||||
@HTTP(method = "POST", path = "/api/logout", hasBody = false)
|
||||
suspend fun logout(@Header(SESSION_ID_HEADER) sessionId: String): ApiResult<Unit, Unit>
|
||||
|
||||
@GET("/api/bookmarks")
|
||||
suspend fun getBookmarks(
|
||||
@Header(SESSION_ID_HEADER) sessionId: String,
|
||||
): ApiResult<BookmarksResponse, Unit>
|
||||
|
||||
@POST("/api/bookmarks")
|
||||
suspend fun addBookmark(
|
||||
@Header(SESSION_ID_HEADER) sessionId: String,
|
||||
@Body bookmarkRequest: BookmarkRequest,
|
||||
): ApiResult<Bookmark, Unit>
|
||||
|
||||
@PUT("/api/bookmarks")
|
||||
suspend fun editBookmark(
|
||||
@Header(SESSION_ID_HEADER) sessionId: String,
|
||||
@Body bookmark: EditedBookmark,
|
||||
): ApiResult<Bookmark, Unit>
|
||||
|
||||
@HTTP(method = "DELETE", path = "/api/bookmarks", hasBody = true)
|
||||
suspend fun deleteBookmark(
|
||||
@Header(SESSION_ID_HEADER) sessionId: String,
|
||||
@Body ids: List<Int>,
|
||||
): ApiResult<Int, Unit>
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -8,7 +8,6 @@ package dev.msfjarvis.claw.api.converters
|
||||
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import java.lang.reflect.Type
|
||||
import okhttp3.ResponseBody
|
||||
import org.jsoup.Jsoup
|
||||
@@ -32,8 +31,7 @@ object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
|
||||
val url = titleElement.attr("href")
|
||||
val tags = elem.select("span.tags > a").map(Element::text)
|
||||
val (commentCount, commentsUrl) = getCommentsData(elem.select("span.comments_label"))
|
||||
val submitter =
|
||||
getSubmitter(elem.select("div.byline").first() ?: error("No byline element found"))
|
||||
val submitter = elem.select("div.byline > a.u-author").text()
|
||||
return LobstersPost(
|
||||
shortId = shortId,
|
||||
title = title,
|
||||
@@ -55,29 +53,11 @@ object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
|
||||
return (countString.toIntOrNull() ?: 0) to commentsUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a bare-bones [User] object given a byline [elem]. We only need this to be usable for
|
||||
* displaying in a list.
|
||||
*/
|
||||
private fun getSubmitter(elem: Element): User {
|
||||
val userElement = elem.select("a.u-author")
|
||||
val avatarElement = elem.select("img.avatar")
|
||||
val username = userElement.text()
|
||||
val avatarUrl = avatarElement.attr("src")
|
||||
return User(
|
||||
username = username,
|
||||
about = "",
|
||||
invitedBy = null,
|
||||
avatarUrl = avatarUrl,
|
||||
createdAt = "",
|
||||
)
|
||||
}
|
||||
|
||||
object Factory : Converter.Factory() {
|
||||
override fun responseBodyConverter(
|
||||
type: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
retrofit: Retrofit,
|
||||
): Converter<ResponseBody, List<LobstersPost>> {
|
||||
return SearchConverter
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -16,7 +16,6 @@ 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.ShioriApi
|
||||
import dev.msfjarvis.claw.api.converters.CSRFTokenConverter
|
||||
import dev.msfjarvis.claw.api.converters.SearchConverter
|
||||
import javax.inject.Named
|
||||
@@ -74,8 +73,6 @@ object RetrofitModule {
|
||||
@Provides
|
||||
fun provideSearchApi(@SearchApi retrofit: Retrofit): LobstersSearchApi = retrofit.create()
|
||||
|
||||
@Provides fun provideShioriApi(retrofit: Retrofit): ShioriApi = retrofit.create()
|
||||
|
||||
@Provides
|
||||
@IntKey(0)
|
||||
@IntoMap
|
||||
@@ -94,10 +91,7 @@ object RetrofitModule {
|
||||
@Provides
|
||||
@SearchApi
|
||||
fun provideConverters(): List<@JvmSuppressWildcards Converter.Factory> =
|
||||
listOf(
|
||||
ApiResultConverterFactory,
|
||||
SearchConverter.Factory,
|
||||
)
|
||||
listOf(ApiResultConverterFactory, SearchConverter.Factory)
|
||||
|
||||
@Provides @Named("LobstersURL") fun provideLobstersUrl(): String = LobstersApi.BASE_URL
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -33,7 +33,7 @@ class ApiTest {
|
||||
val posts = api.getHottestPosts(1)
|
||||
assertIs<Success<List<LobstersPost>>>(posts)
|
||||
val commentsOnlyPosts = posts.value.asSequence().filter { it.url.isEmpty() }.toSet()
|
||||
assertThat(commentsOnlyPosts).hasSize(2)
|
||||
assertThat(commentsOnlyPosts).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -10,7 +10,6 @@ import com.google.common.truth.Truth.assertThat
|
||||
import com.slack.eithernet.ApiResult
|
||||
import com.slack.eithernet.test.newEitherNetController
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import dev.msfjarvis.claw.util.TestUtils.assertIs
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -38,7 +37,7 @@ class SearchApiTest {
|
||||
createdAt = "",
|
||||
commentCount = 3,
|
||||
commentsUrl = "https://lobste.rs/s/gjlsdg/chatgpt_visits_emacs_doctor",
|
||||
submitter = User("xenodium", "", null, "/avatars/xenodium-16.png", ""),
|
||||
submitter = "xenodium",
|
||||
tags = listOf("ai", "emacs"),
|
||||
description = "",
|
||||
),
|
||||
@@ -50,7 +49,7 @@ class SearchApiTest {
|
||||
createdAt = "",
|
||||
commentCount = 0,
|
||||
commentsUrl = "https://lobste.rs/s/astcqf/implementing_question_answering_system",
|
||||
submitter = User("asteroid", "", null, "/avatars/asteroid-16.png", ""),
|
||||
submitter = "asteroid",
|
||||
tags = listOf("ai"),
|
||||
description = "",
|
||||
),
|
||||
|
||||
@@ -1,173 +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.api
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import com.slack.eithernet.ApiResultCallAdapterFactory
|
||||
import com.slack.eithernet.ApiResultConverterFactory
|
||||
import com.slack.eithernet.successOrNull
|
||||
import dev.msfjarvis.claw.model.shiori.AuthRequest
|
||||
import dev.msfjarvis.claw.model.shiori.AuthResponse
|
||||
import dev.msfjarvis.claw.model.shiori.Bookmark
|
||||
import dev.msfjarvis.claw.model.shiori.BookmarkRequest
|
||||
import dev.msfjarvis.claw.model.shiori.BookmarksResponse
|
||||
import dev.msfjarvis.claw.model.shiori.EditedBookmark
|
||||
import dev.msfjarvis.claw.model.shiori.Tag
|
||||
import dev.msfjarvis.claw.util.TestUtils.assertIs
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.testcontainers.containers.GenericContainer
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
|
||||
class ShioriApiTest {
|
||||
|
||||
private lateinit var credentials: AuthResponse
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
container.start()
|
||||
runBlocking {
|
||||
credentials =
|
||||
requireNotNull(api.login(AuthRequest(USER, PASSWORD)).successOrNull()) { "Login failed" }
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
container.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getBookmarks() = runTest {
|
||||
val response = api.getBookmarks(credentials.session).successOrNull()
|
||||
assertIs<BookmarksResponse>(response)
|
||||
assertThat(response.page).isEqualTo(1)
|
||||
assertThat(response.bookmarks).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addBookmark() = runTest {
|
||||
val response =
|
||||
api
|
||||
.addBookmark(
|
||||
credentials.session,
|
||||
BookmarkRequest(
|
||||
"https://example.com",
|
||||
false,
|
||||
0,
|
||||
emptyList(),
|
||||
"Example Domain",
|
||||
"""
|
||||
This domain is for use in illustrative examples in documents.
|
||||
You may use this domain in literature without prior coordination or asking for permission.
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
)
|
||||
.successOrNull()
|
||||
assertIs<Bookmark>(response)
|
||||
assertThat(response.url).isEqualTo("https://example.com")
|
||||
assertThat(response.title).isEqualTo("Example Domain")
|
||||
assertThat(response.excerpt)
|
||||
.isEqualTo(
|
||||
"""
|
||||
This domain is for use in illustrative examples in documents.
|
||||
You may use this domain in literature without prior coordination or asking for permission.
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
assertThat(response.id).isAtLeast(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun editBookmark() = runTest {
|
||||
val response =
|
||||
api
|
||||
.addBookmark(
|
||||
credentials.session,
|
||||
BookmarkRequest(
|
||||
"https://example.com",
|
||||
false,
|
||||
0,
|
||||
emptyList(),
|
||||
"Example Domain",
|
||||
"""
|
||||
This domain is for use in illustrative examples in documents.
|
||||
You may use this domain in literature without prior coordination or asking for permission.
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
)
|
||||
.successOrNull()
|
||||
assertIs<Bookmark>(response)
|
||||
assertThat(response.tags).isEmpty()
|
||||
val newBookmark =
|
||||
EditedBookmark(
|
||||
id = response.id,
|
||||
url = response.url,
|
||||
title = response.title,
|
||||
tags = listOf(Tag("examples")),
|
||||
)
|
||||
val edited = api.editBookmark(credentials.session, newBookmark).successOrNull()
|
||||
assertIs<Bookmark>(edited)
|
||||
assertThat(edited.tags).isNotEmpty()
|
||||
assertThat(edited.tags).containsExactly(Tag("examples"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteBookmark() = runTest {
|
||||
val response =
|
||||
api
|
||||
.addBookmark(
|
||||
credentials.session,
|
||||
BookmarkRequest(
|
||||
"https://example.com",
|
||||
false,
|
||||
0,
|
||||
emptyList(),
|
||||
"Example Domain",
|
||||
"""
|
||||
This domain is for use in illustrative examples in documents.
|
||||
You may use this domain in literature without prior coordination or asking for permission.
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
)
|
||||
.successOrNull()
|
||||
assertIs<Bookmark>(response)
|
||||
val count = api.deleteBookmark(credentials.session, listOf(response.id)).successOrNull()
|
||||
assertThat(count).isEqualTo(1)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Default settings for the container
|
||||
private const val USER = "shiori"
|
||||
private const val PASSWORD = "gopher"
|
||||
|
||||
private val container =
|
||||
GenericContainer("ghcr.io/go-shiori/shiori:v1.5.5").withExposedPorts(8080)
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
private val api
|
||||
get() =
|
||||
Retrofit.Builder()
|
||||
.baseUrl("http://${container.host}:${container.firstMappedPort}")
|
||||
.addConverterFactory(ApiResultConverterFactory)
|
||||
.addConverterFactory(json.asConverterFactory(MediaType.get("application/json")))
|
||||
.addCallAdapterFactory(ApiResultCallAdapterFactory)
|
||||
.build()
|
||||
.create<ShioriApi>()
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"username":"msfjarvis","created_at":"2020-04-24T11:41:56.000-05:00","is_admin":false,"about":"Android and Kotlin developer\r\n","is_moderator":false,"karma":574,"avatar_url":"/avatars/msfjarvis-100.png","invited_by_user":"Amolith","github_username":"msfjarvis","twitter_username":"msfjarvis"}
|
||||
{"username":"msfjarvis","created_at":"2020-04-24T11:41:56.000-05:00","is_admin":false,"about":"Android and Kotlin developer, currently working for [Dyte](https://dyte.io/)","is_moderator":false,"karma":1343,"avatar_url":"/avatars/msfjarvis-100.png","invited_by_user":"Amolith","github_username":"msfjarvis"}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,13 +1,11 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
|
||||
plugins {
|
||||
id(libs.plugins.android.test.get().pluginId)
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
@@ -30,21 +28,11 @@ android {
|
||||
matchingFallbacks += "release"
|
||||
}
|
||||
}
|
||||
|
||||
testOptions.managedDevices.devices {
|
||||
create<ManagedVirtualDevice>("api31") {
|
||||
device = "Pixel 6"
|
||||
apiLevel = 31
|
||||
systemImageSource = "aosp"
|
||||
}
|
||||
}
|
||||
|
||||
targetProjectPath = ":android"
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
managedDevices += "api31"
|
||||
useConnectedDevices = false
|
||||
useConnectedDevices = true
|
||||
enableEmulatorDisplay = false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
plugins { `kotlin-dsl` }
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
alias(libs.plugins.android.lint)
|
||||
}
|
||||
|
||||
kotlin.jvmToolchain { languageVersion.set(JavaLanguageVersion.of(17)) }
|
||||
kotlin.jvmToolchain(17)
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
@@ -61,6 +64,8 @@ gradlePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
lint.baseline = project.file("lint-baseline.xml")
|
||||
|
||||
dependencies {
|
||||
implementation(libs.build.agp)
|
||||
implementation(libs.build.cachefix)
|
||||
@@ -69,8 +74,9 @@ dependencies {
|
||||
implementation(libs.build.sentry)
|
||||
implementation(libs.build.spotless)
|
||||
implementation(libs.build.vcu)
|
||||
implementation(libs.build.versions)
|
||||
|
||||
// Expose the generated version catalog API to the plugin.
|
||||
implementation(files(libs::class.java.superclass.protectionDomain.codeSource.location))
|
||||
|
||||
lintChecks(libs.androidx.lint)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -8,31 +8,42 @@
|
||||
|
||||
rootProject.name = "build-logic"
|
||||
|
||||
pluginManagement {
|
||||
plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" }
|
||||
repositories {
|
||||
exclusiveContent {
|
||||
forRepository { gradlePluginPortal() }
|
||||
filter {
|
||||
includeModule(
|
||||
"org.gradle.toolchains.foojay-resolver-convention",
|
||||
"org.gradle.toolchains.foojay-resolver-convention.gradle.plugin",
|
||||
)
|
||||
includeModule("org.gradle.toolchains", "foojay-resolver")
|
||||
includeModule("org.gradle.kotlin.kotlin-dsl", "org.gradle.kotlin.kotlin-dsl.gradle.plugin")
|
||||
includeModule("org.gradle.kotlin", "gradle-kotlin-dsl-plugins")
|
||||
}
|
||||
}
|
||||
exclusiveContent {
|
||||
forRepository { google() }
|
||||
filter {
|
||||
includeGroup("androidx.databinding")
|
||||
includeGroupByRegex("com.android.*")
|
||||
includeGroup("com.google.testing.platform")
|
||||
}
|
||||
}
|
||||
mavenCentral { mavenContent { releasesOnly() } }
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
exclusiveContent {
|
||||
forRepository(::google)
|
||||
filter {
|
||||
includeGroup("androidx.databinding")
|
||||
includeGroup("com.android")
|
||||
includeGroup("com.android.tools.analytics-library")
|
||||
includeGroup("com.android.tools.build")
|
||||
includeGroup("com.android.tools.build.jetifier")
|
||||
includeGroup("com.android.databinding")
|
||||
includeGroup("com.android.tools.ddms")
|
||||
includeGroup("com.android.tools.layoutlib")
|
||||
includeGroup("com.android.tools.lint")
|
||||
includeGroup("com.android.tools.utp")
|
||||
includeGroup("androidx.lint")
|
||||
includeGroupByRegex("com.android.*")
|
||||
includeGroup("com.google.testing.platform")
|
||||
includeModule("com.android.tools", "annotations")
|
||||
includeModule("com.android.tools", "common")
|
||||
includeModule("com.android.tools", "desugar_jdk_libs")
|
||||
includeModule("com.android.tools", "desugar_jdk_libs_configuration")
|
||||
includeModule("com.android.tools", "dvlib")
|
||||
includeModule("com.android.tools", "play-sdk-proto")
|
||||
includeModule("com.android.tools", "repository")
|
||||
includeModule("com.android.tools", "sdklib")
|
||||
includeModule("com.android.tools", "sdk-common")
|
||||
}
|
||||
}
|
||||
exclusiveContent {
|
||||
@@ -43,7 +54,7 @@ dependencyResolutionManagement {
|
||||
includeModule("gradle.plugin.org.gradle.android", "android-cache-fix-gradle-plugin")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
mavenCentral { mavenContent { releasesOnly() } }
|
||||
}
|
||||
versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -33,26 +33,22 @@ class ApplicationPlugin : Plugin<Project> {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
all {
|
||||
setProguardFiles(
|
||||
listOf(
|
||||
"proguard-android-optimize.pro",
|
||||
"proguard-rules.pro",
|
||||
"proguard-rules-missing-classes.pro",
|
||||
)
|
||||
buildTypes.configureEach {
|
||||
setProguardFiles(
|
||||
listOf(
|
||||
"proguard-android-optimize.pro",
|
||||
"proguard-rules.pro",
|
||||
"proguard-rules-missing-classes.pro",
|
||||
)
|
||||
}
|
||||
named("release") {
|
||||
)
|
||||
if (name == "release") {
|
||||
isMinifyEnabled = !project.providers.environmentVariable("DISABLE_MINIFY").isPresent
|
||||
}
|
||||
named("debug") {
|
||||
} else if (name == "debug") {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
project.configureBuildSigning()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,36 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import com.github.benmanes.gradle.versions.VersionsPlugin
|
||||
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
|
||||
import com.github.zafarkhaja.semver.Version
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import nl.littlerobots.vcu.plugin.VersionCatalogUpdateExtension
|
||||
import nl.littlerobots.vcu.plugin.VersionCatalogUpdatePlugin
|
||||
import nl.littlerobots.vcu.plugin.versionSelector
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
|
||||
@Suppress("Unused")
|
||||
class DependencyUpdatesPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
project.pluginManager.apply(VersionsPlugin::class)
|
||||
project.pluginManager.apply(VersionCatalogUpdatePlugin::class)
|
||||
project.tasks.withType<DependencyUpdatesTask>().configureEach {
|
||||
rejectVersionIf { isNonStable(candidate.version) && !isNonStable(currentVersion) }
|
||||
checkConstraints = true
|
||||
checkBuildEnvironmentConstraints = true
|
||||
checkForGradleUpdate = true
|
||||
}
|
||||
project.extensions.configure<VersionCatalogUpdateExtension> {
|
||||
keep.keepUnusedLibraries.set(true)
|
||||
versionSelector {
|
||||
val currentVersion = Version.tryParse(it.currentVersion).getOrNull()
|
||||
val newVersion = Version.tryParse(it.candidate.version).getOrNull()
|
||||
if (currentVersion == null || newVersion == null) {
|
||||
false
|
||||
} else {
|
||||
newVersion.isHigherThan(currentVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNonStable(version: String): Boolean {
|
||||
val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) }
|
||||
val regex = "^[0-9,.v-]+(-r)?$".toRegex()
|
||||
val isStable = stableKeyword || regex.matches(version)
|
||||
return isStable.not()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import dev.msfjarvis.claw.gradle.KotlinCommonPlugin.Companion.JVM_TOOLCHAIN_ACTION
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
@Suppress("Unused")
|
||||
class KotlinAndroidPlugin : Plugin<Project> {
|
||||
@@ -23,5 +26,25 @@ class KotlinAndroidPlugin : Plugin<Project> {
|
||||
apply(KotlinCommonPlugin::class)
|
||||
}
|
||||
project.extensions.getByType<KotlinProjectExtension>().jvmToolchain(JVM_TOOLCHAIN_ACTION)
|
||||
val libs = project.extensions.getByName("libs") as LibrariesForLibs
|
||||
val composeCompilerVersion = libs.versions.composeCompiler.get()
|
||||
val kotlinVersion = libs.versions.kotlin.get()
|
||||
val matches = COMPOSE_COMPILER_VERSION_REGEX.find(composeCompilerVersion)
|
||||
if (matches != null) {
|
||||
val (compilerKotlinVersion) = matches.destructured
|
||||
if (compilerKotlinVersion != kotlinVersion) {
|
||||
project.tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions.freeCompilerArgs.addAll(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=$kotlinVersion",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// Matches against 1.5.0-dev-k1.9.0-6a60475e07f
|
||||
val COMPOSE_COMPILER_VERSION_REGEX = "\\d.\\d.\\d-dev-k(\\d.\\d.\\d+)-[a-z0-9]+".toRegex()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -43,10 +43,7 @@ class KotlinCommonPlugin : Plugin<Project> {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val ADDITIONAL_COMPILER_ARGS =
|
||||
listOf(
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
)
|
||||
private val ADDITIONAL_COMPILER_ARGS = listOf("-opt-in=kotlin.RequiresOptIn")
|
||||
|
||||
val JVM_TOOLCHAIN_ACTION =
|
||||
Action<JavaToolchainSpec> { languageVersion.set(JavaLanguageVersion.of(17)) }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -41,7 +41,7 @@ class KotlinKaptPlugin : Plugin<Project> {
|
||||
}
|
||||
}
|
||||
project.tasks
|
||||
.matching { it.name.startsWith("kapt") && it.name.endsWith("UnitTestKotlin") }
|
||||
.named { it.startsWith("kapt") && it.endsWith("UnitTestKotlin") }
|
||||
.configureEach { enabled = false }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -31,14 +31,13 @@ object LintConfig {
|
||||
if (!isJVM) {
|
||||
enable += "ComposeM2Api"
|
||||
error += "ComposeM2Api"
|
||||
// False-positives in the TestContainers library
|
||||
disable += "DeprecatedCall"
|
||||
}
|
||||
baseline = project.file("lint-baseline.xml")
|
||||
// This is extremely annoying
|
||||
disable += "NewerVersionAvailable"
|
||||
disable += "AndroidGradlePluginVersion"
|
||||
disable += "GradleDependency"
|
||||
// Jetpack Compose lints are incompatible with AGP 8.3.0-alpha06
|
||||
disable += "NewerVersionAvailable"
|
||||
// Can't do anything about this
|
||||
disable += "ObsoleteLintCustomCheck"
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -25,42 +25,63 @@ class SentryPlugin : Plugin<Project> {
|
||||
project.extensions.configure<ApplicationAndroidComponentsExtension> {
|
||||
onVariants(selector().all()) { variant ->
|
||||
val sentryDsn = project.providers.environmentVariable(SENTRY_DSN_PROPERTY)
|
||||
val enableSentry =
|
||||
project.providers.gradleProperty(SENTRY_ENABLE_GRADLE_PROPERTY).isPresent
|
||||
variant.manifestPlaceholders.put("sentryDsn", sentryDsn.getOrElse(""))
|
||||
variant.manifestPlaceholders.put("enableSentry", "${variant.name == "release"}")
|
||||
variant.manifestPlaceholders.put(
|
||||
"sentryEnvironment",
|
||||
when {
|
||||
variant.name.contains("release", true) && enableSentry -> "production"
|
||||
variant.name.contains("release", true) && !enableSentry -> "nightly"
|
||||
else -> "dev"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
project.plugins.apply(io.sentry.android.gradle.SentryPlugin::class)
|
||||
project.extensions.configure<SentryPluginExtension> {
|
||||
val enableMappings =
|
||||
project.providers.gradleProperty(SENTRY_UPLOAD_MAPPINGS_PROPERTY).isPresent
|
||||
includeProguardMapping.set(enableMappings)
|
||||
autoUploadProguardMapping.set(enableMappings)
|
||||
includeProguardMapping.set(true)
|
||||
autoUploadProguardMapping.set(true)
|
||||
uploadNativeSymbols.set(false)
|
||||
autoUploadNativeSymbols.set(false)
|
||||
includeNativeSources.set(false)
|
||||
ignoredVariants.set(emptySet())
|
||||
ignoredBuildTypes.set(setOf("benchmark", "debug"))
|
||||
ignoredBuildTypes.set(setOf("benchmark"))
|
||||
ignoredFlavors.set(emptySet())
|
||||
tracingInstrumentation {
|
||||
enabled.set(true)
|
||||
debug.set(false)
|
||||
logcat.enabled.set(true)
|
||||
forceInstrumentDependencies.set(false)
|
||||
features.set(EnumSet.allOf(InstrumentationFeature::class.java))
|
||||
}
|
||||
experimentalGuardsquareSupport.set(false)
|
||||
dexguardEnabled.set(false)
|
||||
autoInstallation {
|
||||
enabled.set(true)
|
||||
sentryVersion.set(libs.versions.sentry.sdk)
|
||||
}
|
||||
includeDependenciesReport.set(true)
|
||||
includeSourceContext.set(true)
|
||||
autoUploadSourceContext.set(true)
|
||||
additionalSourceDirsForSourceContext.set(emptySet())
|
||||
debug.set(false)
|
||||
org.set("claw")
|
||||
projectName.set("compose-lobsters")
|
||||
authToken.set(project.providers.environmentVariable("SENTRY_AUTH_TOKEN"))
|
||||
url.set(null)
|
||||
telemetry.set(false)
|
||||
telemetryDsn.set(null)
|
||||
}
|
||||
with(project.dependencies) {
|
||||
addProvider("implementation", platform(libs.sentry.bom))
|
||||
addProvider("implementation", libs.sentry.android)
|
||||
}
|
||||
with(project.dependencies) { addProvider("implementation", platform(libs.sentry.bom)) }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val SENTRY_DSN_PROPERTY = "SENTRY_DSN"
|
||||
private const val SENTRY_UPLOAD_MAPPINGS_PROPERTY = "sentryUploadMappings"
|
||||
private const val SENTRY_ENABLE_GRADLE_PROPERTY = "enableSentry"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -45,7 +45,7 @@ class SpotlessPlugin : Plugin<Project> {
|
||||
endWithNewline()
|
||||
licenseHeaderFile(
|
||||
project.file("spotless/license.xml"),
|
||||
"<(adaptive-icon|appwidget-provider|data-extraction-rules|full-backup-content|manifest|vector|resources)"
|
||||
"<(adaptive-icon|appwidget-provider|data-extraction-rules|full-backup-content|manifest|vector|resources)",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,6 @@ class SpotlessPlugin : Plugin<Project> {
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val KTFMT_VERSION = "0.46"
|
||||
private const val KTFMT_VERSION = "0.47"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,34 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.gradle.signing
|
||||
|
||||
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
|
||||
import com.android.build.api.dsl.ApplicationBuildType
|
||||
import com.android.build.api.dsl.CommonExtension
|
||||
import java.util.Properties
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
private const val KEYSTORE_CONFIG_PATH = "keystore.properties"
|
||||
|
||||
/** Configure signing for all build types. */
|
||||
@Suppress("UnstableApiUsage")
|
||||
internal fun Project.configureBuildSigning() {
|
||||
val keystoreConfigFile = rootProject.layout.projectDirectory.file(KEYSTORE_CONFIG_PATH)
|
||||
if (keystoreConfigFile.asFile.exists()) {
|
||||
extensions.configure<BaseAppModuleExtension> {
|
||||
extensions.configure<CommonExtension<*, ApplicationBuildType, *, *, *, *>>("android") {
|
||||
val contents = providers.fileContents(keystoreConfigFile).asText
|
||||
val keystoreProperties = Properties()
|
||||
keystoreProperties.load(contents.get().byteInputStream())
|
||||
signingConfigs {
|
||||
register("release") {
|
||||
val releaseSigningConfig =
|
||||
signingConfigs.register("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
val signingConfig = signingConfigs.getByName("release")
|
||||
buildTypes.all { setSigningConfig(signingConfig) }
|
||||
buildTypes.configureEach { signingConfig = releaseSigningConfig.get() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-21
@@ -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,21 +8,19 @@ package dev.msfjarvis.claw.gradle.versioning
|
||||
|
||||
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
|
||||
import com.android.build.api.variant.VariantOutputConfiguration
|
||||
import com.android.build.gradle.internal.plugins.AppPlugin
|
||||
import com.vdurmont.semver4j.Semver
|
||||
import com.github.zafarkhaja.semver.Version
|
||||
import java.util.Properties
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.register
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
|
||||
/**
|
||||
* A Gradle [Plugin] that takes a [Project] with the [AppPlugin] applied and dynamically sets the
|
||||
* versionCode and versionName properties based on values read from a [VERSIONING_PROP_FILE] file in
|
||||
* the [Project.getProjectDir] directory. It also adds Gradle tasks to bump the major, minor, and
|
||||
* patch versions along with one to prepare the next snapshot.
|
||||
* A Gradle [Plugin] that takes a [Project] with the `com.android.application` applied and
|
||||
* dynamically sets the versionCode and versionName properties based on values read from a
|
||||
* [VERSIONING_PROP_FILE] file in the [Project.getProjectDir] directory. It also adds Gradle tasks
|
||||
* to bump the major, minor, and patch versions along with one to prepare the next snapshot.
|
||||
*/
|
||||
@Suppress("Unused")
|
||||
class VersioningPlugin : Plugin<Project> {
|
||||
@@ -37,14 +35,24 @@ class VersioningPlugin : Plugin<Project> {
|
||||
val contents = providers.fileContents(propFile).asText
|
||||
val versionProps = Properties().also { it.load(contents.get().byteInputStream()) }
|
||||
val versionName =
|
||||
requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_NAME)) {
|
||||
"version.properties must contain a '$VERSIONING_PROP_VERSION_NAME' property"
|
||||
}
|
||||
providers
|
||||
.gradleProperty("VERSION_NAME")
|
||||
.orElse(
|
||||
requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_NAME)) {
|
||||
"version.properties must contain a '$VERSIONING_PROP_VERSION_NAME' property"
|
||||
}
|
||||
)
|
||||
val versionCode =
|
||||
requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_CODE).toInt()) {
|
||||
"version.properties must contain a '$VERSIONING_PROP_VERSION_CODE' property"
|
||||
}
|
||||
project.plugins.withType<AppPlugin> {
|
||||
providers
|
||||
.gradleProperty("VERSION_CODE")
|
||||
.orElse(
|
||||
requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_CODE)) {
|
||||
"version.properties must contain a '$VERSIONING_PROP_VERSION_CODE' property"
|
||||
}
|
||||
)
|
||||
.map(String::toInt)
|
||||
|
||||
project.pluginManager.withPlugin("com.android.application") {
|
||||
androidAppPluginApplied.set(true)
|
||||
extensions.configure<ApplicationAndroidComponentsExtension> {
|
||||
onVariants { variant ->
|
||||
@@ -57,30 +65,30 @@ class VersioningPlugin : Plugin<Project> {
|
||||
}
|
||||
}
|
||||
}
|
||||
val version = Semver(versionName)
|
||||
val version = versionName.map(Version::parse)
|
||||
tasks.register<VersioningTask>("clearPreRelease") {
|
||||
description = "Remove the pre-release suffix from the version"
|
||||
semverString.set(version.withClearedSuffix().toString())
|
||||
semverString.set(version.map(Version::toStableVersion).map(Version::toString))
|
||||
propertyFile.set(propFile)
|
||||
}
|
||||
tasks.register<VersioningTask>("bumpMajor") {
|
||||
description = "Increment the major version"
|
||||
semverString.set(version.withIncMajor().withClearedSuffix().toString())
|
||||
semverString.set(version.map(Version::nextMajorVersion).map(Version::toString))
|
||||
propertyFile.set(propFile)
|
||||
}
|
||||
tasks.register<VersioningTask>("bumpMinor") {
|
||||
description = "Increment the minor version"
|
||||
semverString.set(version.withIncMinor().withClearedSuffix().toString())
|
||||
semverString.set(version.map(Version::nextMinorVersion).map(Version::toString))
|
||||
propertyFile.set(propFile)
|
||||
}
|
||||
tasks.register<VersioningTask>("bumpPatch") {
|
||||
description = "Increment the patch version"
|
||||
semverString.set(version.withIncPatch().withClearedSuffix().toString())
|
||||
semverString.set(version.map(Version::nextPatchVersion).map(Version::toString))
|
||||
propertyFile.set(propFile)
|
||||
}
|
||||
tasks.register<VersioningTask>("bumpSnapshot") {
|
||||
description = "Increment the minor version and add the `SNAPSHOT` suffix"
|
||||
semverString.set(version.withIncMinor().withSuffix("SNAPSHOT").toString())
|
||||
semverString.set(version.map { it.nextMinorVersion("SNAPSHOT") }.map(Version::toString))
|
||||
propertyFile.set(propFile)
|
||||
}
|
||||
afterEvaluate {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.gradle.versioning
|
||||
|
||||
import com.vdurmont.semver4j.Semver
|
||||
import com.github.zafarkhaja.semver.Version
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
@@ -22,11 +22,11 @@ abstract class VersioningTask : DefaultTask() {
|
||||
@get:OutputFile abstract val propertyFile: RegularFileProperty
|
||||
|
||||
/** Generate the Android 'versionCode' property */
|
||||
private fun Semver.androidCode(): Int {
|
||||
return major * 1_00_00 + minor * 1_00 + patch
|
||||
private fun Version.androidCode(): Long {
|
||||
return majorVersion() * 1_00_00 + minorVersion() * 1_00 + patchVersion()
|
||||
}
|
||||
|
||||
private fun Semver.toPropFileText(): String {
|
||||
private fun Version.toPropFileText(): String {
|
||||
val newVersionCode = androidCode()
|
||||
val newVersionName = toString()
|
||||
return buildString {
|
||||
@@ -46,6 +46,6 @@ abstract class VersioningTask : DefaultTask() {
|
||||
|
||||
@TaskAction
|
||||
fun execute() {
|
||||
propertyFile.get().asFile.writeText(Semver(semverString.get()).toPropFileText())
|
||||
propertyFile.get().asFile.writeText(Version.parse(semverString.get()).toPropFileText())
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -11,5 +11,5 @@ internal sealed class NetworkState {
|
||||
|
||||
class Error(val error: Throwable, val description: String) : NetworkState()
|
||||
|
||||
object Loading : NetworkState()
|
||||
data object Loading : NetworkState()
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -46,24 +46,22 @@ import dev.msfjarvis.claw.common.posts.TagRow
|
||||
import dev.msfjarvis.claw.common.ui.NetworkImage
|
||||
import dev.msfjarvis.claw.common.ui.ThemedRichText
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import java.time.Instant
|
||||
import java.time.temporal.TemporalAccessor
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun CommentsHeader(
|
||||
postDetails: LobstersPostDetails,
|
||||
post: UIPost,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val linkMetadata by
|
||||
produceState(
|
||||
initialValue = LinkMetadata(postDetails.url, null),
|
||||
) {
|
||||
runSuspendCatching { postActions.getLinkMetadata(postDetails.url) }
|
||||
produceState(initialValue = LinkMetadata(post.url, null)) {
|
||||
runSuspendCatching { postActions.getLinkMetadata(post.url) }
|
||||
.onSuccess { metadata -> value = metadata }
|
||||
}
|
||||
|
||||
@@ -72,8 +70,8 @@ internal fun CommentsHeader(
|
||||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
PostTitle(title = postDetails.title, isRead = false)
|
||||
TagRow(tags = postDetails.tags.toImmutableList())
|
||||
PostTitle(title = post.title, isRead = false)
|
||||
TagRow(tags = post.tags.toImmutableList())
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
if (linkMetadata.url.isNotBlank()) {
|
||||
@@ -81,34 +79,29 @@ internal fun CommentsHeader(
|
||||
linkMetadata = linkMetadata,
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
postActions.viewPost(postDetails.shortId, linkMetadata.url, postDetails.commentsUrl)
|
||||
postActions.viewPost(post.shortId, linkMetadata.url, post.commentsUrl)
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
if (postDetails.description.isNotBlank()) {
|
||||
ThemedRichText(htmlConverter.convertHTMLToMarkdown(postDetails.description))
|
||||
if (post.description.isNotBlank()) {
|
||||
ThemedRichText(htmlConverter.convertHTMLToMarkdown(post.description))
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
Submitter(
|
||||
text = AnnotatedString("Submitted by ${postDetails.submitter.username}"),
|
||||
avatarUrl = "https://lobste.rs/${postDetails.submitter.avatarUrl}",
|
||||
contentDescription = "User avatar for ${postDetails.submitter.username}",
|
||||
text = AnnotatedString("Submitted by ${post.submitter}"),
|
||||
avatarUrl = "https://lobste.rs/avatars/${post.submitter}-100.png",
|
||||
contentDescription = "User avatar for ${post.submitter}",
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
uriHandler.openUri("https://lobste.rs/u/${postDetails.submitter.username}")
|
||||
},
|
||||
Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${post.submitter}") },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PostLink(
|
||||
linkMetadata: LinkMetadata,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
private fun PostLink(linkMetadata: LinkMetadata, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier.background(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
@@ -155,27 +148,26 @@ internal fun CommentEntry(
|
||||
end = CommentEntryPadding,
|
||||
top = CommentEntryPadding,
|
||||
bottom = CommentEntryPadding,
|
||||
),
|
||||
)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Submitter(
|
||||
text =
|
||||
buildCommenterString(
|
||||
commenterName = comment.user.username,
|
||||
commenterName = comment.user,
|
||||
score = comment.score,
|
||||
isUnread = commentNode.isUnread,
|
||||
createdAt = comment.createdAt,
|
||||
updatedAt = comment.updatedAt,
|
||||
),
|
||||
avatarUrl = "https://lobste.rs/${comment.user.avatarUrl}",
|
||||
contentDescription = "User avatar for ${comment.user.username}",
|
||||
modifier =
|
||||
Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${comment.user.username}") },
|
||||
avatarUrl = "https://lobste.rs/avatars/${comment.user}-100.png",
|
||||
contentDescription = "User avatar for ${comment.user}",
|
||||
modifier = Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${comment.user}") },
|
||||
)
|
||||
if (commentNode.isExpanded) {
|
||||
ThemedRichText(
|
||||
text = htmlConverter.convertHTMLToMarkdown(comment.comment),
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -228,11 +220,7 @@ fun buildCommenterString(
|
||||
if (isUnread) {
|
||||
append(' ')
|
||||
withStyle(
|
||||
style =
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
style = SpanStyle(fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.error)
|
||||
) {
|
||||
append("(unread)")
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -17,7 +17,7 @@ internal data class CommentNode(
|
||||
val children: MutableList<CommentNode> = mutableListOf(),
|
||||
val isUnread: Boolean = false,
|
||||
var isExpanded: Boolean = true,
|
||||
var indentLevel: Int
|
||||
var indentLevel: Int,
|
||||
) {
|
||||
fun addChild(child: CommentNode) {
|
||||
if (comment.shortId == child.comment.parentComment) {
|
||||
@@ -32,7 +32,7 @@ internal data class CommentNode(
|
||||
|
||||
internal fun createListNode(
|
||||
comments: List<Comment>,
|
||||
commentState: PostComments?
|
||||
commentState: PostComments?,
|
||||
): MutableList<CommentNode> {
|
||||
val commentNodes = mutableListOf<CommentNode>()
|
||||
val isUnread = { id: String -> commentState?.commentIds?.contains(id) == false }
|
||||
@@ -43,7 +43,7 @@ internal fun createListNode(
|
||||
CommentNode(
|
||||
comment = comments[i],
|
||||
isUnread = isUnread(comments[i].shortId),
|
||||
indentLevel = 1
|
||||
indentLevel = 1,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
@@ -52,7 +52,7 @@ internal fun createListNode(
|
||||
CommentNode(
|
||||
comment = comments[i],
|
||||
isUnread = isUnread(comments[i].shortId),
|
||||
indentLevel = it.indentLevel + 1
|
||||
indentLevel = it.indentLevel + 1,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -86,11 +86,7 @@ internal fun LazyListScope.nodes(
|
||||
toggleExpanded: (CommentNode) -> Unit,
|
||||
) {
|
||||
nodes.forEach { node ->
|
||||
node(
|
||||
node = node,
|
||||
htmlConverter = htmlConverter,
|
||||
toggleExpanded = toggleExpanded,
|
||||
)
|
||||
node(node = node, htmlConverter = htmlConverter, toggleExpanded = toggleExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,18 +100,10 @@ private fun LazyListScope.node(
|
||||
return
|
||||
}
|
||||
item {
|
||||
CommentEntry(
|
||||
commentNode = node,
|
||||
htmlConverter = htmlConverter,
|
||||
toggleExpanded = toggleExpanded,
|
||||
)
|
||||
CommentEntry(commentNode = node, htmlConverter = htmlConverter, toggleExpanded = toggleExpanded)
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (node.children.isNotEmpty()) {
|
||||
nodes(
|
||||
node.children,
|
||||
htmlConverter = htmlConverter,
|
||||
toggleExpanded = toggleExpanded,
|
||||
)
|
||||
nodes(node.children, htmlConverter = htmlConverter, toggleExpanded = toggleExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -36,12 +36,12 @@ import dev.msfjarvis.claw.common.ui.NetworkError
|
||||
import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun CommentsPageInternal(
|
||||
details: LobstersPostDetails,
|
||||
details: UIPost,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
commentState: PostComments?,
|
||||
@@ -54,11 +54,7 @@ private fun CommentsPageInternal(
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
|
||||
LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) {
|
||||
item {
|
||||
CommentsHeader(
|
||||
postDetails = details,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
)
|
||||
CommentsHeader(post = details, postActions = postActions, htmlConverter = htmlConverter)
|
||||
}
|
||||
|
||||
if (commentNodes.isNotEmpty()) {
|
||||
@@ -123,7 +119,7 @@ fun CommentsPage(
|
||||
when (postDetails) {
|
||||
is Success<*> -> {
|
||||
CommentsPageInternal(
|
||||
details = (postDetails as Success<LobstersPostDetails>).data,
|
||||
details = (postDetails as Success<UIPost>).data,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
commentState = commentState,
|
||||
|
||||
@@ -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.
|
||||
@@ -51,39 +51,37 @@ import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.NetworkImage
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun LobstersCard(
|
||||
post: SavedPost,
|
||||
isSaved: Boolean,
|
||||
isRead: Boolean,
|
||||
post: UIPost,
|
||||
postActions: PostActions,
|
||||
refresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var localSavedState by remember(post, isSaved) { mutableStateOf(isSaved) }
|
||||
var localReadState by remember(post) { mutableStateOf(post.isRead) }
|
||||
var localSavedState by remember(post) { mutableStateOf(post.isSaved) }
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { postActions.viewPost(post.shortId, post.url, post.commentsUrl) }
|
||||
.clickable {
|
||||
postActions.viewPost(post.shortId, post.url, post.commentsUrl)
|
||||
localReadState = true
|
||||
refresh()
|
||||
}
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(start = 16.dp, top = 16.dp, end = 4.dp, bottom = 16.dp),
|
||||
.padding(start = 16.dp, top = 16.dp, end = 4.dp, bottom = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
PostDetails(
|
||||
modifier = Modifier.weight(1f),
|
||||
post = post,
|
||||
isRead = isRead,
|
||||
)
|
||||
PostDetails(post = post, isRead = localReadState, modifier = Modifier.weight(1f))
|
||||
Column(
|
||||
modifier = Modifier.wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
@@ -95,6 +93,7 @@ fun LobstersCard(
|
||||
Modifier.clickable(role = Role.Button) {
|
||||
localSavedState = !localSavedState
|
||||
postActions.toggleSave(post)
|
||||
refresh()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.width(48.dp))
|
||||
@@ -103,7 +102,11 @@ fun LobstersCard(
|
||||
modifier =
|
||||
Modifier.clickable(
|
||||
role = Role.Button,
|
||||
onClick = { postActions.viewComments(post.shortId) },
|
||||
onClick = {
|
||||
postActions.viewComments(post.shortId)
|
||||
localReadState = true
|
||||
refresh()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -112,25 +115,21 @@ fun LobstersCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PostDetails(post: SavedPost, isRead: Boolean, modifier: Modifier = Modifier) {
|
||||
fun PostDetails(post: UIPost, isRead: Boolean, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PostTitle(title = post.title, isRead = isRead)
|
||||
TagRow(tags = post.tags.toImmutableList())
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Submitter(
|
||||
text = AnnotatedString("Submitted by ${post.submitterName}"),
|
||||
avatarUrl = "https://lobste.rs/${post.submitterAvatarUrl}",
|
||||
contentDescription = "User avatar for ${post.submitterName}",
|
||||
text = AnnotatedString("Submitted by ${post.submitter}"),
|
||||
avatarUrl = "https://lobste.rs/avatars/${post.submitter}-100.png",
|
||||
contentDescription = "User avatar for ${post.submitter}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun PostTitle(
|
||||
title: String,
|
||||
isRead: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
internal fun PostTitle(title: String, isRead: Boolean, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = modifier,
|
||||
@@ -169,10 +168,7 @@ internal fun Submitter(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveButton(
|
||||
isSaved: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
private fun SaveButton(isSaved: Boolean, modifier: Modifier = Modifier) {
|
||||
Crossfade(targetState = isSaved, label = "save-button") { saved ->
|
||||
Box(modifier = modifier.padding(12.dp)) {
|
||||
Icon(
|
||||
@@ -186,10 +182,7 @@ private fun SaveButton(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CommentsButton(
|
||||
commentCount: Int?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
private fun CommentsButton(commentCount: Int?, modifier: Modifier = Modifier) {
|
||||
BadgedBox(
|
||||
modifier = modifier.padding(12.dp),
|
||||
badge = {
|
||||
@@ -219,10 +212,7 @@ private fun CommentsButton(
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
internal fun TagRow(
|
||||
tags: ImmutableList<String>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
internal fun TagRow(tags: ImmutableList<String>, modifier: Modifier = Modifier) {
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
@@ -233,10 +223,7 @@ internal fun TagRow(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TagText(
|
||||
tag: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
private fun TagText(tag: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = tag,
|
||||
modifier =
|
||||
@@ -250,24 +237,23 @@ private fun TagText(
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun LobstersCardPreview() {
|
||||
private fun LobstersCardPreview() {
|
||||
LobstersTheme {
|
||||
LobstersCard(
|
||||
post =
|
||||
SavedPost(
|
||||
UIPost(
|
||||
shortId = "ooga",
|
||||
title = "Simple Anomaly Detection Using Plain SQL",
|
||||
url = "https://hakibenita.com/sql-anomaly-detection",
|
||||
createdAt = "2020-09-21T08:04:24.000-05:00",
|
||||
commentCount = 1,
|
||||
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
|
||||
submitterName = "Haki",
|
||||
submitterAvatarUrl = "/avatars/Haki-100.png",
|
||||
submitter = "Haki",
|
||||
tags = listOf("databases", "apis"),
|
||||
description = "",
|
||||
isSaved = true,
|
||||
isRead = true,
|
||||
),
|
||||
isRead = true,
|
||||
isSaved = true,
|
||||
postActions =
|
||||
object : PostActions {
|
||||
override fun viewPost(postId: String, postUrl: String, commentsUrl: String) {}
|
||||
@@ -276,10 +262,10 @@ fun LobstersCardPreview() {
|
||||
|
||||
override fun viewCommentsPage(commentsUrl: String) {}
|
||||
|
||||
override fun toggleSave(post: SavedPost) {}
|
||||
override fun toggleSave(post: UIPost) {}
|
||||
|
||||
override suspend fun getComments(postId: String): LobstersPostDetails {
|
||||
return LobstersPostDetails(
|
||||
override suspend fun getComments(postId: String): UIPost {
|
||||
return UIPost(
|
||||
shortId = "ooga",
|
||||
title = "Simple Anomaly Detection Using Plain SQL",
|
||||
url = "https://hakibenita.com/sql-anomaly-detection",
|
||||
@@ -288,7 +274,7 @@ fun LobstersCardPreview() {
|
||||
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
|
||||
tags = listOf("databases", "apis"),
|
||||
description = "",
|
||||
submitter = User("Haki", "", "", "", ""),
|
||||
submitter = "Haki",
|
||||
comments = emptyList(),
|
||||
)
|
||||
}
|
||||
@@ -297,6 +283,7 @@ fun LobstersCardPreview() {
|
||||
return LinkMetadata("", "")
|
||||
}
|
||||
},
|
||||
refresh = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,9 +7,8 @@
|
||||
package dev.msfjarvis.claw.common.posts
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
@Stable
|
||||
interface PostActions {
|
||||
@@ -19,9 +18,9 @@ interface PostActions {
|
||||
|
||||
fun viewCommentsPage(commentsUrl: String)
|
||||
|
||||
fun toggleSave(post: SavedPost)
|
||||
fun toggleSave(post: UIPost)
|
||||
|
||||
suspend fun getComments(postId: String): LobstersPostDetails
|
||||
suspend fun getComments(postId: String): UIPost
|
||||
|
||||
suspend fun getLinkMetadata(url: String): LinkMetadata
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -23,7 +23,6 @@ import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.halilibo.richtext.ui.material3.SetupMaterial3RichText
|
||||
|
||||
val LightThemeColors =
|
||||
lightColorScheme(
|
||||
@@ -112,8 +111,6 @@ fun LobstersTheme(
|
||||
}
|
||||
}
|
||||
CompositionLocalProvider(*providedValues) {
|
||||
MaterialTheme(colorScheme = colorScheme, typography = AppTypography) {
|
||||
SetupMaterial3RichText { content() }
|
||||
}
|
||||
MaterialTheme(colorScheme = colorScheme, typography = AppTypography) { content() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.ui
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.LocalAbsoluteTonalElevation
|
||||
import androidx.compose.material3.NavigationBarDefaults
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
fun ColorScheme.surfaceColorAtNavigationBarElevation(): Color {
|
||||
val elevation = LocalAbsoluteTonalElevation.current + NavigationBarDefaults.Elevation
|
||||
return surfaceColorAtElevation(elevation)
|
||||
}
|
||||
@@ -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.
|
||||
@@ -15,18 +15,15 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.ui.RichText
|
||||
import com.halilibo.richtext.commonmark.Markdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
|
||||
@Composable
|
||||
internal fun ThemedRichText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
internal fun ThemedRichText(text: String, modifier: Modifier = Modifier) {
|
||||
val linkStyle =
|
||||
SpanStyle(
|
||||
background = MaterialTheme.colorScheme.surfaceVariant,
|
||||
@@ -39,10 +36,7 @@ internal fun ThemedRichText(
|
||||
LocalTextStyle provides MaterialTheme.typography.bodyLarge,
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
|
||||
) {
|
||||
RichText(
|
||||
modifier = modifier,
|
||||
style = RichTextStyle.Default.copy(stringStyle = stringStyle),
|
||||
) {
|
||||
RichText(modifier = modifier, style = RichTextStyle.Default.copy(stringStyle = stringStyle)) {
|
||||
Markdown(text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -29,11 +29,7 @@ import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
import io.github.aakira.napier.Napier
|
||||
|
||||
@Composable
|
||||
fun NetworkError(
|
||||
label: String,
|
||||
error: Throwable,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun NetworkError(label: String, error: Throwable, modifier: Modifier = Modifier) {
|
||||
LaunchedEffect(true) { Napier.e(error, "NetworkError") { "Failed to load posts" } }
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier) {
|
||||
@@ -60,15 +56,10 @@ fun NetworkError(
|
||||
Modifier.clickable {
|
||||
clipboard.setText(AnnotatedString(error.stackTraceToString()))
|
||||
showDialog = false
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = "${error.message}",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
text = { Text(text = "${error.message}", style = MaterialTheme.typography.bodyLarge) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -45,7 +45,7 @@ fun PasswordField(
|
||||
if (isPasswordVisible) rememberVectorPainter(Icons.Filled.VisibilityOff)
|
||||
else rememberVectorPainter(Icons.Filled.Visibility),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.clickable { isPasswordVisible = !isPasswordVisible }
|
||||
modifier = Modifier.clickable { isPasswordVisible = !isPasswordVisible },
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
@@ -57,10 +57,6 @@ fun PasswordField(
|
||||
private fun PasswordFieldPreview() {
|
||||
LobstersTheme {
|
||||
var value by remember { mutableStateOf("") }
|
||||
PasswordField(
|
||||
value = value,
|
||||
label = "Password",
|
||||
onValueChange = { value = it },
|
||||
)
|
||||
PasswordField(value = value, label = "Password", onValueChange = { value = it })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -59,10 +59,7 @@ fun SearchBar(
|
||||
},
|
||||
keyboardActions = KeyboardActions(onSearch = { onSearch(value) }),
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Search,
|
||||
),
|
||||
KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Search),
|
||||
singleLine = true,
|
||||
modifier = modifier.focusable().focusRequester(focusRequester),
|
||||
)
|
||||
@@ -73,7 +70,7 @@ fun SearchBar(
|
||||
@DevicePreviews
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun SearchBarPreview() {
|
||||
private fun SearchBarPreview() {
|
||||
LobstersTheme {
|
||||
Box(Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.background).padding(8.dp)) {
|
||||
var value by remember { mutableStateOf("") }
|
||||
@@ -81,7 +78,7 @@ fun SearchBarPreview() {
|
||||
value = value,
|
||||
onValueChange = { value = it },
|
||||
onSearch = {},
|
||||
modifier = Modifier.align(Alignment.TopCenter).fillMaxWidth()
|
||||
modifier = Modifier.align(Alignment.TopCenter).fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-15
@@ -1,17 +1,15 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.decorations
|
||||
package dev.msfjarvis.claw.common.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
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
@@ -20,7 +18,6 @@ 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)
|
||||
@@ -33,10 +30,6 @@ fun ClawAppBar(
|
||||
TopAppBar(
|
||||
title = title,
|
||||
modifier = modifier.shadow(8.dp),
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtNavigationBarElevation()
|
||||
),
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actions,
|
||||
)
|
||||
@@ -45,10 +38,6 @@ fun ClawAppBar(
|
||||
@DevicePreviews
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun ClawAppBarPreview() {
|
||||
LobstersTheme {
|
||||
ClawAppBar(
|
||||
title = { Text("Claw", fontWeight = FontWeight.Bold) },
|
||||
)
|
||||
}
|
||||
private fun ClawAppBarPreview() {
|
||||
LobstersTheme { ClawAppBar(title = { Text("Claw", fontWeight = FontWeight.Bold) }) }
|
||||
}
|
||||
@@ -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.
|
||||
@@ -15,28 +15,21 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
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
|
||||
import java.time.Month
|
||||
|
||||
@Composable
|
||||
fun MonthHeader(
|
||||
month: Month,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun MonthHeader(label: String, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtNavigationBarElevation())
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||
) {
|
||||
Text(
|
||||
text = month.name.lowercase().capitalize(Locale.current),
|
||||
text = label,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||
)
|
||||
@@ -46,6 +39,6 @@ fun MonthHeader(
|
||||
@DevicePreviews
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun MonthHeaderPreview() {
|
||||
LobstersTheme { MonthHeader(month = Month.APRIL) }
|
||||
private fun MonthHeaderPreview() {
|
||||
LobstersTheme { MonthHeader("April 2023") }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -50,12 +50,8 @@ fun UserProfile(
|
||||
.fold(
|
||||
success = { profile -> value = Success(profile) },
|
||||
failure = {
|
||||
value =
|
||||
Error(
|
||||
error = it,
|
||||
description = "Failed to load profile for $username",
|
||||
)
|
||||
}
|
||||
value = Error(error = it, description = "Failed to load profile for $username")
|
||||
},
|
||||
)
|
||||
}
|
||||
when (user) {
|
||||
@@ -81,10 +77,7 @@ fun UserProfile(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserProfileInternal(
|
||||
user: User,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
private fun UserProfileInternal(user: User, modifier: Modifier = Modifier) {
|
||||
Surface(modifier = modifier) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -97,17 +90,10 @@ private fun UserProfileInternal(
|
||||
contentDescription = "Avatar of ${user.username}",
|
||||
modifier = Modifier.requiredSize(120.dp).clip(CircleShape),
|
||||
)
|
||||
Text(
|
||||
text = user.username,
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
)
|
||||
ThemedRichText(
|
||||
text = user.about,
|
||||
)
|
||||
Text(text = user.username, style = MaterialTheme.typography.displaySmall)
|
||||
ThemedRichText(text = user.about)
|
||||
user.invitedBy?.let { invitedBy ->
|
||||
ThemedRichText(
|
||||
text = "Invited by [${invitedBy}](https://lobste.rs/u/${user.invitedBy})",
|
||||
)
|
||||
ThemedRichText(text = "Invited by [${invitedBy}](https://lobste.rs/u/${user.invitedBy})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -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,7 +7,6 @@
|
||||
package dev.msfjarvis.claw.core.injection
|
||||
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -18,6 +17,7 @@ import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import retrofit2.Converter
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
|
||||
@@ -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.
|
||||
@@ -18,7 +18,7 @@ import dagger.multibindings.IntoSet
|
||||
import dev.msfjarvis.claw.core.network.DelegatingSocketFactory
|
||||
import dev.msfjarvis.claw.core.network.NapierLogger
|
||||
import dev.msfjarvis.claw.core.network.UserAgentInterceptor
|
||||
import io.sentry.android.okhttp.SentryOkHttpInterceptor
|
||||
import io.sentry.okhttp.SentryOkHttpInterceptor
|
||||
import java.net.Socket
|
||||
import javax.net.SocketFactory
|
||||
import okhttp3.Cache
|
||||
@@ -92,7 +92,9 @@ interface OkHttpModule {
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideSentryInterceptor(): Interceptor {
|
||||
return SentryOkHttpInterceptor()
|
||||
// Disable capturing of failed requests since there is no real upstream for this app,
|
||||
// if something is going wrong it is almost always someone else's problem.
|
||||
return SentryOkHttpInterceptor(captureFailedRequests = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,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.
|
||||
@@ -29,7 +29,7 @@ open class DelegatingSocketFactory(private val delegate: SocketFactory) : Socket
|
||||
host: String,
|
||||
port: Int,
|
||||
localAddress: InetAddress,
|
||||
localPort: Int
|
||||
localPort: Int,
|
||||
): Socket {
|
||||
val socket = delegate.createSocket(host, port, localAddress, localPort)
|
||||
return configureSocket(socket)
|
||||
@@ -44,7 +44,7 @@ open class DelegatingSocketFactory(private val delegate: SocketFactory) : Socket
|
||||
host: InetAddress,
|
||||
port: Int,
|
||||
localAddress: InetAddress,
|
||||
localPort: Int
|
||||
localPort: Int,
|
||||
): Socket {
|
||||
val socket = delegate.createSocket(host, port, localAddress, localPort)
|
||||
return configureSocket(socket)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -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.
|
||||
@@ -32,7 +32,6 @@ object SavedPostSerializer : KSerializer<SavedPost> {
|
||||
element<Int?>("commentCount", isOptional = true)
|
||||
element<String>("commentsUrl")
|
||||
element<String>("submitterName")
|
||||
element<String>("submitterAvatarUrl")
|
||||
element<List<String>>("tags")
|
||||
element<String>("description")
|
||||
}
|
||||
@@ -46,7 +45,6 @@ object SavedPostSerializer : KSerializer<SavedPost> {
|
||||
var commentCount: Int? = null
|
||||
var commentsUrl = ""
|
||||
var submitterName = ""
|
||||
var submitterAvatarUrl = ""
|
||||
var tags = emptyList<String>()
|
||||
var description = ""
|
||||
while (true) {
|
||||
@@ -58,9 +56,8 @@ object SavedPostSerializer : KSerializer<SavedPost> {
|
||||
4 -> commentCount = decodeNullableSerializableElement(descriptor, 4, Int.serializer())
|
||||
5 -> commentsUrl = decodeStringElement(descriptor, 5)
|
||||
6 -> submitterName = decodeStringElement(descriptor, 6)
|
||||
7 -> submitterAvatarUrl = decodeStringElement(descriptor, 7)
|
||||
8 -> tags = decodeSerializableElement(descriptor, 8, delegateSerializer)
|
||||
9 -> description = decodeStringElement(descriptor, 9)
|
||||
7 -> tags = decodeSerializableElement(descriptor, 7, delegateSerializer)
|
||||
8 -> description = decodeStringElement(descriptor, 8)
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
else -> error("Unexpected index: $index")
|
||||
}
|
||||
@@ -73,7 +70,6 @@ object SavedPostSerializer : KSerializer<SavedPost> {
|
||||
commentCount = commentCount,
|
||||
commentsUrl = commentsUrl,
|
||||
submitterName = submitterName,
|
||||
submitterAvatarUrl = submitterAvatarUrl,
|
||||
tags = tags,
|
||||
description = description,
|
||||
)
|
||||
@@ -89,9 +85,8 @@ object SavedPostSerializer : KSerializer<SavedPost> {
|
||||
encodeNullableSerializableElement(descriptor, 4, Int.serializer(), value.commentCount)
|
||||
encodeStringElement(descriptor, 5, value.commentsUrl)
|
||||
encodeStringElement(descriptor, 6, value.submitterName)
|
||||
encodeStringElement(descriptor, 7, value.submitterAvatarUrl)
|
||||
encodeSerializableElement(descriptor, 8, delegateSerializer, value.tags)
|
||||
encodeStringElement(descriptor, 9, value.description)
|
||||
encodeSerializableElement(descriptor, 7, delegateSerializer, value.tags)
|
||||
encodeStringElement(descriptor, 8, value.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
CREATE TABLE ReadPosts(
|
||||
CREATE TABLE IF NOT EXISTS ReadPosts(
|
||||
id TEXT NOT NULL PRIMARY KEY
|
||||
);
|
||||
|
||||
isPostRead:
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM ReadPosts
|
||||
WHERE id = ?
|
||||
) AS isRead;
|
||||
|
||||
selectAllPosts:
|
||||
SELECT *
|
||||
FROM ReadPosts;
|
||||
|
||||
markRead:
|
||||
INSERT OR REPLACE
|
||||
INSERT OR IGNORE
|
||||
INTO ReadPosts(id)
|
||||
VALUES (?);
|
||||
|
||||
markUnread:
|
||||
DELETE FROM ReadPosts
|
||||
WHERE id = ?;
|
||||
|
||||
isRead:
|
||||
SELECT *
|
||||
FROM ReadPosts
|
||||
WHERE id = ?;
|
||||
|
||||
@@ -10,11 +10,17 @@ CREATE TABLE IF NOT EXISTS SavedPost(
|
||||
commentCount INTEGER AS Int,
|
||||
commentsUrl TEXT NOT NULL,
|
||||
submitterName TEXT NOT NULL,
|
||||
submitterAvatarUrl TEXT NOT NULL,
|
||||
tags TEXT AS List<String> NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT ""
|
||||
);
|
||||
|
||||
isPostSaved:
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM SavedPost
|
||||
WHERE shortId = ?
|
||||
) AS isSaved;
|
||||
|
||||
insertOrReplacePost:
|
||||
INSERT OR REPLACE
|
||||
INTO SavedPost
|
||||
@@ -36,8 +42,3 @@ deletePost:
|
||||
DELETE
|
||||
FROM SavedPost
|
||||
WHERE shortId = ?;
|
||||
|
||||
selectPost:
|
||||
SELECT *
|
||||
FROM SavedPost
|
||||
WHERE shortId = ?;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CREATE TABLE ReadPosts(
|
||||
CREATE TABLE IF NOT EXISTS ReadPosts(
|
||||
id TEXT NOT NULL PRIMARY KEY
|
||||
);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import kotlin.Int;
|
||||
import kotlin.String;
|
||||
import kotlin.collections.List;
|
||||
|
||||
ALTER TABLE SavedPost RENAME TO SavedPost_Old;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS SavedPost(
|
||||
shortId TEXT NOT NULL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
createdAt TEXT NOT NULL,
|
||||
commentCount INTEGER AS Int,
|
||||
commentsUrl TEXT NOT NULL,
|
||||
submitterName TEXT NOT NULL,
|
||||
tags TEXT AS List<String> NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT ""
|
||||
);
|
||||
|
||||
INSERT INTO SavedPost(shortId, title, url, createdAt, commentCount, commentsUrl, submitterName, tags, description)
|
||||
SELECT shortId, title, url, createdAt, commentCount, commentsUrl, submitterName, tags, description
|
||||
FROM SavedPost_Old;
|
||||
|
||||
DROP TABLE SavedPost_Old;
|
||||
+1
-2
@@ -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.
|
||||
@@ -52,7 +52,6 @@ class SavedPostSerializerTest {
|
||||
commentCount = 13,
|
||||
commentsUrl = "https://lobste.rs/s/nbigsf/fun_format_friday_you_now_have_super",
|
||||
submitterName = "LenFalken",
|
||||
submitterAvatarUrl = "/avatars/LenFalken-100.png",
|
||||
tags = listOf("ask", "programming"),
|
||||
description =
|
||||
"<p>You suddenly have in your possession a super computer. What comes next? What projects are suddenly possible for you? What performance tests can you now explore?</p>\n",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user