Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a41b465648 | ||
|
|
a7038600b3 | ||
|
|
3143556c63 | ||
|
|
5e2d9a2355 | ||
|
|
b4adfede90 | ||
|
|
ed85ba905d | ||
|
|
c29e4065d6 | ||
|
|
bcf4f02a01 | ||
|
|
72b9409383 | ||
|
|
2c762428af | ||
|
|
58e427c13a | ||
|
|
974f03ee89 | ||
|
|
f5e6b25ec6 | ||
|
|
be5f073acf | ||
|
|
ca6b941cbb | ||
|
|
1e5f830652 | ||
|
|
e72a5fda5c | ||
|
|
5c2fe6e630 | ||
|
|
5907c2a09a | ||
|
|
e3b381c665 | ||
|
|
f1d30e59b0 | ||
|
|
7669a85f43 | ||
|
|
c05db765c8 | ||
|
|
4c8e30bcc9 | ||
|
|
d32819b1ff | ||
|
|
a2d046bd0b | ||
|
|
3ad4ff1c63 | ||
|
|
1defffa504 | ||
|
|
0d78cb4112 | ||
|
|
7c59130af9 | ||
|
|
4137d73539 | ||
|
|
0f8a649e3b | ||
|
|
7e7a4bec83 | ||
|
|
b442b14cc1 | ||
|
|
97886f6ece | ||
|
|
7b9245e62c | ||
|
|
99aa62d0e4 | ||
|
|
c3cf8a8a39 | ||
|
|
340655f18a | ||
|
|
10fa84b09e | ||
|
|
f21a03b20b | ||
|
|
56394fc275 | ||
|
|
9fd2d9f17b | ||
|
|
0c0506b6d7 | ||
|
|
f94741144c | ||
|
|
29c0369d43 | ||
|
|
e700987d14 | ||
|
|
c5ec6cd680 | ||
|
|
e48a6d41d5 | ||
|
|
5870af4017 | ||
|
|
cb4c85b41c | ||
|
|
cd6a04c5fd | ||
|
|
401214794d | ||
|
|
fdd2f7008c | ||
|
|
18ef9c63b9 | ||
|
|
d343f3db2b | ||
|
|
42cc852a20 | ||
|
|
44be6eacbb | ||
|
|
778be597dd | ||
|
|
255439e2b4 | ||
|
|
63c3e26f5c | ||
|
|
d3c3e3ca08 | ||
|
|
8d6aba9865 | ||
|
|
5e91e20c51 | ||
|
|
84c9dd3762 | ||
|
|
24ef11b315 | ||
|
|
bdf218cd85 | ||
|
|
b3ff40b12b | ||
|
|
bad5bf5457 | ||
|
|
cf3ca27966 | ||
|
|
127a0674a2 | ||
|
|
b1a18c5dec | ||
|
|
0a39f56775 | ||
|
|
5daeb8d580 | ||
|
|
415ddae9f7 | ||
|
|
4c95680f3b | ||
|
|
b6cb9a2077 | ||
|
|
13eefbba2a | ||
|
|
6548f7d654 | ||
|
|
1e4217598f | ||
|
|
0359860b82 | ||
|
|
37f42dc107 | ||
|
|
7eb4b45ab1 | ||
|
|
3706fdd284 | ||
|
|
f1fa96f8ca | ||
|
|
36cadcdd78 | ||
|
|
cf3ac329f6 | ||
|
|
0581cedfb0 | ||
|
|
0fc5adc197 | ||
|
|
0680585b77 | ||
|
|
2a3514e3b5 | ||
|
|
fd8b65be72 | ||
|
|
50870b5fb5 | ||
|
|
7c5d935801 | ||
|
|
2862ada772 | ||
|
|
65d1457386 | ||
|
|
0f63611c2d | ||
|
|
7767f1c6d7 | ||
|
|
f351b60073 | ||
|
|
752dee8ed6 | ||
|
|
0573e8cb2d | ||
|
|
75fd41a0d8 | ||
|
|
59f8577d1f | ||
|
|
85d4b50d28 | ||
|
|
c3499d434b | ||
|
|
701e0d7fcc | ||
|
|
9a5a417418 | ||
|
|
1221992d26 | ||
|
|
144cdb5e50 | ||
|
|
8133b2c091 | ||
|
|
869cc650e3 | ||
|
|
7a3262338d | ||
|
|
85ff15e8ca | ||
|
|
3d34c4f466 | ||
|
|
6da6364a55 | ||
|
|
26b04cce16 | ||
|
|
9ab2400e1f | ||
|
|
c3b30cc86c | ||
|
|
2a97383e7d | ||
|
|
dd20885270 | ||
|
|
fe752f839a | ||
|
|
be9a888816 | ||
|
|
648e7b31cf | ||
|
|
21b520f786 | ||
|
|
8bbcabc8f6 | ||
|
|
0e4e344a2f | ||
|
|
7b25b6aa85 | ||
|
|
5343168b8a | ||
|
|
9b39fcf1fc | ||
|
|
8ce7ac96d0 | ||
|
|
b5c57500b1 | ||
|
|
c089e5ae5a | ||
|
|
d8a76cf1e7 | ||
|
|
92fce55feb | ||
|
|
60a430baa4 | ||
|
|
b0c8de888c | ||
|
|
73ab2dd1d3 | ||
|
|
7c4f843f42 | ||
|
|
123ec904e3 | ||
|
|
80c9211d37 | ||
|
|
b030694f63 | ||
|
|
e40e196354 | ||
|
|
b7abef3f8f | ||
|
|
5bde6cd9cb | ||
|
|
cb8c90ace9 | ||
|
|
9c2c238e1e | ||
|
|
21a7baa309 | ||
|
|
a9d75148fd | ||
|
|
3209b5b1fe | ||
|
|
c749a97ded | ||
|
|
f1ab468ceb | ||
|
|
ebd0e5cb5d | ||
|
|
19bf81ac0e | ||
|
|
569d5af8fb | ||
|
|
dc754dabb3 | ||
|
|
ed42532416 | ||
|
|
211f39da52 | ||
|
|
fefac25781 | ||
|
|
e0c9c88ca9 | ||
|
|
ea7f7b6306 | ||
|
|
8ceca6c5ed | ||
|
|
e9ff85ad44 | ||
|
|
b8e6d4d3fe | ||
|
|
e7fdfcab69 | ||
|
|
ad8c830b17 | ||
|
|
25d2d4ddb7 | ||
|
|
22d1755fd2 | ||
|
|
d397680d33 | ||
|
|
40cd7758dc | ||
|
|
2dc8eae861 | ||
|
|
2d23ceee83 | ||
|
|
cccc6fd64b | ||
|
|
aced95c542 | ||
|
|
90edeefd6e | ||
|
|
a5b90851c8 | ||
|
|
7d0c6972ef | ||
|
|
18cb5dc07c | ||
|
|
5343e13215 | ||
|
|
4d06a2722c | ||
|
|
b9f5bee35e | ||
|
|
72078b2768 | ||
|
|
465191609b | ||
|
|
6e1858a55e | ||
|
|
3eeb947914 | ||
|
|
d2c99fa8b7 | ||
|
|
dbba96426d | ||
|
|
a5be35a667 | ||
|
|
6e291a8df1 | ||
|
|
24898cf9fb | ||
|
|
66e64ac305 | ||
|
|
ee6bcc864f | ||
|
|
8de49d670b | ||
|
|
13b1c699f6 | ||
|
|
86a70ffb48 | ||
|
|
2bb71238bc | ||
|
|
7d491de86c | ||
|
|
02575a917b | ||
|
|
5206851b22 | ||
|
|
74c36f27ae | ||
|
|
2048a2bb9b | ||
|
|
ea48cdb4f3 | ||
|
|
942b9f3fb4 | ||
|
|
5f6d635c7a | ||
|
|
47a398f518 | ||
|
|
9fdd72220f | ||
|
|
0a1913e056 | ||
|
|
68db3dcda6 | ||
|
|
c50c9c8528 | ||
|
|
8a418f6649 | ||
|
|
acdc924047 | ||
|
|
2fa1d3f06f | ||
|
|
7ffa225487 | ||
|
|
c1659eb2dd | ||
|
|
1e888494ee | ||
|
|
054ba13dc3 | ||
|
|
301760c39f | ||
|
|
79fd814c14 | ||
|
|
a82d0e7063 | ||
|
|
c6ea1182e0 | ||
|
|
5e7b79eb89 | ||
|
|
3738d94789 | ||
|
|
fb0e372917 | ||
|
|
8945f9e33f | ||
|
|
2895e7ffe7 | ||
|
|
f102166fae | ||
|
|
c51e28b8e1 | ||
|
|
a9923eb8c0 | ||
|
|
de2f3964e0 | ||
|
|
7c5ccbf63b | ||
|
|
b4b96222ed | ||
|
|
a920f899fb | ||
|
|
31b39a49d6 | ||
|
|
bfb4e9741a | ||
|
|
bc5c5a13b4 | ||
|
|
19c50f40cd | ||
|
|
bb0a987d80 | ||
|
|
dba380e4c4 | ||
|
|
4a52524212 | ||
|
|
1a94ba5a40 | ||
|
|
2022255200 | ||
|
|
ab1951f9c5 | ||
|
|
a9176b8508 | ||
|
|
02ad998b06 | ||
|
|
31059abf23 |
Binary file not shown.
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 294 KiB |
+15
-30
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
":dependencyDashboard",
|
||||
@@ -15,24 +16,25 @@
|
||||
"https://maven.google.com/",
|
||||
"https://repo1.maven.org/maven2",
|
||||
"https://plugins.gradle.org/m2/",
|
||||
"https://jitpack.io",
|
||||
],
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"androidx.compose.compiler:compiler",
|
||||
"com.squareup.anvil",
|
||||
"dev.drewhamilton.poko",
|
||||
"org.jetbrains.kotlin:kotlin-gradle-plugin",
|
||||
"org.jetbrains.kotlin.plugin.serialization",
|
||||
],
|
||||
"groupName": "Kotlin and Compose",
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^androidx.*",
|
||||
],
|
||||
"groupName": "androidx"
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^kotlin",
|
||||
"^org.jetbrains.kotlin",
|
||||
"^androidx.compose.compiler",
|
||||
"^anvil",
|
||||
"^coil",
|
||||
],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^io.sentry",
|
||||
@@ -41,16 +43,10 @@
|
||||
"groupName": "sentry"
|
||||
},
|
||||
{
|
||||
"managers": [
|
||||
"gradle"
|
||||
],
|
||||
"packagePatterns": [
|
||||
// Renovate refuses to stop resolving this from the snapshots repo
|
||||
"^app.cash.sqldelight",
|
||||
// Currently on a pre-release version for bugfixes
|
||||
"matchPackageNames": [
|
||||
"com.android.tools:r8",
|
||||
],
|
||||
"enabled": false
|
||||
"enabled": false,
|
||||
},
|
||||
],
|
||||
"regexManagers": [
|
||||
@@ -76,16 +72,5 @@
|
||||
"depNameTemplate": "androidx.compose.compiler:compiler",
|
||||
"registryUrlTemplate": "https://maven.google.com",
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"gradle/libs.versions.toml"
|
||||
],
|
||||
"matchStrings": [
|
||||
"sentry-sdk = \"(?<currentValue>.*)\""
|
||||
],
|
||||
"datasourceTemplate": "maven",
|
||||
"depNameTemplate": "io.sentry:sentry-android",
|
||||
"registryUrlTemplate": "https://repo1.maven.org/maven2/",
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ on:
|
||||
- cron: '43 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
baseline-profile:
|
||||
runs-on: macos-latest
|
||||
@@ -15,28 +18,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Accept all SDK licenses
|
||||
shell: bash
|
||||
run: printf 'y\ny\ny\ny\ny\ny\n' | $ANDROID_HOME/tools/bin/sdkmanager --licenses
|
||||
|
||||
- name: Get build-tools directory
|
||||
id: build-tools-path
|
||||
shell: bash
|
||||
run: echo "dir=${ANDROID_HOME}/build-tools/34.0.0-rc3" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Cache build-tools
|
||||
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3
|
||||
with:
|
||||
path: ${{ steps.build-tools-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-34.0.0-rc3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
@@ -48,7 +36,7 @@ jobs:
|
||||
git config user.email noreply@github.com
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 # v2.4.2
|
||||
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
+10
-39
@@ -18,37 +18,23 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Accept all SDK licenses
|
||||
shell: bash
|
||||
run: printf 'y\ny\ny\ny\ny\ny\n' | $ANDROID_HOME/tools/bin/sdkmanager --licenses
|
||||
|
||||
- name: Get build-tools directory
|
||||
id: build-tools-path
|
||||
shell: bash
|
||||
run: echo "dir=${ANDROID_HOME}/build-tools/34.0.0-rc3" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Cache build-tools
|
||||
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3
|
||||
with:
|
||||
path: ${{ steps.build-tools-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-34.0.0-rc3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Run unit tests
|
||||
uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 # v2.4.2
|
||||
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
|
||||
with:
|
||||
arguments: --no-configuration-cache --stacktrace check
|
||||
arguments: --no-configuration-cache --stacktrace check -PslimTests
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
dependency-graph: generate-and-submit
|
||||
|
||||
- name: (Fail-only) Upload test report
|
||||
if: failure()
|
||||
@@ -63,25 +49,10 @@ jobs:
|
||||
needs: [ "check" ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
|
||||
- name: Accept all SDK licenses
|
||||
shell: bash
|
||||
run: printf 'y\ny\ny\ny\ny\ny\n' | $ANDROID_HOME/tools/bin/sdkmanager --licenses
|
||||
|
||||
- name: Get build-tools directory
|
||||
id: build-tools-path
|
||||
shell: bash
|
||||
run: echo "dir=${ANDROID_HOME}/build-tools/34.0.0-rc3" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Cache build-tools
|
||||
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3
|
||||
with:
|
||||
path: ${{ steps.build-tools-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-34.0.0-rc3
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
@@ -89,12 +60,12 @@ jobs:
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$ENCRYPT_KEY"
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
env:
|
||||
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
|
||||
- name: Build release app
|
||||
uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 # v2.4.2
|
||||
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
with:
|
||||
|
||||
@@ -2,9 +2,11 @@ name: CodeQL
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: '31 7 * * 3'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@@ -16,37 +18,22 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
|
||||
- name: Accept all SDK licenses
|
||||
shell: bash
|
||||
run: printf 'y\ny\ny\ny\ny\ny\n' | $ANDROID_HOME/tools/bin/sdkmanager --licenses
|
||||
|
||||
- name: Get build-tools directory
|
||||
id: build-tools-path
|
||||
shell: bash
|
||||
run: echo "dir=${ANDROID_HOME}/build-tools/34.0.0-rc3" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Cache build-tools
|
||||
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3
|
||||
with:
|
||||
path: ${{ steps.build-tools-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-34.0.0-rc3
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 # v2.4.2
|
||||
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5
|
||||
uses: github/codeql-action/init@6ca1aa8c195c3ca3e77c174fe0356db1bce3b319 # v2.21.1
|
||||
with:
|
||||
languages: java
|
||||
tools: latest
|
||||
@@ -55,9 +42,9 @@ jobs:
|
||||
- name: Build project
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew assemble
|
||||
./gradlew assemble -PslimTests
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5
|
||||
uses: github/codeql-action/analyze@6ca1aa8c195c3ca3e77c174fe0356db1bce3b319 # v2.21.1
|
||||
with:
|
||||
category: "/language:java"
|
||||
|
||||
@@ -2,33 +2,21 @@ name: Release to Google Play
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
publish-google-play-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Accept all SDK licenses
|
||||
shell: bash
|
||||
run: printf 'y\ny\ny\ny\ny\ny\n' | $ANDROID_HOME/tools/bin/sdkmanager --licenses
|
||||
|
||||
- name: Get build-tools directory
|
||||
id: build-tools-path
|
||||
shell: bash
|
||||
run: echo "dir=${ANDROID_HOME}/build-tools/34.0.0-rc3" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Cache build-tools
|
||||
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3
|
||||
with:
|
||||
path: ${{ steps.build-tools-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-34.0.0-rc3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
@@ -40,7 +28,7 @@ jobs:
|
||||
git config user.email noreply@github.com
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 # v2.4.2
|
||||
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
@@ -48,9 +36,9 @@ jobs:
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$ENCRYPT_KEY"
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
env:
|
||||
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
|
||||
- name: Get current version
|
||||
shell: bash
|
||||
@@ -60,7 +48,7 @@ jobs:
|
||||
echo VERSION="${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Update changelog
|
||||
uses: msfjarvis/keep-a-changelog-new-release@ce8769801992253adb0209055c236a0cd192500b # 2.0.0
|
||||
uses: msfjarvis/keep-a-changelog-new-release@bf43ae42f4e42f137572950af426efcf6e777207 # 2.1.0
|
||||
with:
|
||||
tag: v${{ env.VERSION }}
|
||||
version: ${{ env.VERSION }}
|
||||
|
||||
@@ -246,3 +246,4 @@ hs_err_pid*
|
||||
keystore.properties
|
||||
distribution/
|
||||
screenshots/
|
||||
claw-export.json
|
||||
|
||||
+58
-1
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.32.0] - 2023-07-27
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to AndroidX July 26 release
|
||||
* Revert selectable text feature due to sporadic crashes
|
||||
|
||||
## [1.31.0] - 2023-07-25
|
||||
|
||||
### Added
|
||||
|
||||
* Added the ability to search for posts
|
||||
* Text in the comments page is now selectable
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to Compose July release
|
||||
* Upgrade to Kotlin 1.9.0
|
||||
|
||||
## [1.30.0] - 2023-07-02
|
||||
|
||||
### Changed
|
||||
|
||||
* Added another workaround for native library loading crash
|
||||
|
||||
## [1.29.0] - 2023-06-08
|
||||
|
||||
### Added
|
||||
|
||||
* Backup and restore options for saved posts
|
||||
|
||||
### Fixed
|
||||
|
||||
* Spamming the comments button no longer causes it to be opened multiple times
|
||||
* Saved posts screen now has a visual indication when you have nothing saved
|
||||
* Iconography has been updated across the board to be more consistent
|
||||
|
||||
## [1.28.0] - 2023-06-03
|
||||
|
||||
### Changed
|
||||
|
||||
* Navigation transitions have been slightly sped up
|
||||
|
||||
### Fixed
|
||||
|
||||
* Add workaround for a native library loading crash observed on some devices
|
||||
|
||||
## [1.27.0] - 2023-05-31
|
||||
|
||||
### Changed
|
||||
@@ -222,7 +269,17 @@ 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.27.0...HEAD
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.32.0...HEAD
|
||||
|
||||
[1.32.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.31.0...v1.32.0
|
||||
|
||||
[1.31.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.30.0...v1.31.0
|
||||
|
||||
[1.30.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.29.0...v1.30.0
|
||||
|
||||
[1.29.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.28.0...v1.29.0
|
||||
|
||||
[1.28.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.27.0...v1.28.0
|
||||
|
||||
[1.27.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.26.0...v1.27.0
|
||||
|
||||
|
||||
@@ -11,10 +11,11 @@ Unofficial Android app for read-only access to [lobste.rs](https://lobste.rs), b
|
||||
> Snapshots from the development branch can be obtained [here](https://github.com/msfjarvis/compose-lobsters/releases/tag/nightly).
|
||||
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature.webp"
|
||||
alt="Side by side screenshots of the app's main page in dark and light UI modes"
|
||||
height="550" />
|
||||
alt="A grid of screenshots from the app, in clockwise order: the main screen, the comments page, the search page and the saved posts page"
|
||||
width="550" />
|
||||
|
||||
## Dependency Diagram
|
||||
|
||||
```mermaid
|
||||
%%{
|
||||
init: {
|
||||
@@ -28,16 +29,12 @@ graph LR
|
||||
android --> api
|
||||
android --> common
|
||||
android --> core
|
||||
android --> coroutine-utils
|
||||
android --> database
|
||||
android --> metadata-extractor
|
||||
android --> model
|
||||
api --> model
|
||||
common --> core
|
||||
common --> database
|
||||
common --> model
|
||||
metadata-extractor --> model
|
||||
database --> core
|
||||
|
||||
```
|
||||
## License
|
||||
|
||||
@@ -16,6 +16,7 @@ plugins {
|
||||
id("dev.msfjarvis.claw.sentry")
|
||||
id("dev.msfjarvis.claw.versioning-plugin")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.modulegraphassert)
|
||||
alias(libs.plugins.whetstone)
|
||||
}
|
||||
|
||||
@@ -36,6 +37,13 @@ android {
|
||||
applicationIdSuffix = ".benchmark"
|
||||
}
|
||||
}
|
||||
packaging { jniLibs { useLegacyPackaging = true } }
|
||||
}
|
||||
|
||||
moduleGraphAssert {
|
||||
assertOnAnyBuild = true
|
||||
maxHeight = 4
|
||||
restricted = arrayOf(":core -X> :.*")
|
||||
}
|
||||
|
||||
whetstone {
|
||||
@@ -60,7 +68,7 @@ dependencies {
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.coil)
|
||||
implementation(libs.copydown)
|
||||
implementation(libs.dagger)
|
||||
@@ -68,13 +76,13 @@ dependencies {
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.napier)
|
||||
implementation(libs.soloader)
|
||||
implementation(libs.sqldelight.extensions.coroutines)
|
||||
implementation(libs.unfurl)
|
||||
implementation(projects.api)
|
||||
implementation(projects.common)
|
||||
implementation(projects.core)
|
||||
implementation(projects.coroutineUtils)
|
||||
implementation(projects.database)
|
||||
implementation(projects.metadataExtractor)
|
||||
implementation(projects.model)
|
||||
|
||||
kapt(libs.dagger.compiler)
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
|
||||
<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
|
||||
|
||||
<issue
|
||||
id="UsingMaterialAndMaterial3Libraries"
|
||||
message="Using a material import while also using the material3 library"
|
||||
errorLine1="import androidx.compose.material.ExperimentalMaterialApi"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPosts.kt"
|
||||
line="17"
|
||||
column="8"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">Claw β</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
@@ -6,13 +11,13 @@
|
||||
<application
|
||||
android:name=".ClawApplication"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/full_backup_content"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Claw.SplashScreen"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
tools:targetApi="tiramisu">
|
||||
<profileable
|
||||
android:shell="true"
|
||||
|
||||
Generated
+24074
-16726
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 Harsh Shandilya.
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -15,6 +15,7 @@ import androidx.work.WorkManager
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.app.ApplicationComponentOwner
|
||||
import com.deliveryhero.whetstone.app.ContributesAppInjector
|
||||
import com.facebook.soloader.SoLoader
|
||||
import dev.msfjarvis.claw.android.work.SavedPostUpdaterWorker
|
||||
import dev.msfjarvis.claw.core.injection.AppPlugin
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -31,6 +32,7 @@ class ClawApplication : Application(), ApplicationComponentOwner {
|
||||
override fun onCreate() {
|
||||
Whetstone.inject(this)
|
||||
super.onCreate()
|
||||
SoLoader.init(this, false)
|
||||
plugins.forEach { plugin -> plugin.apply(this) }
|
||||
val postUpdateWorkRequest =
|
||||
PeriodicWorkRequestBuilder<SavedPostUpdaterWorker>(POST_REFRESH_PERIOD, TimeUnit.HOURS)
|
||||
|
||||
@@ -16,7 +16,7 @@ import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dev.msfjarvis.claw.util.coroutines.DispatcherProvider
|
||||
import dev.msfjarvis.claw.core.coroutines.DispatcherProvider
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 Harsh Shandilya.
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
|
||||
+24
-5
@@ -1,25 +1,44 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 Harsh Shandilya.
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
|
||||
import com.chimbori.crux.Crux
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.github.aakira.napier.Napier
|
||||
import me.saket.unfurl.UnfurlLogger
|
||||
import me.saket.unfurl.Unfurler
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
object MetadataExtractorModule {
|
||||
@Provides
|
||||
fun provideCrux(
|
||||
fun provideUnfurlLogger(): UnfurlLogger {
|
||||
return object : UnfurlLogger {
|
||||
override fun log(message: String) {
|
||||
Napier.d { message }
|
||||
}
|
||||
|
||||
override fun log(e: Throwable, message: String) {
|
||||
Napier.e(e) { message }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideUnfurler(
|
||||
okHttpClient: OkHttpClient,
|
||||
): Crux {
|
||||
return Crux(okHttpClient = okHttpClient)
|
||||
logger: UnfurlLogger,
|
||||
): Unfurler {
|
||||
return Unfurler(
|
||||
httpClient = okHttpClient,
|
||||
logger = logger,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class DatabaseDispatcher
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class MainDispatcher
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class IODispatcher
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 Harsh Shandilya.
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -13,7 +13,7 @@ import com.slack.eithernet.ApiResult.Success
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dev.msfjarvis.claw.android.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.paging
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dagger.assisted.Assisted
|
||||
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.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Duplicate of [LobstersPagingSource] with an additional optimization to not continue loading
|
||||
* further pages if nothing is found in the current one. This is required to prevent the app from
|
||||
* hammering the search endpoint with up to 70 consecutive requests for the search results of a
|
||||
* blank query.
|
||||
*/
|
||||
class SearchPagingSource
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : PagingSource<Int, LobstersPost>() {
|
||||
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
|
||||
val page = params.key ?: STARTING_PAGE_INDEX
|
||||
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
|
||||
is ApiResult.Success -> {
|
||||
// Optimization: prevent fetching more pages if we found no items, this means
|
||||
// there is no active search query.
|
||||
val nextKey = if (result.value.isEmpty()) null else page + 1
|
||||
LoadResult.Page(
|
||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||
data = result.value,
|
||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||
nextKey = nextKey,
|
||||
)
|
||||
}
|
||||
is ApiResult.Failure.NetworkFailure -> LoadResult.Error(result.error)
|
||||
is ApiResult.Failure.UnknownFailure -> LoadResult.Error(result.error)
|
||||
is ApiResult.Failure.HttpFailure,
|
||||
is ApiResult.Failure.ApiFailure ->
|
||||
LoadResult.Error(IOException("API returned an invalid response"))
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(remoteFetcher: RemoteFetcher<LobstersPost>): SearchPagingSource
|
||||
}
|
||||
}
|
||||
@@ -7,29 +7,37 @@
|
||||
package dev.msfjarvis.claw.android.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.ImportExport
|
||||
import androidx.compose.material.icons.filled.NewReleases
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Whatshot
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.NavigateBefore
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material.icons.outlined.Whatshot
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -46,12 +54,14 @@ import androidx.navigation.navDeepLink
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.android.ui.datatransfer.DataTransferScreen
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
|
||||
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
|
||||
import dev.msfjarvis.claw.android.ui.decorations.TransparentSystemBars
|
||||
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
|
||||
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
|
||||
import dev.msfjarvis.claw.android.ui.lists.SearchList
|
||||
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
@@ -79,8 +89,10 @@ fun LobstersApp(
|
||||
val hottestListState = rememberLazyListState()
|
||||
val newestListState = rememberLazyListState()
|
||||
val savedListState = rememberLazyListState()
|
||||
val searchListState = rememberLazyListState()
|
||||
val navController = rememberNavController()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val postActions = rememberPostActions(urlLauncher, navController, viewModel)
|
||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentDestination = backStackEntry?.destination?.route
|
||||
@@ -101,52 +113,75 @@ fun LobstersApp(
|
||||
NavigationItem(
|
||||
label = "Hottest",
|
||||
route = Destinations.Hottest.route,
|
||||
icon = rememberVectorPainter(Icons.Outlined.Whatshot),
|
||||
selectedIcon = rememberVectorPainter(Icons.Filled.Whatshot),
|
||||
icon = Icons.Outlined.Whatshot,
|
||||
selectedIcon = Icons.Filled.Whatshot,
|
||||
) {
|
||||
coroutineScope.launch { hottestListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Newest",
|
||||
route = Destinations.Newest.route,
|
||||
icon = rememberVectorPainter(Icons.Outlined.NewReleases),
|
||||
selectedIcon = rememberVectorPainter(Icons.Filled.NewReleases),
|
||||
icon = Icons.Outlined.NewReleases,
|
||||
selectedIcon = Icons.Filled.NewReleases,
|
||||
) {
|
||||
coroutineScope.launch { newestListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Saved",
|
||||
route = Destinations.Saved.route,
|
||||
icon = rememberVectorPainter(Icons.Outlined.FavoriteBorder),
|
||||
selectedIcon = rememberVectorPainter(Icons.Filled.Favorite),
|
||||
icon = Icons.Outlined.FavoriteBorder,
|
||||
selectedIcon = Icons.Filled.Favorite,
|
||||
) {
|
||||
coroutineScope.launch { savedListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Search",
|
||||
route = Destinations.Search.route,
|
||||
icon = Icons.Outlined.Search,
|
||||
selectedIcon = Icons.Filled.Search,
|
||||
) {
|
||||
coroutineScope.launch { searchListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
)
|
||||
|
||||
TransparentSystemBars()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
ClawAppBar(
|
||||
navigationIcon = {
|
||||
if (navItems.none { it.route == currentDestination }) {
|
||||
IconButton(
|
||||
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() }
|
||||
if (currentDestination != Destinations.Search.route) {
|
||||
ClawAppBar(
|
||||
navigationIcon = {
|
||||
if (
|
||||
navController.previousBackStackEntry != null &&
|
||||
navItems.none { it.route == currentDestination }
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(Icons.Outlined.NavigateBefore),
|
||||
contentDescription = "Go back to previous screen",
|
||||
)
|
||||
IconButton(
|
||||
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = "Go back to previous screen",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
title = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
IconButton(onClick = { navController.navigate(Destinations.DataTransfer.route) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ImportExport,
|
||||
contentDescription = "Data transfer options",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
|
||||
@@ -157,6 +192,7 @@ fun LobstersApp(
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
modifier = modifier.semantics { testTagsAsResourceId = true },
|
||||
) { paddingValues ->
|
||||
Row(modifier = Modifier.padding(paddingValues)) {
|
||||
@@ -171,6 +207,9 @@ fun LobstersApp(
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Destinations.startDestination.route,
|
||||
// Make animations 2x faster than default specs
|
||||
enterTransition = { fadeIn(animationSpec = tween(350)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(350)) },
|
||||
) {
|
||||
val uri = LobstersApi.BASE_URL
|
||||
composable(route = Destinations.Hottest.route) {
|
||||
@@ -199,6 +238,17 @@ fun LobstersApp(
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
composable(Destinations.Search.route) {
|
||||
setWebUri("https://lobste.rs/search")
|
||||
SearchList(
|
||||
items = viewModel.searchResults,
|
||||
listState = searchListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
postActions = postActions,
|
||||
searchQuery = viewModel.searchQuery,
|
||||
setSearchQuery = { query -> viewModel.searchQuery = query },
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Destinations.Comments.route,
|
||||
arguments = listOf(navArgument("postId") { type = NavType.StringType }),
|
||||
@@ -231,6 +281,14 @@ fun LobstersApp(
|
||||
getProfile = viewModel::getUserProfile,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.DataTransfer.route) {
|
||||
DataTransferScreen(
|
||||
context = context,
|
||||
importPosts = viewModel::importPosts,
|
||||
exportPosts = viewModel::exportPosts,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
+4
-4
@@ -19,7 +19,7 @@ import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.navigation.NavController
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
@@ -57,7 +57,7 @@ fun ClawNavigationBar(
|
||||
icon = {
|
||||
Crossfade(isCurrentDestination, label = "nav-label") {
|
||||
Icon(
|
||||
painter = if (it) navItem.selectedIcon else navItem.icon,
|
||||
imageVector = if (it) navItem.selectedIcon else navItem.icon,
|
||||
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
|
||||
)
|
||||
}
|
||||
@@ -86,7 +86,7 @@ fun ClawNavigationBar(
|
||||
class NavigationItem(
|
||||
val label: String,
|
||||
val route: String,
|
||||
val icon: Painter,
|
||||
val selectedIcon: Painter,
|
||||
val icon: ImageVector,
|
||||
val selectedIcon: ImageVector,
|
||||
val listStateResetCallback: () -> Unit,
|
||||
)
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ fun ClawNavigationRail(
|
||||
icon = {
|
||||
Crossfade(isCurrentDestination, label = "nav-label") {
|
||||
Icon(
|
||||
painter = if (it) navItem.selectedIcon else navItem.icon,
|
||||
imageVector = if (it) navItem.selectedIcon else navItem.icon,
|
||||
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -19,8 +19,6 @@ import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
fun Context.getActivity(): ComponentActivity? {
|
||||
return when (this) {
|
||||
@@ -30,14 +28,6 @@ fun Context.getActivity(): ComponentActivity? {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 solution.
|
||||
*/
|
||||
fun String.toLocalDateTime(): LocalDateTime {
|
||||
return LocalDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(this))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberPostActions(
|
||||
urlLauncher: UrlLauncher,
|
||||
@@ -51,9 +41,10 @@ fun rememberPostActions(
|
||||
}
|
||||
|
||||
override fun viewComments(postId: String) {
|
||||
navController.navigate(
|
||||
val currentRoute = navController.currentDestination?.route
|
||||
val newRoute =
|
||||
Destinations.Comments.route.replace(Destinations.Comments.placeholder, postId)
|
||||
)
|
||||
if (currentRoute != Destinations.Comments.route) navController.navigate(newRoute)
|
||||
}
|
||||
|
||||
override fun viewCommentsPage(commentsUrl: String) {
|
||||
|
||||
@@ -6,15 +6,25 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.lists
|
||||
|
||||
import androidx.activity.compose.ReportDrawn
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Inbox
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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
|
||||
@@ -29,23 +39,33 @@ fun DatabasePosts(
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ReportDrawn()
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
) {
|
||||
items.forEach { (month, posts) ->
|
||||
stickyHeader(contentType = "month-header") { MonthHeader(month = month) }
|
||||
items(
|
||||
items = posts,
|
||||
key = { it.shortId },
|
||||
contentType = { "LobstersItem" },
|
||||
) { item ->
|
||||
ListItem(
|
||||
item = item,
|
||||
isSaved = { true },
|
||||
postActions = postActions,
|
||||
)
|
||||
Divider()
|
||||
if (items.isEmpty()) {
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Inbox,
|
||||
contentDescription = "Empty inbox icon",
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally).size(36.dp),
|
||||
)
|
||||
Text(text = "No saved posts", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(state = listState) {
|
||||
items.forEach { (month, posts) ->
|
||||
stickyHeader(contentType = "month-header") { MonthHeader(month = month) }
|
||||
items(
|
||||
items = posts,
|
||||
key = { it.shortId },
|
||||
contentType = { "LobstersItem" },
|
||||
) { item ->
|
||||
ListItem(
|
||||
item = item,
|
||||
isSaved = { true },
|
||||
postActions = postActions,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.lists
|
||||
|
||||
import androidx.activity.compose.ReportDrawnWhen
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -17,7 +18,7 @@ import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -44,6 +45,7 @@ fun NetworkPosts(
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ReportDrawnWhen { lazyPagingItems.itemCount > 0 }
|
||||
val refreshLoadState = lazyPagingItems.loadState.refresh
|
||||
val isRefreshing = refreshLoadState == LoadState.Loading
|
||||
val pullRefreshState = rememberPullRefreshState(isRefreshing, lazyPagingItems::refresh)
|
||||
@@ -72,7 +74,7 @@ fun NetworkPosts(
|
||||
postActions = postActions,
|
||||
)
|
||||
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
if (lazyPagingItems.loadState.append == LoadState.Loading) {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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.lists
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SearchOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.isTraversalGroup
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Composable
|
||||
fun SearchList(
|
||||
items: Flow<PagingData<LobstersPost>>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: suspend (SavedPost) -> Boolean,
|
||||
postActions: PostActions,
|
||||
searchQuery: String,
|
||||
setSearchQuery: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val lazyPagingItems = items.collectAsLazyPagingItems()
|
||||
val triggerSearch = { query: String ->
|
||||
setSearchQuery(query)
|
||||
lazyPagingItems.refresh()
|
||||
}
|
||||
DisposableEffect(true) {
|
||||
// Clear search field when navigating away
|
||||
onDispose { setSearchQuery("") }
|
||||
}
|
||||
|
||||
var searchActive by remember { mutableStateOf(false) }
|
||||
Column(modifier = Modifier.semantics { isTraversalGroup = true }.zIndex(1f).fillMaxWidth()) {
|
||||
SearchBar(
|
||||
value = searchQuery,
|
||||
onValueChange = setSearchQuery,
|
||||
onSearch = {
|
||||
triggerSearch(it)
|
||||
searchActive = true
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp).testTag("search_bar"),
|
||||
)
|
||||
if (searchActive) {
|
||||
NetworkPosts(
|
||||
lazyPagingItems = lazyPagingItems,
|
||||
listState = listState,
|
||||
isPostSaved = isPostSaved,
|
||||
postActions = postActions,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.SearchOff,
|
||||
contentDescription = "No search results",
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally).size(36.dp),
|
||||
)
|
||||
Text(text = "Nothing to see here", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 Harsh Shandilya.
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -21,6 +21,10 @@ sealed class Destinations {
|
||||
override val route = "saved"
|
||||
}
|
||||
|
||||
object Search : Destinations() {
|
||||
override val route = "search"
|
||||
}
|
||||
|
||||
object Comments : Destinations() {
|
||||
const val placeholder = "{postId}"
|
||||
override val route = "comments/$placeholder"
|
||||
@@ -31,6 +35,10 @@ sealed class Destinations {
|
||||
override val route = "user/$placeholder"
|
||||
}
|
||||
|
||||
object DataTransfer : Destinations() {
|
||||
override val route: String = "datatransfer"
|
||||
}
|
||||
|
||||
companion object {
|
||||
val startDestination
|
||||
get() = Hottest
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* 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.
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.android.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.api.injection.BaseUrl
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -24,7 +24,7 @@ constructor(
|
||||
@BaseUrl private val url: String,
|
||||
) {
|
||||
suspend fun extractToken(): String? {
|
||||
val request = Request.Builder().url(url).get().build()
|
||||
val request = Request.Builder().url(url).build()
|
||||
return withContext(dispatcher) {
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
val doc = Jsoup.parse(response.body?.string() ?: return@use null)
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
@@ -13,17 +16,23 @@ import androidx.paging.PagingConfig
|
||||
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
|
||||
import com.slack.eithernet.ApiResult.Failure
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import dev.msfjarvis.claw.android.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
|
||||
import dev.msfjarvis.claw.android.ui.toLocalDateTime
|
||||
import dev.msfjarvis.claw.android.paging.SearchPagingSource
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.time.LocalDateTime
|
||||
import java.time.Month
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
@@ -38,9 +47,13 @@ class ClawViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val api: LobstersApi,
|
||||
private val searchApi: LobstersSearchApi,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val commentsRepository: CommentsRepository,
|
||||
private val linkMetadataRepository: LinkMetadataRepository,
|
||||
private val dataTransferRepository: DataTransferRepository,
|
||||
private val pagingSourceFactory: LobstersPagingSource.Factory,
|
||||
private val searchPagingSourceFactory: SearchPagingSource.Factory,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
private val hottestPostsPager =
|
||||
@@ -51,6 +64,10 @@ constructor(
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
pagingSourceFactory.create(api::getNewestPosts)
|
||||
}
|
||||
private val searchResultsPager =
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
searchPagingSourceFactory.create { searchApi.searchPosts(searchQuery, it) }
|
||||
}
|
||||
|
||||
val hottestPosts
|
||||
get() = hottestPostsPager.flow
|
||||
@@ -64,6 +81,11 @@ constructor(
|
||||
val savedPosts
|
||||
get() = savedPostsFlow.map(::mapSavedPosts)
|
||||
|
||||
val searchResults
|
||||
get() = searchResultsPager.flow
|
||||
|
||||
var searchQuery by mutableStateOf("")
|
||||
|
||||
private fun mapSavedPosts(items: List<SavedPost>): ImmutableMap<Month, List<SavedPost>> {
|
||||
val sorted = items.sortedByDescending { post -> post.createdAt.toLocalDateTime() }
|
||||
return sorted.groupBy { post -> post.createdAt.toLocalDateTime().month }.toImmutableMap()
|
||||
@@ -101,10 +123,10 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSeenComments(postId: String) = savedPostsRepository.getSeenComments(postId)
|
||||
suspend fun getSeenComments(postId: String) = commentsRepository.getSeenComments(postId)
|
||||
|
||||
fun markSeenComments(postId: String, comments: List<Comment>) {
|
||||
viewModelScope.launch { savedPostsRepository.markSeenComments(postId, comments) }
|
||||
viewModelScope.launch { commentsRepository.markSeenComments(postId, comments) }
|
||||
}
|
||||
|
||||
suspend fun getLinkMetadata(url: String) =
|
||||
@@ -120,4 +142,18 @@ constructor(
|
||||
is Failure.ApiFailure -> throw IOException("API returned an invalid response")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun importPosts(input: InputStream) = dataTransferRepository.importPosts(input)
|
||||
|
||||
suspend fun exportPosts(output: OutputStream) = dataTransferRepository.exportPosts(output)
|
||||
|
||||
/**
|
||||
* 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
|
||||
* solution.
|
||||
*/
|
||||
private fun String.toLocalDateTime(): LocalDateTime {
|
||||
if (isEmpty()) return LocalDateTime.now(ZoneId.systemDefault())
|
||||
return LocalDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(this))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.database.LobstersDatabase
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class CommentsRepository
|
||||
@Inject
|
||||
constructor(
|
||||
database: LobstersDatabase,
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val postCommentsQueries = database.postCommentsQueries
|
||||
|
||||
suspend fun getSeenComments(postId: String) =
|
||||
withContext(dbDispatcher) { postCommentsQueries.getCommentIds(postId).executeAsOneOrNull() }
|
||||
|
||||
suspend fun markSeenComments(postId: String, comments: List<Comment>) {
|
||||
withContext(dbDispatcher) {
|
||||
postCommentsQueries.rememberComments(PostComments(postId, comments.map { it.shortId }))
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.database.LobstersDatabase
|
||||
import dev.msfjarvis.claw.database.SavedPostSerializer
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.encodeToStream
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
class DataTransferRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val database: LobstersDatabase,
|
||||
private val json: Json,
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val serializer = ListSerializer(SavedPostSerializer)
|
||||
private val savedPostQueries = database.savedPostQueries
|
||||
|
||||
suspend fun importPosts(input: InputStream) {
|
||||
val posts: List<SavedPost> =
|
||||
withContext(ioDispatcher) {
|
||||
json.decodeFromStream(
|
||||
serializer,
|
||||
input,
|
||||
)
|
||||
}
|
||||
withContext(dbDispatcher) {
|
||||
database.transaction { posts.forEach(savedPostQueries::insertOrReplacePost) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun exportPosts(output: OutputStream) {
|
||||
val posts = withContext(dbDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
|
||||
withContext(ioDispatcher) { json.encodeToStream(serializer, posts, output) }
|
||||
}
|
||||
}
|
||||
+9
-4
@@ -1,21 +1,26 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import dev.msfjarvis.claw.metadata.MetadataExtractor
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.saket.unfurl.Unfurler
|
||||
|
||||
class LinkMetadataRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val metadataExtractor: MetadataExtractor,
|
||||
private val unfurler: Unfurler,
|
||||
@IODispatcher private val dispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
suspend fun getLinkMetadata(url: String): LinkMetadata {
|
||||
return metadataExtractor.getExtractedMetadata(url)
|
||||
val result = withContext(dispatcher) { unfurler.unfurl(url) }
|
||||
return LinkMetadata(url, result?.favicon?.toString())
|
||||
}
|
||||
}
|
||||
|
||||
+1
-13
@@ -8,11 +8,9 @@ package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import dev.msfjarvis.claw.android.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.database.LobstersDatabase
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import io.github.aakira.napier.Napier
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -25,7 +23,6 @@ constructor(
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val savedPostQueries = database.savedPostQueries
|
||||
private val postCommentsQueries = database.postCommentsQueries
|
||||
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
|
||||
|
||||
suspend fun savePost(post: SavedPost) {
|
||||
@@ -47,15 +44,6 @@ constructor(
|
||||
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
|
||||
}
|
||||
|
||||
suspend fun getSeenComments(postId: String) =
|
||||
withContext(dbDispatcher) { postCommentsQueries.getCommentIds(postId).executeAsOneOrNull() }
|
||||
|
||||
suspend fun markSeenComments(postId: String, comments: List<Comment>) {
|
||||
withContext(dbDispatcher) {
|
||||
postCommentsQueries.rememberComments(PostComments(postId, comments.map { it.shortId }))
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "SavedPostsRepository"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -41,7 +41,6 @@ constructor(
|
||||
.map { post -> lobstersApi.getPostDetails(post.shortId) }
|
||||
.filterIsInstance<Success<LobstersPostDetails>>()
|
||||
.map { result -> result.value.toDbModel() }
|
||||
.toList()
|
||||
.let { savedPostsRepository.savePosts(it) }
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
<bool name="use_light_status_bar">false</bool>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
<bool name="use_light_status_bar">true</bool>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">Claw</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Claw.SplashScreen" parent="Theme.SplashScreen">
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<include
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<full-backup-content>
|
||||
<include
|
||||
domain="database"
|
||||
|
||||
@@ -8,7 +8,6 @@ package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
@@ -18,7 +17,6 @@ import okhttp3.mockwebserver.RecordedRequest
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class CSRFRepositoryTest {
|
||||
@Test
|
||||
fun `correctly extracts CSRF token`() = runTest {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
|
||||
#
|
||||
versioning-plugin.versionCode=12700
|
||||
versioning-plugin.versionName=1.27.0
|
||||
versioning-plugin.versionCode=13200
|
||||
versioning-plugin.versionName=1.32.0
|
||||
|
||||
@@ -24,6 +24,7 @@ dependencies {
|
||||
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.javax.inject)
|
||||
implementation(libs.jsoup)
|
||||
|
||||
testImplementation(testFixtures(libs.eithernet))
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
|
||||
<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -9,7 +9,6 @@ package dev.msfjarvis.claw.api
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.Tags
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
@@ -30,12 +29,6 @@ interface LobstersApi {
|
||||
@GET("u/{username}.json")
|
||||
suspend fun getUser(@Path("username") username: String): ApiResult<User, Unit>
|
||||
|
||||
@GET("t/{tags}.json")
|
||||
suspend fun getPostsByTags(
|
||||
@Path("tags") tag: Tags,
|
||||
@Query("page") page: Int,
|
||||
): ApiResult<List<LobstersPost>, Unit>
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://lobste.rs"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.api
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface LobstersSearchApi {
|
||||
@GET("/search?what=stories&order=newest")
|
||||
suspend fun searchPosts(
|
||||
@Query("q") searchQuery: String,
|
||||
@Query("page") page: Int,
|
||||
): ApiResult<List<LobstersPost>, Unit>
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.api.converters
|
||||
|
||||
import dev.msfjarvis.claw.api.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
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
|
||||
object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
|
||||
override fun convert(value: ResponseBody): List<LobstersPost> {
|
||||
val elements =
|
||||
Jsoup.parse(value.string(), LobstersApi.BASE_URL).select("div.story_liner.h-entry")
|
||||
return elements.map(::parsePost)
|
||||
}
|
||||
|
||||
private fun parsePost(elem: Element): LobstersPost {
|
||||
val parent = elem.parent() ?: error("$elem must have a parent")
|
||||
val shortId = parent.attr("data-shortid")
|
||||
val titleElement = elem.select("span.link.h-cite > a")
|
||||
val title = titleElement.text()
|
||||
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"))
|
||||
return LobstersPost(
|
||||
shortId = shortId,
|
||||
title = title,
|
||||
url = url,
|
||||
commentCount = commentCount,
|
||||
commentsUrl = commentsUrl,
|
||||
tags = tags,
|
||||
submitter = submitter,
|
||||
// The value of these fields is irrelevant for our use case
|
||||
createdAt = "",
|
||||
description = "",
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCommentsData(elem: Elements): Pair<Int, String> {
|
||||
val linkElement = elem.select("a")
|
||||
val countString = linkElement.text().trimStart().substringBefore(" ")
|
||||
val commentsUrl = linkElement.attr("href")
|
||||
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
|
||||
): Converter<ResponseBody, List<LobstersPost>> {
|
||||
return SearchConverter
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.api.injection
|
||||
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.slack.eithernet.ApiResultCallAdapterFactory
|
||||
import com.slack.eithernet.ApiResultConverterFactory
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import javax.inject.Qualifier
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
object ApiModule {
|
||||
@Provides
|
||||
fun provideRetrofit(
|
||||
client: OkHttpClient,
|
||||
converterFactories: Set<@JvmSuppressWildcards Converter.Factory>,
|
||||
@BaseUrl baseUrl: String,
|
||||
): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
.baseUrl(baseUrl)
|
||||
.addConverterFactory(ApiResultConverterFactory)
|
||||
.addCallAdapterFactory(ApiResultCallAdapterFactory)
|
||||
.apply { converterFactories.forEach(this::addConverterFactory) }
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideApi(retrofit: Retrofit): LobstersApi {
|
||||
return retrofit.create()
|
||||
}
|
||||
|
||||
@Provides @BaseUrl fun provideBaseUrl(): String = LobstersApi.BASE_URL
|
||||
}
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class BaseUrl
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.injection
|
||||
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.slack.eithernet.ApiResultCallAdapterFactory
|
||||
import com.slack.eithernet.ApiResultConverterFactory
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.api.converters.SearchConverter
|
||||
import javax.inject.Qualifier
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.CallAdapter
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
|
||||
/**
|
||||
* Ideally the multibindings used here would only use [dagger.multibindings.IntoSet], but its lack
|
||||
* of ordering guarantees means that we roll a die on each app launch that [Converter]s and
|
||||
* [CallAdapter]s are in the correct order to be able to deserialize responses. Thus, the module
|
||||
* uses [IntoMap] with [IntKey]s to fake the presence of a fixed order by sorting on the key of the
|
||||
* injected [Map]s when injecting them into [Retrofit].
|
||||
*/
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
object RetrofitModule {
|
||||
@Provides
|
||||
fun provideRetrofit(
|
||||
client: OkHttpClient,
|
||||
converterFactories: Map<Int, @JvmSuppressWildcards Converter.Factory>,
|
||||
callAdapterFactories: Map<Int, @JvmSuppressWildcards CallAdapter.Factory>,
|
||||
@BaseUrl baseUrl: String,
|
||||
): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
.baseUrl(baseUrl)
|
||||
.apply { converterFactories.toSortedMap().values.forEach(this::addConverterFactory) }
|
||||
.apply { callAdapterFactories.toSortedMap().values.forEach(this::addCallAdapterFactory) }
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@SearchApi
|
||||
fun provideSearchApiRetrofit(
|
||||
client: OkHttpClient,
|
||||
@SearchApi converterFactories: List<@JvmSuppressWildcards Converter.Factory>,
|
||||
callAdapterFactories: Map<Int, @JvmSuppressWildcards CallAdapter.Factory>,
|
||||
@BaseUrl baseUrl: String,
|
||||
): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
.baseUrl(baseUrl)
|
||||
.apply { converterFactories.forEach(this::addConverterFactory) }
|
||||
.apply { callAdapterFactories.toSortedMap().values.forEach(this::addCallAdapterFactory) }
|
||||
.addConverterFactory(SearchConverter.Factory)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides fun provideApi(retrofit: Retrofit): LobstersApi = retrofit.create()
|
||||
|
||||
@Provides
|
||||
fun provideSearchApi(@SearchApi retrofit: Retrofit): LobstersSearchApi = retrofit.create()
|
||||
|
||||
@Provides
|
||||
@IntKey(0)
|
||||
@IntoMap
|
||||
fun provideApiResultConverter(): Converter.Factory = ApiResultConverterFactory
|
||||
|
||||
@Provides
|
||||
@IntKey(0)
|
||||
@IntoMap
|
||||
fun provideApiResultCallAdapter(): CallAdapter.Factory = ApiResultCallAdapterFactory
|
||||
|
||||
@Provides
|
||||
@SearchApi
|
||||
fun provideConverters(): List<@JvmSuppressWildcards Converter.Factory> =
|
||||
listOf(
|
||||
ApiResultConverterFactory,
|
||||
SearchConverter.Factory,
|
||||
)
|
||||
|
||||
@Provides @BaseUrl fun provideBaseUrl(): String = LobstersApi.BASE_URL
|
||||
}
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class BaseUrl
|
||||
|
||||
/** Internal Dagger [Qualifier] to identify dependencies exclusive to [LobstersSearchApi]. */
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) private annotation class SearchApi
|
||||
@@ -11,14 +11,11 @@ import com.slack.eithernet.ApiResult.Success
|
||||
import com.slack.eithernet.test.newEitherNetController
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.Tags
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import dev.msfjarvis.claw.util.TestUtils.assertIs
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ApiTest {
|
||||
private val wrapper = ApiWrapper(newEitherNetController())
|
||||
private val api
|
||||
@@ -52,25 +49,4 @@ class ApiTest {
|
||||
assertIs<Success<User>>(user)
|
||||
assertThat(user.value.username).isEqualTo("msfjarvis")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get posts by single tag`() = runTest {
|
||||
val tags = Tags()
|
||||
tags.addTag("meta")
|
||||
val posts = api.getPostsByTags(tags, 1)
|
||||
assertIs<Success<List<LobstersPost>>>(posts)
|
||||
assertThat(posts.value).hasSize(25)
|
||||
assertThat(posts.value[0].tags).contains("meta")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get posts by multiple tags`() = runTest {
|
||||
val tags = Tags()
|
||||
tags.addTag("programming")
|
||||
tags.addTag("rust")
|
||||
val posts = api.getPostsByTags(tags, 1)
|
||||
assertIs<Success<List<LobstersPost>>>(posts)
|
||||
assertThat(posts.value).hasSize(25)
|
||||
assertThat(posts.value[0].tags).containsAnyOf("programming", "rust")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,14 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.api
|
||||
|
||||
import com.slack.eithernet.ApiResult.Companion.httpFailure
|
||||
import com.slack.eithernet.ApiResult.Companion.success
|
||||
import com.slack.eithernet.test.EitherNetController
|
||||
import com.slack.eithernet.test.enqueue
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.Tags
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import dev.msfjarvis.claw.util.TestUtils.getJson
|
||||
import dev.msfjarvis.claw.util.TestUtils.getResource
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
|
||||
@@ -26,23 +23,10 @@ class ApiWrapper(controller: EitherNetController<LobstersApi>) {
|
||||
ignoreUnknownKeys = true
|
||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||
}
|
||||
private val hottest: List<LobstersPost> = json.decodeFromString(getJson("hottest.json"))
|
||||
private val hottest: List<LobstersPost> = json.decodeFromString(getResource("hottest.json"))
|
||||
private val postDetails: LobstersPostDetails =
|
||||
json.decodeFromString(getJson("post_details_tdfoqh.json"))
|
||||
private val user: User = json.decodeFromString(getJson("msfjarvis.json"))
|
||||
private val metaPosts: List<LobstersPost> = json.decodeFromString(getJson("meta.json"))
|
||||
private val programmingRustPosts: List<LobstersPost> =
|
||||
json.decodeFromString(getJson("programming_rust.json"))
|
||||
private val getPostsBody = { args: Array<Any> ->
|
||||
val tags = args[0] as Tags
|
||||
if ("meta" in tags) {
|
||||
success(metaPosts)
|
||||
} else if ("programming" in tags && "rust" in tags) {
|
||||
success(programmingRustPosts)
|
||||
} else {
|
||||
httpFailure(400)
|
||||
}
|
||||
}
|
||||
json.decodeFromString(getResource("post_details_tdfoqh.json"))
|
||||
private val user: User = json.decodeFromString(getResource("msfjarvis.json"))
|
||||
|
||||
val api = controller.api
|
||||
|
||||
@@ -51,7 +35,5 @@ class ApiWrapper(controller: EitherNetController<LobstersApi>) {
|
||||
controller.enqueue(LobstersApi::getHottestPosts) { success(hottest) }
|
||||
controller.enqueue(LobstersApi::getPostDetails) { success(postDetails) }
|
||||
controller.enqueue(LobstersApi::getUser) { success(user) }
|
||||
controller.enqueue(LobstersApi::getPostsByTags, getPostsBody)
|
||||
controller.enqueue(LobstersApi::getPostsByTags, getPostsBody)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.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
|
||||
|
||||
class SearchApiTest {
|
||||
private val wrapper = SearchApiWrapper(newEitherNetController())
|
||||
private val api
|
||||
get() = wrapper.api
|
||||
|
||||
@Test
|
||||
fun `search is able to parse HTML`() = runTest {
|
||||
val posts = api.searchPosts("ChatGPT", 1)
|
||||
assertIs<ApiResult.Success<List<LobstersPost>>>(posts)
|
||||
assertThat(posts.value).containsAtLeastElementsIn(results).inOrder()
|
||||
assertThat(posts.value).hasSize(20)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val results =
|
||||
listOf(
|
||||
LobstersPost(
|
||||
shortId = "gjlsdg",
|
||||
title = "ChatGPT visits the Emacs doctor",
|
||||
url = "https://xenodium.com/chatgpt-visits-the-emacs-doctor/",
|
||||
createdAt = "",
|
||||
commentCount = 3,
|
||||
commentsUrl = "/s/gjlsdg/chatgpt_visits_emacs_doctor",
|
||||
submitter = User("xenodium", "", null, "/avatars/xenodium-16.png", ""),
|
||||
tags = listOf("ai", "emacs"),
|
||||
description = "",
|
||||
),
|
||||
LobstersPost(
|
||||
shortId = "astcqf",
|
||||
title =
|
||||
"Implementing a question-answering system for PDF documents using ChatGPT and Redis",
|
||||
url = "https://mstack.nl/blog/20230623-chatgpt-question-pdf-document/",
|
||||
createdAt = "",
|
||||
commentCount = 0,
|
||||
commentsUrl = "/s/astcqf/implementing_question_answering_system",
|
||||
submitter = User("asteroid", "", null, "/avatars/asteroid-16.png", ""),
|
||||
tags = listOf("ai"),
|
||||
description = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.Companion.success
|
||||
import com.slack.eithernet.test.EitherNetController
|
||||
import com.slack.eithernet.test.enqueue
|
||||
import dev.msfjarvis.claw.api.converters.SearchConverter
|
||||
import dev.msfjarvis.claw.util.TestUtils.getResource
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.ResponseBody
|
||||
|
||||
class SearchApiWrapper(controller: EitherNetController<LobstersSearchApi>) {
|
||||
val api = controller.api
|
||||
|
||||
init {
|
||||
controller.enqueue(LobstersSearchApi::searchPosts) {
|
||||
success(
|
||||
SearchConverter.convert(
|
||||
ResponseBody.create(MediaType.get("text/html"), getResource("search_chatgpt_page.html"))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import kotlin.contracts.contract
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
object TestUtils {
|
||||
fun getJson(path: String): String {
|
||||
fun getResource(path: String): String {
|
||||
// Load the JSON response
|
||||
val uri = javaClass.classLoader!!.getResource(path)
|
||||
val file = File(uri.path)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<queries>
|
||||
<package android:name="dev.msfjarvis.claw.android" />
|
||||
|
||||
+22
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -8,6 +8,7 @@ package dev.msfjarvis.claw.benchmark
|
||||
|
||||
import androidx.benchmark.macro.BaselineProfileMode
|
||||
import androidx.benchmark.macro.CompilationMode
|
||||
import androidx.benchmark.macro.FrameTimingMetric
|
||||
import androidx.benchmark.macro.StartupMode
|
||||
import androidx.benchmark.macro.StartupTimingMetric
|
||||
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||
@@ -28,29 +29,39 @@ class BaselineProfileBenchmark {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun startupNoCompilation() {
|
||||
startup(CompilationMode.None())
|
||||
fun noCompilation() {
|
||||
exploreUI(CompilationMode.None())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun startupBaselineFullyCompiled() {
|
||||
startup(CompilationMode.Full())
|
||||
fun fullyCompiled() {
|
||||
exploreUI(CompilationMode.Full())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun startupBaselineProfile() {
|
||||
startup(CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require))
|
||||
fun baselineProfile() {
|
||||
exploreUI(CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require))
|
||||
}
|
||||
|
||||
private fun startup(compilationMode: CompilationMode) {
|
||||
private fun exploreUI(compilationMode: CompilationMode) {
|
||||
benchmarkRule.measureRepeated(
|
||||
packageName = PACKAGE_NAME,
|
||||
metrics = listOf(StartupTimingMetric()),
|
||||
iterations = 10,
|
||||
metrics = listOf(FrameTimingMetric(), StartupTimingMetric()),
|
||||
compilationMode = compilationMode,
|
||||
startupMode = StartupMode.COLD,
|
||||
compilationMode = compilationMode
|
||||
iterations = 10,
|
||||
) {
|
||||
device.executeShellCommand("pm clear $PACKAGE_NAME")
|
||||
|
||||
startActivityAndWait()
|
||||
|
||||
device.waitForIdle()
|
||||
|
||||
device.savePosts()
|
||||
|
||||
device.exploreScreens()
|
||||
|
||||
device.returnToHottestScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.benchmark
|
||||
|
||||
import androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi
|
||||
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
@@ -14,7 +13,6 @@ import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalStableBaselineProfilesApi::class)
|
||||
class BaselineProfileGenerator {
|
||||
@get:Rule val baselineProfileRule = BaselineProfileRule()
|
||||
private lateinit var device: UiDevice
|
||||
@@ -27,11 +25,20 @@ class BaselineProfileGenerator {
|
||||
|
||||
@Test
|
||||
fun generateBaselineProfile() =
|
||||
baselineProfileRule.collectStableBaselineProfile(
|
||||
baselineProfileRule.collect(
|
||||
packageName = PACKAGE_NAME,
|
||||
stableIterations = 2,
|
||||
maxIterations = 8,
|
||||
) {
|
||||
device.executeShellCommand("pm clear $PACKAGE_NAME")
|
||||
|
||||
startActivityAndWait()
|
||||
|
||||
device.waitForIdle()
|
||||
|
||||
device.savePosts()
|
||||
|
||||
device.exploreScreens()
|
||||
|
||||
device.returnToHottestScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,56 +6,38 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.benchmark
|
||||
|
||||
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.BySelector
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import androidx.test.uiautomator.UiObject2
|
||||
import androidx.test.uiautomator.Until
|
||||
|
||||
const val PACKAGE_NAME = "dev.msfjarvis.claw.android.benchmark"
|
||||
private const val AWAIT_TIMEOUT = 10_000L
|
||||
private const val SAVE_BUTTON_DESC = "Add to saved posts"
|
||||
private const val SAVE_BUTTON_ID = "save_button"
|
||||
private const val NAV_ID_HOTTEST = "HOTTEST"
|
||||
private const val NAV_ID_NEWEST = "NEWEST"
|
||||
private const val NAV_ID_SAVED = "SAVED"
|
||||
private const val COMMENT_BUTTON_DESC = "Open comments"
|
||||
|
||||
@Suppress("Unused")
|
||||
fun MacrobenchmarkScope.exploreUI(device: UiDevice) {
|
||||
startActivityAndWait()
|
||||
device.run {
|
||||
savePosts()
|
||||
|
||||
exploreScreens()
|
||||
|
||||
returnToHottestScreen()
|
||||
|
||||
openCommentsScreen()
|
||||
}
|
||||
fun UiDevice.savePosts() {
|
||||
waitForObject(By.res(SAVE_BUTTON_ID)).click()
|
||||
}
|
||||
|
||||
private fun UiDevice.waitForSubmitterName() {
|
||||
wait(Until.hasObject(By.textContains("Submitted by")), AWAIT_TIMEOUT)
|
||||
}
|
||||
|
||||
private fun UiDevice.savePosts() {
|
||||
waitForSubmitterName()
|
||||
findObjects(By.desc(SAVE_BUTTON_DESC)).forEach(UiObject2::click)
|
||||
}
|
||||
|
||||
private fun UiDevice.exploreScreens() {
|
||||
fun UiDevice.exploreScreens() {
|
||||
listOf(NAV_ID_HOTTEST, NAV_ID_NEWEST, NAV_ID_SAVED).forEach { tag ->
|
||||
findObject(By.res(tag)).click()
|
||||
waitForSubmitterName()
|
||||
waitForObject(By.res(tag)).click()
|
||||
waitForIdle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun UiDevice.returnToHottestScreen() {
|
||||
findObject(By.res(NAV_ID_HOTTEST)).click()
|
||||
waitForSubmitterName()
|
||||
fun UiDevice.returnToHottestScreen() {
|
||||
waitForObject(By.res(NAV_ID_HOTTEST)).click()
|
||||
}
|
||||
|
||||
private fun UiDevice.openCommentsScreen() {
|
||||
waitForSubmitterName()
|
||||
findObjects(By.desc(COMMENT_BUTTON_DESC)).first().click()
|
||||
private fun UiDevice.waitForObject(selector: BySelector, timeout: Long = AWAIT_TIMEOUT): UiObject2 {
|
||||
if (wait(Until.hasObject(selector), timeout)) {
|
||||
return findObject(selector)
|
||||
}
|
||||
|
||||
error("Object with selector [$selector] not found")
|
||||
}
|
||||
|
||||
@@ -4,22 +4,9 @@
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins { `kotlin-dsl` }
|
||||
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
sourceCompatibility = JavaVersion.VERSION_17.toString()
|
||||
targetCompatibility = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
|
||||
}
|
||||
kotlin.jvmToolchain { languageVersion.set(JavaLanguageVersion.of(17)) }
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
|
||||
@@ -16,7 +16,6 @@ import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
||||
import com.android.build.gradle.BaseExtension
|
||||
import dev.msfjarvis.claw.gradle.LintConfig.configureLint
|
||||
import org.gradle.android.AndroidCacheFixPlugin
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||
@@ -28,9 +27,10 @@ import org.gradle.kotlin.dsl.getByType
|
||||
class AndroidCommonPlugin : Plugin<Project> {
|
||||
|
||||
private companion object {
|
||||
const val COMPILE_SDK = 33
|
||||
const val COMPILE_SDK = 34
|
||||
const val MIN_SDK = 26
|
||||
const val TARGET_SDK = 33
|
||||
const val TARGET_SDK = 34
|
||||
const val SLIM_TESTS_PROPERTY = "slimTests"
|
||||
}
|
||||
|
||||
override fun apply(project: Project) {
|
||||
@@ -52,11 +52,6 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
resources.excludes.add("**/META-INF/LGPL2.1")
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
testOptions {
|
||||
animationsDisabled = true
|
||||
unitTests.isReturnDefaultValues = true
|
||||
@@ -68,22 +63,27 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
val libs = catalog.named("libs")
|
||||
project.dependencies.addProvider("lintChecks", libs.findLibrary("slack-compose-lints").get())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Project.configureSlimTests() {
|
||||
// Disable unit test tasks on the release build type for Android Library projects
|
||||
extensions.findByType<LibraryAndroidComponentsExtension>()?.run {
|
||||
beforeVariants(selector().withBuildType("release")) {
|
||||
(it as HasUnitTestBuilder).enableUnitTest = false
|
||||
it.enableAndroidTest = false
|
||||
private fun Project.configureSlimTests() {
|
||||
if (!providers.gradleProperty(SLIM_TESTS_PROPERTY).isPresent) {
|
||||
return
|
||||
}
|
||||
// Disable unit test tasks on the release build type for Android Library projects
|
||||
extensions.findByType<LibraryAndroidComponentsExtension>()?.run {
|
||||
beforeVariants(selector().all()) {
|
||||
if (it.name == "debug") return@beforeVariants
|
||||
(it as HasUnitTestBuilder).enableUnitTest = false
|
||||
it.enableAndroidTest = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disable unit test tasks on the release build type for Android Application projects.
|
||||
extensions.findByType<ApplicationAndroidComponentsExtension>()?.run {
|
||||
beforeVariants(selector().withBuildType("release")) {
|
||||
(it as HasUnitTestBuilder).enableUnitTest = false
|
||||
it.enableAndroidTest = false
|
||||
// Disable unit test tasks on the release build type for Android Application projects.
|
||||
extensions.findByType<ApplicationAndroidComponentsExtension>()?.run {
|
||||
beforeVariants(selector().all()) {
|
||||
if (it.name == "debug") return@beforeVariants
|
||||
(it as HasUnitTestBuilder).enableUnitTest = false
|
||||
it.enableAndroidTest = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,7 @@ class ApplicationPlugin : Plugin<Project> {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("release") {
|
||||
isMinifyEnabled = true
|
||||
all {
|
||||
setProguardFiles(
|
||||
listOf(
|
||||
"proguard-android-optimize.pro",
|
||||
@@ -44,6 +43,7 @@ class ApplicationPlugin : Plugin<Project> {
|
||||
)
|
||||
)
|
||||
}
|
||||
named("release") { isMinifyEnabled = true }
|
||||
named("debug") {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import dev.msfjarvis.claw.gradle.KotlinCommonPlugin.Companion.JVM_TOOLCHAIN_ACTION
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper
|
||||
|
||||
@Suppress("Unused")
|
||||
@@ -19,5 +22,6 @@ class KotlinAndroidPlugin : Plugin<Project> {
|
||||
apply(KotlinAndroidPluginWrapper::class)
|
||||
apply(KotlinCommonPlugin::class)
|
||||
}
|
||||
project.extensions.getByType<KotlinProjectExtension>().jvmToolchain(JVM_TOOLCHAIN_ACTION)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Action
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.api.tasks.testing.Test
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
import org.gradle.jvm.toolchain.JavaToolchainSpec
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
@Suppress("Unused")
|
||||
class KotlinCommonPlugin : Plugin<Project> {
|
||||
@@ -27,18 +27,11 @@ class KotlinCommonPlugin : Plugin<Project> {
|
||||
LintConfig.configureSubProject(project)
|
||||
}
|
||||
project.tasks.run {
|
||||
withType<JavaCompile>().configureEach {
|
||||
sourceCompatibility = JavaVersion.VERSION_17.toString()
|
||||
targetCompatibility = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
withType<KotlinJvmCompile>().configureEach {
|
||||
withType<KotlinCompile>().configureEach {
|
||||
compilerOptions {
|
||||
allWarningsAsErrors.set(
|
||||
project.providers.environmentVariable("GITHUB_WORKFLOW").isPresent
|
||||
)
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
allWarningsAsErrors.set(true)
|
||||
freeCompilerArgs.addAll(ADDITIONAL_COMPILER_ARGS)
|
||||
languageVersion.set(KotlinVersion.KOTLIN_1_7)
|
||||
languageVersion.set(KotlinVersion.KOTLIN_1_8)
|
||||
}
|
||||
}
|
||||
withType<Test>().configureEach {
|
||||
@@ -49,10 +42,13 @@ class KotlinCommonPlugin : Plugin<Project> {
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
companion object {
|
||||
private val ADDITIONAL_COMPILER_ARGS =
|
||||
listOf(
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
)
|
||||
|
||||
val JVM_TOOLCHAIN_ACTION =
|
||||
Action<JavaToolchainSpec> { languageVersion.set(JavaLanguageVersion.of(17)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,25 @@ package dev.msfjarvis.claw.gradle
|
||||
|
||||
import com.android.build.api.dsl.Lint
|
||||
import com.android.build.gradle.LintPlugin
|
||||
import dev.msfjarvis.claw.gradle.KotlinCommonPlugin.Companion.JVM_TOOLCHAIN_ACTION
|
||||
import dev.msfjarvis.claw.gradle.LintConfig.configureLint
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.findByType
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper
|
||||
|
||||
@Suppress("Unused")
|
||||
class KotlinJvmPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
project.pluginManager.apply(KotlinPluginWrapper::class)
|
||||
project.pluginManager.apply(LintPlugin::class)
|
||||
project.pluginManager.run {
|
||||
apply(KotlinPluginWrapper::class)
|
||||
apply(LintPlugin::class)
|
||||
apply(KotlinCommonPlugin::class)
|
||||
}
|
||||
project.extensions.findByType<Lint>()?.configureLint(project, isJVM = true)
|
||||
project.pluginManager.apply(KotlinCommonPlugin::class)
|
||||
project.extensions.getByType<KotlinProjectExtension>().jvmToolchain(JVM_TOOLCHAIN_ACTION)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,14 @@ class SpotlessPlugin : Plugin<Project> {
|
||||
}
|
||||
format("xml") {
|
||||
target("**/*.xml")
|
||||
targetExclude("**/build/", ".idea/")
|
||||
targetExclude("**/build/", ".idea/", "/spotless/", "**/lint-baseline.xml")
|
||||
trimTrailingWhitespace()
|
||||
indentWithSpaces()
|
||||
endWithNewline()
|
||||
licenseHeaderFile(
|
||||
project.file("spotless/license.xml"),
|
||||
"<manifest|<resources|<full-backup-content|<data-extraction-rules|<adaptive-icon"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
|
||||
<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
package dev.msfjarvis.claw.common.comments
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -30,7 +29,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
@@ -61,7 +59,7 @@ internal fun CommentsHeader(
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val linkMetadata by
|
||||
produceState(
|
||||
initialValue = LinkMetadata(postDetails.url, null, null),
|
||||
initialValue = LinkMetadata(postDetails.url, null),
|
||||
) {
|
||||
runCatching { postActions.getLinkMetadata(postDetails.url) }
|
||||
.onSuccess { metadata -> value = metadata }
|
||||
@@ -116,7 +114,7 @@ private fun PostLink(
|
||||
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
NetworkImage(
|
||||
url = linkMetadata.faviconUrl,
|
||||
placeholder = rememberVectorPainter(Icons.Filled.Public),
|
||||
placeholder = Icons.Filled.Public,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
@@ -170,16 +168,11 @@ internal fun CommentEntry(
|
||||
modifier =
|
||||
Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${comment.user.username}") },
|
||||
)
|
||||
AnimatedContent(
|
||||
label = "comment_body",
|
||||
targetState = commentNode.isExpanded,
|
||||
) { expandedState ->
|
||||
if (expandedState) {
|
||||
ThemedRichText(
|
||||
text = htmlConverter.convertHTMLToMarkdown(comment.comment),
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
if (commentNode.isExpanded) {
|
||||
ThemedRichText(
|
||||
text = htmlConverter.convertHTMLToMarkdown(comment.comment),
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
package dev.msfjarvis.claw.common.comments
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
|
||||
@@ -95,7 +95,7 @@ private fun LazyListScope.node(
|
||||
htmlConverter = htmlConverter,
|
||||
toggleExpanded = toggleExpanded,
|
||||
)
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (node.children.isNotEmpty()) {
|
||||
nodes(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 Harsh Shandilya.
|
||||
* 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.
|
||||
|
||||
@@ -28,13 +28,13 @@ import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Comment
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.outlined.Comment
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -46,7 +46,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -98,7 +98,7 @@ fun LobstersCard(
|
||||
postActions.toggleSave(post)
|
||||
},
|
||||
)
|
||||
Divider(modifier = Modifier.width(48.dp))
|
||||
HorizontalDivider(modifier = Modifier.width(48.dp))
|
||||
CommentsButton(
|
||||
commentCount = post.commentCount,
|
||||
modifier =
|
||||
@@ -154,7 +154,7 @@ internal fun Submitter(
|
||||
) {
|
||||
NetworkImage(
|
||||
url = avatarUrl,
|
||||
placeholder = rememberVectorPainter(Icons.Filled.AccountCircle),
|
||||
placeholder = Icons.Filled.AccountCircle,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.requiredSize(24.dp).clip(CircleShape),
|
||||
)
|
||||
@@ -171,13 +171,10 @@ private fun SaveButton(
|
||||
Crossfade(targetState = isSaved, label = "save-button") { saved ->
|
||||
Box(modifier = modifier.padding(12.dp)) {
|
||||
Icon(
|
||||
painter =
|
||||
rememberVectorPainter(
|
||||
if (saved) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder
|
||||
),
|
||||
imageVector = if (saved) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
contentDescription = if (saved) "Remove from saved posts" else "Add to saved posts",
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
modifier = Modifier.align(Alignment.Center).testTag("save_button"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -207,10 +204,10 @@ private fun CommentsButton(
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(Icons.Outlined.Comment),
|
||||
imageVector = Icons.Filled.Comment,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
contentDescription = "Open comments",
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
modifier = Modifier.align(Alignment.Center).testTag("comments_button"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -292,7 +289,7 @@ fun LobstersCardPreview() {
|
||||
}
|
||||
|
||||
override suspend fun getLinkMetadata(url: String): LinkMetadata {
|
||||
return LinkMetadata("", "", "")
|
||||
return LinkMetadata("", "")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 Harsh Shandilya.
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -25,6 +26,7 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
import io.github.aakira.napier.Napier
|
||||
|
||||
@Composable
|
||||
fun NetworkError(
|
||||
@@ -32,6 +34,7 @@ fun NetworkError(
|
||||
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) {
|
||||
Text(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2022 Harsh Shandilya.
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -8,21 +8,23 @@ package dev.msfjarvis.claw.common.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import coil.compose.AsyncImage
|
||||
|
||||
@Composable
|
||||
internal fun NetworkImage(
|
||||
url: String?,
|
||||
placeholder: Painter,
|
||||
placeholder: ImageVector,
|
||||
contentDescription: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val painter = rememberVectorPainter(placeholder)
|
||||
AsyncImage(
|
||||
model = url,
|
||||
placeholder = placeholder,
|
||||
error = placeholder,
|
||||
fallback = placeholder,
|
||||
placeholder = painter,
|
||||
error = painter,
|
||||
fallback = painter,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.common.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
placeholder = { Text(text = "Search") },
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { onSearch(value) }) {
|
||||
Icon(imageVector = Icons.Outlined.Search, contentDescription = "Search")
|
||||
}
|
||||
},
|
||||
keyboardActions = KeyboardActions(onSearch = { onSearch(value) }),
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Search,
|
||||
),
|
||||
singleLine = true,
|
||||
modifier = modifier.focusable(),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SearchBarPreview() {
|
||||
LobstersTheme {
|
||||
Box(Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.background).padding(8.dp)) {
|
||||
var value by remember { mutableStateOf("") }
|
||||
SearchBar(
|
||||
value = value,
|
||||
onValueChange = { value = it },
|
||||
onSearch = {},
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.NetworkState
|
||||
import dev.msfjarvis.claw.common.NetworkState.Error
|
||||
@@ -92,7 +91,7 @@ private fun UserProfileInternal(
|
||||
) {
|
||||
NetworkImage(
|
||||
url = "https://lobste.rs/${user.avatarUrl}",
|
||||
placeholder = rememberVectorPainter(Icons.Filled.AccountCircle),
|
||||
placeholder = Icons.Filled.AccountCircle,
|
||||
contentDescription = "Avatar of ${user.username}",
|
||||
modifier = Modifier.requiredSize(120.dp).clip(CircleShape),
|
||||
)
|
||||
|
||||
@@ -16,13 +16,16 @@ android { namespace = "dev.msfjarvis.claw.core" }
|
||||
anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
dependencies {
|
||||
api(libs.javax.inject)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.okhttp.loggingInterceptor)
|
||||
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(platform(libs.sentry.bom))
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.javax.inject)
|
||||
implementation(libs.napier)
|
||||
implementation(libs.okhttp.core)
|
||||
implementation(libs.retrofit.kotlinxSerializationConverter)
|
||||
implementation(libs.sentry.okhttp)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
|
||||
<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
|
||||
|
||||
</issues>
|
||||
|
||||
+1
-3
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
@file:Suppress("InjectDispatcher") // False-positive
|
||||
|
||||
package dev.msfjarvis.claw.util.coroutines
|
||||
package dev.msfjarvis.claw.core.coroutines
|
||||
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -23,8 +23,6 @@ interface DispatcherProvider {
|
||||
|
||||
fun io(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
|
||||
|
||||
fun database(): CoroutineDispatcher = Dispatchers.IO.limitedParallelism(1)
|
||||
}
|
||||
|
||||
+18
-4
@@ -1,20 +1,29 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
package dev.msfjarvis.claw.core.injection
|
||||
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dev.msfjarvis.claw.util.coroutines.DefaultDispatcherProvider
|
||||
import dev.msfjarvis.claw.util.coroutines.DispatcherProvider
|
||||
import dev.msfjarvis.claw.core.coroutines.DefaultDispatcherProvider
|
||||
import dev.msfjarvis.claw.core.coroutines.DispatcherProvider
|
||||
import javax.inject.Qualifier
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class DatabaseDispatcher
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class MainDispatcher
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class IODispatcher
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class DefaultDispatcher
|
||||
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
interface CoroutineDispatcherModule {
|
||||
@@ -36,5 +45,10 @@ interface CoroutineDispatcherModule {
|
||||
fun provideMainDispatcher(dispatcherProvider: DispatcherProvider): CoroutineDispatcher {
|
||||
return dispatcherProvider.main()
|
||||
}
|
||||
|
||||
@[Provides DefaultDispatcher]
|
||||
fun provideDefaultDispatcher(dispatcherProvider: DispatcherProvider): CoroutineDispatcher {
|
||||
return dispatcherProvider.default()
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -11,7 +11,8 @@ import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFact
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoSet
|
||||
import dagger.multibindings.IntKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
@@ -21,10 +22,11 @@ import retrofit2.Converter
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
object RetrofitModule {
|
||||
object JsonModule {
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
@IntKey(1)
|
||||
@IntoMap
|
||||
fun provideJsonConverterFactory(json: Json): Converter.Factory {
|
||||
val contentType = "application/json".toMediaType()
|
||||
return json.asConverterFactory(contentType)
|
||||
@@ -18,6 +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 java.net.Socket
|
||||
import javax.net.SocketFactory
|
||||
import okhttp3.Cache
|
||||
@@ -87,5 +88,11 @@ interface OkHttpModule {
|
||||
fun provideHttpLoggingInterceptor(logger: HttpLoggingInterceptor.Logger): Interceptor {
|
||||
return HttpLoggingInterceptor(logger).setLevel(HttpLoggingInterceptor.Level.BASIC)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideSentryInterceptor(): Interceptor {
|
||||
return SentryOkHttpInterceptor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +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.
|
||||
*/
|
||||
plugins { id("dev.msfjarvis.claw.kotlin-jvm") }
|
||||
|
||||
dependencies {
|
||||
api(libs.javax.inject)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
|
||||
|
||||
</issues>
|
||||
@@ -9,6 +9,7 @@ import dev.msfjarvis.claw.gradle.addTestDependencies
|
||||
plugins {
|
||||
id("dev.msfjarvis.claw.android-library")
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.sqldelight)
|
||||
alias(libs.plugins.whetstone)
|
||||
@@ -33,7 +34,9 @@ dependencies {
|
||||
implementation(libs.sqldelight.androidDriver)
|
||||
implementation(libs.sqldelight.primitiveAdapters)
|
||||
implementation(libs.sqlite.android)
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
|
||||
testImplementation(libs.sqldelight.jvmDriver)
|
||||
testImplementation(libs.kotlinx.serialization.json)
|
||||
addTestDependencies(project)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
|
||||
<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<manifest />
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.database
|
||||
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.element
|
||||
import kotlinx.serialization.encoding.CompositeDecoder
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.encoding.decodeStructure
|
||||
import kotlinx.serialization.encoding.encodeStructure
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
object SavedPostSerializer : KSerializer<SavedPost> {
|
||||
private val delegateSerializer = ListSerializer(String.serializer())
|
||||
override val descriptor: SerialDescriptor =
|
||||
buildClassSerialDescriptor("SavedPost") {
|
||||
element<String>("shortId")
|
||||
element<String>("title")
|
||||
element<String>("url")
|
||||
element<String>("createdAt")
|
||||
element<Int?>("commentCount", isOptional = true)
|
||||
element<String>("commentsUrl")
|
||||
element<String>("submitterName")
|
||||
element<String>("submitterAvatarUrl")
|
||||
element<List<String>>("tags")
|
||||
element<String>("description")
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): SavedPost {
|
||||
return decoder.decodeStructure(descriptor) {
|
||||
var shortId = ""
|
||||
var title = ""
|
||||
var url = ""
|
||||
var createdAt = ""
|
||||
var commentCount: Int? = null
|
||||
var commentsUrl = ""
|
||||
var submitterName = ""
|
||||
var submitterAvatarUrl = ""
|
||||
var tags = emptyList<String>()
|
||||
var description = ""
|
||||
while (true) {
|
||||
when (val index = decodeElementIndex(descriptor)) {
|
||||
0 -> shortId = decodeStringElement(descriptor, 0)
|
||||
1 -> title = decodeStringElement(descriptor, 1)
|
||||
2 -> url = decodeStringElement(descriptor, 2)
|
||||
3 -> createdAt = decodeStringElement(descriptor, 3)
|
||||
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)
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
else -> error("Unexpected index: $index")
|
||||
}
|
||||
}
|
||||
SavedPost(
|
||||
shortId = shortId,
|
||||
title = title,
|
||||
url = url,
|
||||
createdAt = createdAt,
|
||||
commentCount = commentCount,
|
||||
commentsUrl = commentsUrl,
|
||||
submitterName = submitterName,
|
||||
submitterAvatarUrl = submitterAvatarUrl,
|
||||
tags = tags,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: SavedPost) {
|
||||
encoder.encodeStructure(descriptor) {
|
||||
encodeStringElement(descriptor, 0, value.shortId)
|
||||
encodeStringElement(descriptor, 1, value.title)
|
||||
encodeStringElement(descriptor, 2, value.url)
|
||||
encodeStringElement(descriptor, 3, value.createdAt)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.database
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import java.io.File
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
class SavedPostSerializerTest {
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||
}
|
||||
private val text = getJson()
|
||||
|
||||
@Test
|
||||
fun serialize() {
|
||||
val encoded = json.encodeToString(SavedPostSerializer, SAVED_POST)
|
||||
assertThat(encoded).isNotEmpty()
|
||||
assertThat(encoded).isEqualTo(text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deserialize() {
|
||||
val decoded = json.decodeFromString(SavedPostSerializer, text)
|
||||
assertThat(decoded).isEqualTo(SAVED_POST)
|
||||
}
|
||||
|
||||
private fun getJson(): String {
|
||||
// Load the JSON response
|
||||
val uri = javaClass.classLoader!!.getResource("saved_post.json")
|
||||
val file = File(uri.path)
|
||||
return String(file.readBytes())
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val SAVED_POST =
|
||||
SavedPost(
|
||||
title = "Fun Format Friday: You now have a super computer. What next?",
|
||||
shortId = "nbigsf",
|
||||
url = "",
|
||||
createdAt = "2023-05-04T23:43:50.000-05:00",
|
||||
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",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"short_id":"nbigsf","title":"Fun Format Friday: You now have a super computer. What next?","url":"","created_at":"2023-05-04T23:43:50.000-05:00","comment_count":13,"comments_url":"https://lobste.rs/s/nbigsf/fun_format_friday_you_now_have_super","submitter_name":"LenFalken","submitter_avatar_url":"/avatars/LenFalken-100.png","tags":["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