Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35469825c7 | ||
|
|
f565bc41d7 | ||
|
|
9446f09f22 | ||
|
|
925158b149 | ||
|
|
6f1c1b3629 | ||
|
|
cb6e71c8bf | ||
|
|
17258ea4d7 | ||
|
|
035d33fcea | ||
|
|
de46f1b159 | ||
|
|
4b4d133e8b | ||
|
|
5a1cdc73c5 | ||
|
|
c5fe4f5325 | ||
|
|
e2fa605db6 | ||
|
|
55580bc51c | ||
|
|
fe78f97f6a | ||
|
|
9d46e6f460 | ||
|
|
21f9f6f476 | ||
|
|
2ba8eb9430 | ||
|
|
6ea194dd0e | ||
|
|
1d3d4f7455 | ||
|
|
fdb79d4a6d | ||
|
|
89fce2010c | ||
|
|
fb878824e8 | ||
|
|
aff5af4771 | ||
|
|
dd06c93935 | ||
|
|
d9e664c440 | ||
|
|
0a492a169e | ||
|
|
6ae71adbca | ||
|
|
cbaa098005 | ||
|
|
741e82a425 | ||
|
|
c694b57e08 | ||
|
|
2c7bc2d668 | ||
|
|
765f83a2ae | ||
|
|
58f6607404 | ||
|
|
9b27783886 | ||
|
|
c6de087615 | ||
|
|
86f7b54b87 | ||
|
|
defe305ad7 | ||
|
|
cc210a3597 | ||
|
|
f7c7bb293b | ||
|
|
ab1db27dc7 | ||
|
|
71a86fa9aa | ||
|
|
fcc20d2f1e | ||
|
|
8922142ade | ||
|
|
eb28fe0120 | ||
|
|
d73a755cf2 | ||
|
|
7a648a77b0 | ||
|
|
c721b8b1da | ||
|
|
ce62671276 | ||
|
|
ba4cd1b0c6 | ||
|
|
a18ee3d6e5 | ||
|
|
15fa833d8e | ||
|
|
06edbeb903 | ||
|
|
082dc9cc82 | ||
|
|
0b3286cd1e | ||
|
|
f17fb4487e | ||
|
|
339162d595 | ||
|
|
da6fefb1b0 | ||
|
|
8d3ed24d65 | ||
|
|
ba219af1ac | ||
|
|
4218beac9d | ||
|
|
d5d71c1c82 | ||
|
|
348b3a7c65 | ||
|
|
f636b56ee3 | ||
|
|
ed81ceaa94 | ||
|
|
7a78c77b6a | ||
|
|
337cd259a6 | ||
|
|
e7781517aa | ||
|
|
ce3411e956 | ||
|
|
a58ca2c419 | ||
|
|
608d855cae | ||
|
|
eb529be286 | ||
|
|
68dbe97442 | ||
|
|
5374410c67 | ||
|
|
cbb697216c | ||
|
|
25a11bde59 | ||
|
|
62bffc33fd | ||
|
|
45f384d048 | ||
|
|
b9d92ecefa | ||
|
|
01fd8bee0c | ||
|
|
9048d253e6 | ||
|
|
16b660ad0e | ||
|
|
2273768556 | ||
|
|
ab01554b82 | ||
|
|
cce12fb138 | ||
|
|
6f4c1a0fbd | ||
|
|
3663ca1ec1 | ||
|
|
f39bf0b043 | ||
|
|
f55d89b17e | ||
|
|
f6185a3dc5 | ||
|
|
be7bd38341 | ||
|
|
b3277e2367 | ||
|
|
f560bb6fbf | ||
|
|
013138afb8 | ||
|
|
d6d639d059 | ||
|
|
31b2d66b9b | ||
|
|
4b278d68e6 | ||
|
|
e81687bf53 | ||
|
|
a0ea6d6e1c | ||
|
|
17eb9a4c45 | ||
|
|
2e8ad58979 | ||
|
|
680d43f78c | ||
|
|
0d1fb26b52 | ||
|
|
7540430e1d | ||
|
|
095ec1ed04 | ||
|
|
ce62a7b1fc | ||
|
|
4b89775f8a | ||
|
|
a5af23e330 | ||
|
|
46cc0910bf | ||
|
|
37e0fdb0a7 | ||
|
|
a3e7e76931 | ||
|
|
cfdab8ddcd | ||
|
|
389924a544 | ||
|
|
b3634a0762 | ||
|
|
985bc1361a | ||
|
|
d3d3a6af09 | ||
|
|
3883160234 | ||
|
|
d04ad16aee | ||
|
|
0067418ffb | ||
|
|
deb900f8f8 | ||
|
|
e6f6ed75f1 | ||
|
|
6226c3b0c8 | ||
|
|
1a525d6e5d | ||
|
|
bc213fe3c9 | ||
|
|
731666936c | ||
|
|
67ec7e92b5 | ||
|
|
40bfcb07b6 | ||
|
|
e5f09a7aef | ||
|
|
341bdb0eda | ||
|
|
27d68f2774 | ||
|
|
d259cd8eb5 | ||
|
|
c9e5ded43a | ||
|
|
a8e03e22bf | ||
|
|
7094016e7e | ||
|
|
1bfa1f3981 | ||
|
|
655745e049 | ||
|
|
f83a22b310 | ||
|
|
025c24a9d9 | ||
|
|
c8c3e83e7b | ||
|
|
7d8327af18 | ||
|
|
58bab2f538 | ||
|
|
cf93eda6c1 | ||
|
|
a3b7af0f79 | ||
|
|
91bf5e5555 | ||
|
|
61a72b330d | ||
|
|
6a1164363c | ||
|
|
df9a82482c | ||
|
|
778d98c7f2 | ||
|
|
cae759a09e | ||
|
|
52126b2248 | ||
|
|
5782521824 | ||
|
|
3522dc529d | ||
|
|
8602e68b60 | ||
|
|
fb4514d70e | ||
|
|
35d86bff11 | ||
|
|
193730e16c | ||
|
|
ed32186984 | ||
|
|
eb449740eb | ||
|
|
327fd43dd6 | ||
|
|
bc2345f415 | ||
|
|
fd1bdc79f8 | ||
|
|
22a2419185 | ||
|
|
de921bf786 | ||
|
|
3cf6b22d95 | ||
|
|
82ce45b8e4 | ||
|
|
dfe95a646e | ||
|
|
7b0b206905 | ||
|
|
1b1984064c | ||
|
|
7e03e2571f | ||
|
|
23f0b32634 | ||
|
|
f844062980 | ||
|
|
84f982b20e | ||
|
|
c6739487a4 | ||
|
|
be0f46371e | ||
|
|
85351d5b1c | ||
|
|
23366e4536 | ||
|
|
0d1f69d36e | ||
|
|
d77db45f37 | ||
|
|
20c5b6c4a5 | ||
|
|
62f512e319 | ||
|
|
e740ddb7cc | ||
|
|
68924d6f35 | ||
|
|
94db512518 | ||
|
|
f39e9889f3 | ||
|
|
b0ffbcd07d | ||
|
|
19412cc963 | ||
|
|
45c7dbb3f7 | ||
|
|
3b0d0e55dc | ||
|
|
ae21ebe80d | ||
|
|
cca14765ce | ||
|
|
725718d18c | ||
|
|
61b000dc48 | ||
|
|
cf850071e5 | ||
|
|
fe473e70a3 | ||
|
|
ce6540a6f3 | ||
|
|
ac3e60053a |
@@ -6,3 +6,6 @@ cbdfa67d075ec1f8ceec0f3559498cbca01783a0
|
||||
|
||||
# Renamed Renovate config file
|
||||
f304304bc5b64edf189aaa473b05df912067a8bd
|
||||
|
||||
# Refactored database module into core and impl
|
||||
7b0b206905b8dc1f824a2d2a10e3d94c6b9dd11c
|
||||
|
||||
|
Before Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 313 KiB |
@@ -22,6 +22,7 @@
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"androidx.compose.compiler:compiler",
|
||||
"com.google.devtools.ksp",
|
||||
"dev.drewhamilton.poko",
|
||||
"org.jetbrains.kotlin:kotlin-gradle-plugin",
|
||||
"org.jetbrains.kotlin.plugin.serialization",
|
||||
@@ -32,6 +33,8 @@
|
||||
"matchPackagePatterns": [
|
||||
"^androidx.*",
|
||||
],
|
||||
"matchPackageNames": [
|
||||
],
|
||||
"groupName": "androidx"
|
||||
},
|
||||
{
|
||||
@@ -43,7 +46,6 @@
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"com.android.tools:r8",
|
||||
"com.squareup.anvil",
|
||||
],
|
||||
"enabled": false,
|
||||
|
||||
@@ -18,18 +18,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Run unit tests
|
||||
uses: gradle/gradle-build-action@ef76a971e2fa3f867b617efd72f2fbd72cf6f8bc # v2.8.0
|
||||
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
|
||||
with:
|
||||
arguments: --no-configuration-cache --stacktrace check -PslimTests
|
||||
gradle-home-cache-cleanup: true
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: Test report
|
||||
path: android/build/reports
|
||||
path: '**/build/reports/tests/**'
|
||||
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -49,26 +49,39 @@ jobs:
|
||||
- check
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 # v3
|
||||
uses: actions/dependency-review-action@7bbfa034e752445ea40215fff1c3bf9597993d3f # v3
|
||||
with:
|
||||
base-ref: refs/heads/main
|
||||
head-ref: ${{ github.ref }}
|
||||
|
||||
validate-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Check changelog format
|
||||
uses: mindsers/changelog-reader-action@b97ce03a10d9bdbb07beb491c76a5a01d78cd3ef # v2.2.2
|
||||
with:
|
||||
path: ./CHANGELOG.md
|
||||
validation_level: error
|
||||
|
||||
deploy-release-snapshot:
|
||||
runs-on: ubuntu-latest
|
||||
if: "github.event_name == 'push' && github.event.ref == 'refs/heads/main'"
|
||||
needs:
|
||||
- check
|
||||
- validate-changelog
|
||||
- dependency-review
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
@@ -81,7 +94,7 @@ jobs:
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
|
||||
- name: Build release app
|
||||
uses: gradle/gradle-build-action@ef76a971e2fa3f867b617efd72f2fbd72cf6f8bc # v2.8.0
|
||||
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
name: Code quality analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- renovate/**
|
||||
schedule:
|
||||
- cron: '31 7 * * 3'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
name: CodeQL
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7
|
||||
with:
|
||||
languages: java
|
||||
tools: latest
|
||||
queries: +security-extended
|
||||
|
||||
- name: Build project
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew assembleDebug assembleRelease
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7
|
||||
with:
|
||||
category: "/language:java"
|
||||
mobsfscan:
|
||||
name: MobSF
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Run mobsfscan
|
||||
uses: MobSF/mobsfscan@b8503e0027d134f55cc3306582efbbbeaa96c7ba
|
||||
with:
|
||||
args: . --sarif --output results.sarif || true
|
||||
|
||||
- name: Upload mobsfscan report
|
||||
uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -1,53 +0,0 @@
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- renovate/**
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '31 7 * * 3'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@ef76a971e2fa3f867b617efd72f2fbd72cf6f8bc # v2.8.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
|
||||
with:
|
||||
languages: java
|
||||
tools: latest
|
||||
queries: +security-extended
|
||||
|
||||
- name: Build project
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew assemble -PslimTests
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
|
||||
with:
|
||||
category: "/language:java"
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
|
||||
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
git config user.email noreply@github.com
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@ef76a971e2fa3f867b617efd72f2fbd72cf6f8bc # v2.8.0
|
||||
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
*.json
|
||||
baseline-prof.txt
|
||||
startup-prof.txt
|
||||
**/test/resources/**
|
||||
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.38.0] - 2023-11-20
|
||||
|
||||
### Changed
|
||||
|
||||
* Add brand new icon by dzuk
|
||||
* Fix a bug that caused the app to fetch the same 20 posts over and over
|
||||
* Significantly improve scrolling performance when there are a lot of saved posts
|
||||
* Move search feature to its own screen
|
||||
* Make top app and bottom system bars use the same color
|
||||
* Reduce unnecessary API calls in search screen
|
||||
* Upgrade to Compose November releases
|
||||
* Enable logging of SQLite queries
|
||||
|
||||
## [1.37.0] - 2023-10-06
|
||||
|
||||
### Changed
|
||||
|
||||
* Fixed a crash that infrequently happened in the comments page
|
||||
* Fixed a crash when trying to list saved posts
|
||||
* Fixed a crash in home screen widget when user had less than 50 saved posts
|
||||
* Upgraded to Compose 1.6.0-alpha07
|
||||
|
||||
## [1.36.0] - 2023-10-04
|
||||
|
||||
### Added
|
||||
|
||||
* Introduce an initial attempt at a home screen widget
|
||||
|
||||
### Changed
|
||||
|
||||
* Fixed a crash triggered when swiping items
|
||||
* Adjusted pull to refresh component to match Material You theme
|
||||
|
||||
## [1.35.0] - 2023-09-19
|
||||
|
||||
### Changed
|
||||
@@ -301,7 +334,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
* Initial Play Store release
|
||||
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.35.0...HEAD
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.38.0...HEAD
|
||||
|
||||
[1.38.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.37.0...v1.38.0
|
||||
|
||||
[1.37.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.36.0...v1.37.0
|
||||
|
||||
[1.36.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.35.0...v1.36.0
|
||||
|
||||
[1.35.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.34.0...v1.35.0
|
||||
|
||||
|
||||
@@ -10,34 +10,14 @@ Unofficial Android app for read-only access to [lobste.rs](https://lobste.rs), b
|
||||
|
||||
> Snapshots from the development branch can be obtained [here](https://github.com/msfjarvis/compose-lobsters/releases/tag/nightly).
|
||||
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature.webp"
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature_light.webp#gh-light-mode-only"
|
||||
alt="A grid of screenshots from the app, in clockwise order: the main screen, the comments page, the search page and the saved posts page"
|
||||
width="550" />
|
||||
|
||||
## Dependency Diagram
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature_dark.webp#gh-dark-mode-only"
|
||||
alt="A grid of screenshots from the app, in clockwise order: the main screen, the comments page, the search page and the saved posts page"
|
||||
width="550" />
|
||||
|
||||
```mermaid
|
||||
%%{
|
||||
init: {
|
||||
'theme': 'dark'
|
||||
}
|
||||
}%%
|
||||
|
||||
graph TB
|
||||
|
||||
benchmark --> android
|
||||
android --> api
|
||||
android --> common
|
||||
android --> core
|
||||
android --> database
|
||||
android --> model
|
||||
android --> benchmark
|
||||
api --> model
|
||||
common --> core
|
||||
common --> database
|
||||
common --> model
|
||||
|
||||
```
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE)
|
||||
|
||||
@@ -46,6 +46,9 @@ licensee {
|
||||
allow("MIT")
|
||||
ignoreDependencies("org.commonmark") { because("Commonmark is BSD licensed") }
|
||||
allowUrl("https://jsoup.org/license") { because("Jsoup is MIT licensed") }
|
||||
allowUrl("https://opensource.org/licenses/isc-license.txt") {
|
||||
because("kotlin-result is ISC licensed")
|
||||
}
|
||||
}
|
||||
|
||||
moduleGraphAssert {
|
||||
@@ -65,12 +68,12 @@ dependencies {
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.material)
|
||||
implementation(libs.androidx.compose.glance)
|
||||
implementation(libs.androidx.compose.glance.m3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material3.window.size)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.lifecycle.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
@@ -82,6 +85,7 @@ dependencies {
|
||||
implementation(libs.jsoup)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.material3.pulltorefresh)
|
||||
implementation(libs.napier)
|
||||
implementation(libs.sqldelight.extensions.coroutines)
|
||||
implementation(libs.swipe)
|
||||
@@ -89,7 +93,7 @@ dependencies {
|
||||
implementation(projects.api)
|
||||
implementation(projects.common)
|
||||
implementation(projects.core)
|
||||
implementation(projects.database)
|
||||
implementation(projects.database.impl)
|
||||
implementation(projects.model)
|
||||
|
||||
kapt(libs.dagger.compiler)
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
|
||||
<issue
|
||||
id="VectorPath"
|
||||
message="Very long vector path (918 characters), which is bad for performance. Considering reducing precision, removing minor details or rasterizing vector."
|
||||
errorLine1=" android:pathData="M431.3,341.2C435.9,337 580.3,236 710.5,223.8C747.3,220.3 790.7,254.7 833.2,312.5C875.9,370.7 892.9,433.4 875.9,443C863.2,450.2 838.6,407.4 758.8,396.3C743.4,394.2 730.4,393.4 719.4,393.5C738.5,423.4 745.6,451.1 737.2,458C733.1,461.4 673.6,438.7 612.9,436.5C570.1,435 535.2,440.9 506.7,451.3C496.9,454.9 486,453.8 477.2,448.3C444.5,427.9 424.2,406.2 418.4,376.2C415.8,362.8 420.9,349.4 431.3,341.2ZM274.7,394.8C285.8,370.1 312.2,325.9 346.3,305.4C383.6,282.9 335,390.3 427.3,489.8C519.5,589.3 586.5,623.8 655.1,581.5C723.7,539.2 746.5,518.1 753,518.7C764,519.6 772.9,536.1 764.4,589.5C763,598.2 761.3,606.5 759.3,614.2C828.7,575 871.7,516.3 889,520C912,524.8 918.3,617.8 852.1,721C811.8,784 768.8,830.7 741.4,857.5C724.3,874 699.7,880 677,873.1C512,822.6 315.5,733.8 202.9,652C193.8,645.2 186.8,635.9 183,625.1C171.8,593.7 161.3,548.1 168.5,495.3C179.5,414.2 222.7,352.9 245.2,335.9C261.9,323.2 267.1,362.4 274.7,394.8Z""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_launcher_foreground.xml"
|
||||
line="17"
|
||||
column="27"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -132,6 +132,17 @@
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".SearchActivity"
|
||||
android:configChanges="colorMode|density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
|
||||
android:exported="true"
|
||||
android:label="Claw Search"
|
||||
android:theme="@style/Theme.Claw">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
@@ -146,23 +157,34 @@
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
<!-- https://issuetracker.google.com/issues/258619948 -->
|
||||
<receiver
|
||||
android:name="androidx.profileinstaller.ProfileInstallReceiver"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:name=".glance.WidgetReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION" />
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/saved_posts_widget_info" />
|
||||
</receiver>
|
||||
<!-- Required: set your sentry.io project identifier (DSN) -->
|
||||
<meta-data android:name="io.sentry.dsn" android:value="${sentryDsn}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.dsn"
|
||||
android:value="${sentryDsn}" />
|
||||
<!-- enable automatic breadcrumbs for user interactions (clicks, swipes, scrolls) -->
|
||||
<meta-data android:name="io.sentry.traces.user-interaction.enable" android:value="${enableSentry}" />
|
||||
<meta-data android:name="io.sentry.breadcrumbs.user-interaction" android:value="${enableSentry}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.traces.user-interaction.enable"
|
||||
android:value="${enableSentry}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.breadcrumbs.user-interaction"
|
||||
android:value="${enableSentry}" />
|
||||
<!-- enable screenshot for crashes -->
|
||||
<meta-data android:name="io.sentry.attach-screenshot" android:value="${enableSentry}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-screenshot"
|
||||
android:value="${enableSentry}" />
|
||||
<!-- enable view hierarchy for crashes -->
|
||||
<meta-data android:name="io.sentry.attach-view-hierarchy" android:value="${enableSentry}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-view-hierarchy"
|
||||
android:value="${enableSentry}" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -7,9 +7,11 @@
|
||||
package dev.msfjarvis.claw.android
|
||||
|
||||
import android.app.assist.AssistContent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
@@ -18,7 +20,7 @@ import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
|
||||
import dev.msfjarvis.claw.android.ui.LobstersApp
|
||||
import dev.msfjarvis.claw.android.ui.screens.LobstersPostsScreen
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
@@ -36,14 +38,25 @@ class MainActivity : ComponentActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
installSplashScreen()
|
||||
Whetstone.inject(this)
|
||||
enableEdgeToEdge()
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle =
|
||||
SystemBarStyle.light(
|
||||
Color.TRANSPARENT,
|
||||
Color.TRANSPARENT,
|
||||
),
|
||||
navigationBarStyle =
|
||||
SystemBarStyle.light(
|
||||
Color.TRANSPARENT,
|
||||
Color.TRANSPARENT,
|
||||
),
|
||||
)
|
||||
setContent {
|
||||
val windowSizeClass = calculateWindowSizeClass(this)
|
||||
LobstersTheme(
|
||||
dynamicColor = true,
|
||||
providedValues = arrayOf(LocalUriHandler provides urlLauncher),
|
||||
) {
|
||||
LobstersApp(
|
||||
LobstersPostsScreen(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
windowSizeClass = windowSizeClass,
|
||||
@@ -63,4 +76,8 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NAVIGATION_KEY = "postId"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.app.assist.AssistContent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
|
||||
import dev.msfjarvis.claw.android.ui.screens.SearchScreen
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesActivityInjector
|
||||
class SearchActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var urlLauncher: UrlLauncher
|
||||
@Inject lateinit var htmlConverter: HTMLConverter
|
||||
@Inject lateinit var viewModel: ClawViewModel
|
||||
private var webUri: String? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Whetstone.inject(this)
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle =
|
||||
SystemBarStyle.light(
|
||||
Color.TRANSPARENT,
|
||||
Color.TRANSPARENT,
|
||||
),
|
||||
navigationBarStyle =
|
||||
SystemBarStyle.light(
|
||||
Color.TRANSPARENT,
|
||||
Color.TRANSPARENT,
|
||||
),
|
||||
)
|
||||
setContent {
|
||||
LobstersTheme(
|
||||
dynamicColor = true,
|
||||
providedValues = arrayOf(LocalUriHandler provides urlLauncher),
|
||||
) {
|
||||
SearchScreen(urlLauncher, htmlConverter, { webUri = it })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent?) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
if (outContent != null) {
|
||||
if (webUri != null) {
|
||||
outContent.webUri = Uri.parse(webUri)
|
||||
} else {
|
||||
outContent.webUri = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.appWidgetBackground
|
||||
import androidx.glance.appwidget.lazy.LazyColumn
|
||||
import androidx.glance.appwidget.lazy.items
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.background
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.material3.ColorProviders
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import dev.msfjarvis.claw.common.theme.DarkThemeColors
|
||||
import dev.msfjarvis.claw.common.theme.LightThemeColors
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
|
||||
class SavedPostsWidget(
|
||||
private val posts: List<SavedPost>,
|
||||
) : GlanceAppWidget() {
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
provideContent {
|
||||
GlanceTheme(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) GlanceTheme.colors
|
||||
else ColorProviders(light = LightThemeColors, dark = DarkThemeColors)
|
||||
) {
|
||||
WidgetHost()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WidgetHost() {
|
||||
LazyColumn(
|
||||
modifier =
|
||||
GlanceModifier.fillMaxSize()
|
||||
.background(GlanceTheme.colors.background)
|
||||
.appWidgetBackground(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
val style = MaterialTheme.typography.titleLarge
|
||||
Text(
|
||||
text = "Saved posts",
|
||||
style =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onBackground,
|
||||
fontSize = style.fontSize,
|
||||
fontWeight = style.fontWeight.toGlance(),
|
||||
fontStyle = style.fontStyle.toGlance(),
|
||||
),
|
||||
modifier = GlanceModifier.padding(vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
items(posts) { post -> WidgetListEntry(post = post) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceComposable
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.action.ActionParameters.Key
|
||||
import androidx.glance.action.actionParametersOf
|
||||
import androidx.glance.action.actionStartActivity
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.action.actionStartActivity
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.background
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.wrapContentWidth
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.MainActivity.Companion.NAVIGATION_KEY
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
|
||||
private val destinationKey = Key<String>(NAVIGATION_KEY)
|
||||
|
||||
@Composable
|
||||
@GlanceComposable
|
||||
fun WidgetListEntry(
|
||||
post: SavedPost,
|
||||
modifier: GlanceModifier = GlanceModifier,
|
||||
) {
|
||||
val titleStyle = MaterialTheme.typography.titleMedium
|
||||
val subtitleStyle = MaterialTheme.typography.labelLarge
|
||||
val commentsAction =
|
||||
actionStartActivity<MainActivity>(actionParametersOf(destinationKey to post.shortId))
|
||||
val postAction =
|
||||
if (post.url.isNotEmpty()) actionStartActivity(Intent(Intent.ACTION_VIEW, Uri.parse(post.url)))
|
||||
else commentsAction
|
||||
Box(modifier.padding(8.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
GlanceModifier.fillMaxWidth()
|
||||
.background(GlanceTheme.colors.surfaceVariant)
|
||||
.cornerRadius(16.dp)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = post.title,
|
||||
modifier = GlanceModifier.defaultWeight().padding(start = 4.dp).clickable(postAction),
|
||||
style =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onSurfaceVariant,
|
||||
fontSize = titleStyle.fontSize,
|
||||
fontWeight = titleStyle.fontWeight.toGlance(),
|
||||
fontStyle = titleStyle.fontStyle.toGlance(),
|
||||
)
|
||||
)
|
||||
post.commentCount?.let { count ->
|
||||
Text(
|
||||
text = "$count comments",
|
||||
modifier =
|
||||
GlanceModifier.wrapContentWidth().padding(end = 4.dp).clickable(commentsAction),
|
||||
style =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onSurfaceVariant,
|
||||
fontSize = subtitleStyle.fontSize,
|
||||
fontWeight = subtitleStyle.fontWeight.toGlance(),
|
||||
fontStyle = subtitleStyle.fontStyle.toGlance(),
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.broadcastreceiver.ContributesBroadcastReceiverInjector
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@ContributesBroadcastReceiverInjector
|
||||
class WidgetReceiver : GlanceAppWidgetReceiver() {
|
||||
|
||||
@Inject lateinit var viewModel: ClawViewModel
|
||||
|
||||
override val glanceAppWidget: GlanceAppWidget
|
||||
get() = SavedPostsWidget(runBlocking { viewModel.savedPosts.first().take(50) })
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Whetstone.inject(this, context)
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.glance
|
||||
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
fun FontWeight?.toGlance(): androidx.glance.text.FontWeight? {
|
||||
return when (this) {
|
||||
FontWeight.Normal -> androidx.glance.text.FontWeight.Normal
|
||||
FontWeight.Medium -> androidx.glance.text.FontWeight.Medium
|
||||
FontWeight.Bold -> androidx.glance.text.FontWeight.Bold
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun FontStyle?.toGlance(): androidx.glance.text.FontStyle? {
|
||||
return when (this) {
|
||||
FontStyle.Normal -> androidx.glance.text.FontStyle.Normal
|
||||
FontStyle.Italic -> androidx.glance.text.FontStyle.Italic
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import com.squareup.anvil.annotations.optional.ForScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dev.msfjarvis.claw.core.coroutines.DispatcherProvider
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
class DataStoreModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideLoginDatastore(
|
||||
@ForScope(ApplicationScope::class) context: Context,
|
||||
dispatcherProvider: DispatcherProvider,
|
||||
): DataStore<Preferences> {
|
||||
return PreferenceDataStoreFactory.create(
|
||||
corruptionHandler = null,
|
||||
migrations = emptyList(),
|
||||
scope = CoroutineScope(dispatcherProvider.io() + SupervisorJob()),
|
||||
) {
|
||||
context.preferencesDataStoreFile("login")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import java.io.IOException
|
||||
@@ -21,15 +22,16 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Duplicate of [LobstersPagingSource] with an additional optimization to not continue loading
|
||||
* further pages if nothing is found in the current one. This is required to prevent the app from
|
||||
* hammering the search endpoint with up to 70 consecutive requests for the search results of a
|
||||
* blank query.
|
||||
* Specialized variant of [LobstersPagingSource] to prevent hammering the Lobsters search endpoint
|
||||
* with unnecessary requests. Rather than abstract out the API call this paging source takes in the
|
||||
* relevant parameters to short-circuit when there is no query specified in order to avoid calling
|
||||
* the API at all.
|
||||
*/
|
||||
class SearchPagingSource
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
|
||||
private val searchApi: LobstersSearchApi,
|
||||
@Assisted private val queryProvider: () -> String,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : PagingSource<Int, LobstersPost>() {
|
||||
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int? {
|
||||
@@ -39,8 +41,14 @@ constructor(
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
|
||||
val searchQuery = queryProvider()
|
||||
// If there is no query, we don't need to call the API at all.
|
||||
if (searchQuery.isEmpty()) {
|
||||
return LoadResult.Page(itemsBefore = 0, data = emptyList(), prevKey = null, nextKey = null)
|
||||
}
|
||||
val page = params.key ?: STARTING_PAGE_INDEX
|
||||
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
|
||||
val result = withContext(ioDispatcher) { searchApi.searchPosts(searchQuery, page) }
|
||||
return when (result) {
|
||||
is ApiResult.Success -> {
|
||||
// Optimization: prevent fetching more pages if we found no items, this means
|
||||
// there is no active search query.
|
||||
@@ -62,6 +70,6 @@ constructor(
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(remoteFetcher: RemoteFetcher<LobstersPost>): SearchPagingSource
|
||||
fun create(queryProvider: () -> String): SearchPagingSource
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.settings
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import javax.inject.Inject
|
||||
|
||||
class LoginSettings
|
||||
@Inject
|
||||
constructor(
|
||||
val dataStore: DataStore<Preferences>,
|
||||
) {
|
||||
companion object {
|
||||
val CSRF_TOKEN = stringPreferencesKey("csrf_token")
|
||||
val LOGIN_COOKIE = stringPreferencesKey("login_cookie")
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ package dev.msfjarvis.claw.android.ui.decorations
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -19,6 +20,7 @@ import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.DevicePreviews
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
import dev.msfjarvis.claw.common.ui.surfaceColorAtNavigationBarElevation
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -31,7 +33,10 @@ fun ClawAppBar(
|
||||
TopAppBar(
|
||||
title = title,
|
||||
modifier = modifier.shadow(8.dp),
|
||||
colors = TopAppBarDefaults.topAppBarColors(),
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtNavigationBarElevation()
|
||||
),
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actions,
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ fun DatabasePosts(
|
||||
key = { it.shortId },
|
||||
contentType = { "LobstersItem" },
|
||||
) { item ->
|
||||
ListItem(
|
||||
LobstersListItem(
|
||||
item = item,
|
||||
isSaved = { true },
|
||||
isRead = { false },
|
||||
|
||||
@@ -21,14 +21,13 @@ import me.saket.swipe.SwipeAction
|
||||
import me.saket.swipe.SwipeableActionsBox
|
||||
|
||||
@Composable
|
||||
fun ListItem(
|
||||
fun LobstersListItem(
|
||||
item: SavedPost,
|
||||
isSaved: suspend (SavedPost) -> Boolean,
|
||||
isSaved: (SavedPost) -> Boolean,
|
||||
isRead: suspend (String) -> Boolean,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val saved by produceState(false, item) { value = isSaved(item) }
|
||||
val read by produceState(false, item.shortId) { value = isRead(item.shortId) }
|
||||
val commentsAction =
|
||||
SwipeAction(
|
||||
@@ -41,7 +40,7 @@ fun ListItem(
|
||||
) {
|
||||
LobstersCard(
|
||||
post = item,
|
||||
isSaved = saved,
|
||||
isSaved = isSaved(item),
|
||||
isRead = read,
|
||||
postActions = postActions,
|
||||
modifier = modifier,
|
||||
@@ -14,13 +14,8 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -35,13 +30,16 @@ import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import eu.bambooapps.material3.pullrefresh.PullRefreshIndicator
|
||||
import eu.bambooapps.material3.pullrefresh.pullRefresh
|
||||
import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NetworkPosts(
|
||||
lazyPagingItems: LazyPagingItems<LobstersPost>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: suspend (SavedPost) -> Boolean,
|
||||
isPostSaved: (SavedPost) -> Boolean,
|
||||
isPostRead: suspend (String) -> Boolean,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -69,7 +67,7 @@ fun NetworkPosts(
|
||||
val item = lazyPagingItems[index]
|
||||
if (item != null) {
|
||||
val dbModel = item.toSavedPost()
|
||||
ListItem(
|
||||
LobstersListItem(
|
||||
item = dbModel,
|
||||
isSaved = isPostSaved,
|
||||
isRead = isPostRead,
|
||||
@@ -95,8 +93,7 @@ fun NetworkPosts(
|
||||
refreshing = isRefreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
backgroundColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = contentColorFor(MaterialTheme.colorScheme.surface),
|
||||
shadowElevation = 6.dp, // From M2 implementation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,25 +6,11 @@
|
||||
*/
|
||||
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
|
||||
@@ -43,7 +29,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
fun SearchList(
|
||||
items: Flow<PagingData<LobstersPost>>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: suspend (SavedPost) -> Boolean,
|
||||
isPostSaved: (SavedPost) -> Boolean,
|
||||
postActions: PostActions,
|
||||
searchQuery: String,
|
||||
setSearchQuery: (String) -> Unit,
|
||||
@@ -54,42 +40,19 @@ fun SearchList(
|
||||
setSearchQuery(query)
|
||||
lazyPagingItems.refresh()
|
||||
}
|
||||
DisposableEffect(true) {
|
||||
// Clear search field when navigating away
|
||||
onDispose { setSearchQuery("") }
|
||||
}
|
||||
|
||||
var searchActive by remember { mutableStateOf(false) }
|
||||
Column(modifier = Modifier.semantics { isTraversalGroup = true }.zIndex(1f).fillMaxWidth()) {
|
||||
Column(modifier = modifier.semantics { isTraversalGroup = true }.zIndex(1f).fillMaxWidth()) {
|
||||
SearchBar(
|
||||
value = searchQuery,
|
||||
onValueChange = setSearchQuery,
|
||||
onSearch = {
|
||||
triggerSearch(it)
|
||||
searchActive = true
|
||||
},
|
||||
onSearch = { triggerSearch(it) },
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp).testTag("search_bar"),
|
||||
)
|
||||
if (searchActive) {
|
||||
NetworkPosts(
|
||||
lazyPagingItems = lazyPagingItems,
|
||||
listState = listState,
|
||||
isPostSaved = isPostSaved,
|
||||
isPostRead = { false },
|
||||
postActions = postActions,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.SearchOff,
|
||||
contentDescription = "No search results",
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally).size(36.dp),
|
||||
)
|
||||
Text(text = "Nothing to see here", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
NetworkPosts(
|
||||
lazyPagingItems = lazyPagingItems,
|
||||
listState = listState,
|
||||
isPostSaved = isPostSaved,
|
||||
isPostRead = { false },
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,6 @@ 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"
|
||||
@@ -39,6 +35,10 @@ sealed class Destinations {
|
||||
override val route: String = "datatransfer"
|
||||
}
|
||||
|
||||
object Search : Destinations() {
|
||||
override val route: String = "search"
|
||||
}
|
||||
|
||||
companion object {
|
||||
val startDestination
|
||||
get() = Hottest
|
||||
|
||||
@@ -4,45 +4,58 @@
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.ImportExport
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.NewReleases
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Whatshot
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material.icons.outlined.Whatshot
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
@@ -52,17 +65,20 @@ import androidx.navigation.navArgument
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.android.SearchActivity
|
||||
import dev.msfjarvis.claw.android.ui.datatransfer.DataTransferScreen
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawAppBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
|
||||
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
|
||||
import dev.msfjarvis.claw.android.ui.getActivity
|
||||
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
|
||||
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
|
||||
import dev.msfjarvis.claw.android.ui.lists.SearchList
|
||||
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import dev.msfjarvis.claw.android.ui.rememberPostActions
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
@@ -75,7 +91,7 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LobstersApp(
|
||||
fun LobstersPostsScreen(
|
||||
urlLauncher: UrlLauncher,
|
||||
htmlConverter: HTMLConverter,
|
||||
windowSizeClass: WindowSizeClass,
|
||||
@@ -86,7 +102,6 @@ fun LobstersApp(
|
||||
val hottestListState = rememberLazyListState()
|
||||
val newestListState = rememberLazyListState()
|
||||
val savedListState = rememberLazyListState()
|
||||
val searchListState = rememberLazyListState()
|
||||
val navController = rememberNavController()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
@@ -97,10 +112,19 @@ fun LobstersApp(
|
||||
|
||||
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
|
||||
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
|
||||
val savedPosts by viewModel.savedPosts.collectAsState(persistentMapOf())
|
||||
val savedPosts by viewModel.savedPostsByMonth.collectAsState(persistentMapOf())
|
||||
|
||||
val navigationType = ClawNavigationType.fromSize(windowSizeClass.widthSizeClass)
|
||||
|
||||
val postIdOverride = context.getActivity()?.intent?.extras?.getString(MainActivity.NAVIGATION_KEY)
|
||||
LaunchedEffect(false) {
|
||||
if (postIdOverride != null) {
|
||||
navController.navigate(
|
||||
Destinations.Comments.route.replace(Destinations.Comments.placeholder, postIdOverride)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val navItems =
|
||||
persistentListOf(
|
||||
NavigationItem(
|
||||
@@ -127,52 +151,76 @@ fun LobstersApp(
|
||||
) {
|
||||
coroutineScope.launch { savedListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Search",
|
||||
route = Destinations.Search.route,
|
||||
icon = Icons.Outlined.Search,
|
||||
selectedIcon = Icons.Filled.Search,
|
||||
) {
|
||||
coroutineScope.launch { searchListState.animateScrollToItem(index = 0) }
|
||||
},
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (currentDestination != Destinations.Search.route) {
|
||||
ClawAppBar(
|
||||
navigationIcon = {
|
||||
if (
|
||||
navController.previousBackStackEntry != null &&
|
||||
navItems.none { it.route == currentDestination }
|
||||
ClawAppBar(
|
||||
navigationIcon = {
|
||||
if (
|
||||
navController.previousBackStackEntry != null &&
|
||||
navItems.none { it.route == currentDestination }
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() },
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Go back to previous screen",
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Go back to previous screen",
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
}
|
||||
},
|
||||
title = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
IconButton(onClick = { navController.navigate(Destinations.DataTransfer.route) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ImportExport,
|
||||
contentDescription = "Data transfer options",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
IconButton(onClick = { expanded = true }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = "Extra options")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Backup and restore") },
|
||||
onClick = {
|
||||
navController.navigate(Destinations.DataTransfer.route)
|
||||
expanded = false
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ImportExport,
|
||||
contentDescription = "Data transfer options",
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Search posts") },
|
||||
onClick = {
|
||||
context.startActivity(Intent(context, SearchActivity::class.java))
|
||||
expanded = false
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Search,
|
||||
contentDescription = "Search posts",
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
|
||||
@@ -231,17 +279,6 @@ 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 }),
|
||||
@@ -251,7 +288,10 @@ fun LobstersApp(
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.placeholder}" },
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val postId = requireNotNull(backStackEntry.arguments?.getString("postId"))
|
||||
val postId =
|
||||
requireNotNull(backStackEntry.arguments?.getString("postId")) {
|
||||
"Navigating to ${Destinations.Comments.route} without necessary 'postId' argument"
|
||||
}
|
||||
setWebUri("https://lobste.rs/s/$postId")
|
||||
CommentsPage(
|
||||
postId = postId,
|
||||
@@ -267,7 +307,10 @@ fun LobstersApp(
|
||||
deepLinks =
|
||||
listOf(navDeepLink { uriPattern = "$uri/u/${Destinations.User.placeholder}" }),
|
||||
) { backStackEntry ->
|
||||
val username = requireNotNull(backStackEntry.arguments?.getString("username"))
|
||||
val username =
|
||||
requireNotNull(backStackEntry.arguments?.getString("username")) {
|
||||
"Navigating to ${Destinations.User.route} without necessary 'username' argument"
|
||||
}
|
||||
setWebUri("https://lobste.rs/u/$username")
|
||||
UserProfile(
|
||||
username = username,
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||
import dev.msfjarvis.claw.android.ui.lists.SearchList
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import dev.msfjarvis.claw.android.ui.rememberPostActions
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
urlLauncher: UrlLauncher,
|
||||
htmlConverter: HTMLConverter,
|
||||
setWebUri: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val postActions = rememberPostActions(urlLauncher, navController, viewModel)
|
||||
val listState = rememberLazyListState()
|
||||
Scaffold(modifier = modifier) { paddingValues ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Destinations.Search.route,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
) {
|
||||
composable(route = Destinations.Search.route) {
|
||||
setWebUri("https://lobste.rs/search")
|
||||
SearchList(
|
||||
items = viewModel.searchResults,
|
||||
listState = listState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
postActions = postActions,
|
||||
searchQuery = viewModel.searchQuery,
|
||||
setSearchQuery = { query -> viewModel.searchQuery = query },
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Destinations.Comments.route,
|
||||
arguments = listOf(navArgument("postId") { type = NavType.StringType }),
|
||||
) { backStackEntry ->
|
||||
val postId =
|
||||
requireNotNull(backStackEntry.arguments?.getString("postId")) {
|
||||
"Navigating to ${Destinations.Comments.route} without necessary 'postId' argument"
|
||||
}
|
||||
setWebUri("https://lobste.rs/s/$postId")
|
||||
CommentsPage(
|
||||
postId = postId,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,23 +6,29 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
|
||||
import com.slack.eithernet.ApiResult.Failure
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import com.squareup.anvil.annotations.optional.ForScope
|
||||
import dev.msfjarvis.claw.android.glance.SavedPostsWidget
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
|
||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
|
||||
import dev.msfjarvis.claw.android.paging.SearchPagingSource
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.core.injection.MainDispatcher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import java.io.IOException
|
||||
@@ -37,9 +43,12 @@ import javax.inject.Inject
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@ContributesViewModel
|
||||
@@ -47,7 +56,6 @@ class ClawViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val api: LobstersApi,
|
||||
private val searchApi: LobstersSearchApi,
|
||||
private val commentsRepository: CommentsRepository,
|
||||
private val readPostsRepository: ReadPostsRepository,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
@@ -56,7 +64,9 @@ constructor(
|
||||
private val pagingSourceFactory: LobstersPagingSource.Factory,
|
||||
private val searchPagingSourceFactory: SearchPagingSource.Factory,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
|
||||
@ForScope(ApplicationScope::class) context: Context,
|
||||
) : AndroidViewModel(context as Application) {
|
||||
private val hottestPostsPager =
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
pagingSourceFactory.create(api::getHottestPosts)
|
||||
@@ -67,7 +77,7 @@ constructor(
|
||||
}
|
||||
private val searchResultsPager =
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
searchPagingSourceFactory.create { searchApi.searchPosts(searchQuery, it) }
|
||||
searchPagingSourceFactory.create { searchQuery }
|
||||
}
|
||||
|
||||
val hottestPosts
|
||||
@@ -76,24 +86,41 @@ constructor(
|
||||
val newestPosts
|
||||
get() = newestPostsPager.flow
|
||||
|
||||
private val savedPostsFlow
|
||||
get() = savedPostsRepository.savedPosts
|
||||
|
||||
val savedPosts
|
||||
get() = savedPostsFlow.map(::mapSavedPosts)
|
||||
get() =
|
||||
savedPostsRepository.savedPosts.map {
|
||||
it.sortedByDescending { post -> post.createdAt.toLocalDateTime() }
|
||||
}
|
||||
|
||||
val savedPostsByMonth
|
||||
get() = savedPosts.map(::mapSavedPosts)
|
||||
|
||||
val searchResults
|
||||
get() = searchResultsPager.flow
|
||||
|
||||
var searchQuery by mutableStateOf("")
|
||||
|
||||
private val _savedPostsMutex = Mutex()
|
||||
private var _savedPosts = emptyList<String>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
savedPosts.collectLatest {
|
||||
_savedPostsMutex.withLock { _savedPosts = it.map(SavedPost::shortId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapSavedPosts(items: List<SavedPost>): ImmutableMap<Month, List<SavedPost>> {
|
||||
val sorted = items.sortedByDescending { post -> post.createdAt.toLocalDateTime() }
|
||||
val sorted =
|
||||
items.sortedWith { post1, post2 ->
|
||||
post2.createdAt.toLocalDateTime().compareTo(post1.createdAt.toLocalDateTime())
|
||||
}
|
||||
return sorted.groupBy { post -> post.createdAt.toLocalDateTime().month }.toImmutableMap()
|
||||
}
|
||||
|
||||
suspend fun isPostSaved(post: SavedPost): Boolean {
|
||||
return savedPostsFlow.first().any { savedPost -> savedPost.shortId == post.shortId }
|
||||
fun isPostSaved(post: SavedPost): Boolean {
|
||||
return _savedPosts.contains(post.shortId)
|
||||
}
|
||||
|
||||
fun toggleSave(post: SavedPost) {
|
||||
@@ -104,6 +131,10 @@ constructor(
|
||||
} else {
|
||||
savedPostsRepository.savePost(post)
|
||||
}
|
||||
val newPosts = savedPosts.first()
|
||||
withContext(mainDispatcher) {
|
||||
SavedPostsWidget(newPosts.take(50)).updateAll(getApplication())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
package dev.msfjarvis.claw.android.work
|
||||
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.deliveryhero.whetstone.worker.ContributesWorker
|
||||
import com.deliveryhero.whetstone.worker.WorkerScope
|
||||
import com.slack.eithernet.ApiResult.Success
|
||||
import com.squareup.anvil.annotations.optional.ForScope
|
||||
import dev.msfjarvis.claw.android.glance.SavedPostsWidget
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
@@ -42,6 +44,7 @@ constructor(
|
||||
.filterIsInstance<Success<LobstersPostDetails>>()
|
||||
.map { result -> result.value.toSavedPost() }
|
||||
.let { savedPostsRepository.savePosts(it) }
|
||||
SavedPostsWidget(savedPostsRepository.savedPosts.first().take(50)).updateAll(applicationContext)
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<!--
|
||||
~ Copyright © 2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1080"
|
||||
android:viewportHeight="1080">
|
||||
<group android:scaleX="0.7"
|
||||
android:scaleY="0.7"
|
||||
android:translateX="162"
|
||||
android:translateY="162">
|
||||
<path
|
||||
android:pathData="M431.3,341.2C435.9,337 580.3,236 710.5,223.8C747.3,220.3 790.7,254.7 833.2,312.5C875.9,370.7 892.9,433.4 875.9,443C863.2,450.2 838.6,407.4 758.8,396.3C743.4,394.2 730.4,393.4 719.4,393.5C738.5,423.4 745.6,451.1 737.2,458C733.1,461.4 673.6,438.7 612.9,436.5C570.1,435 535.2,440.9 506.7,451.3C496.9,454.9 486,453.8 477.2,448.3C444.5,427.9 424.2,406.2 418.4,376.2C415.8,362.8 420.9,349.4 431.3,341.2ZM274.7,394.8C285.8,370.1 312.2,325.9 346.3,305.4C383.6,282.9 335,390.3 427.3,489.8C519.5,589.3 586.5,623.8 655.1,581.5C723.7,539.2 746.5,518.1 753,518.7C764,519.6 772.9,536.1 764.4,589.5C763,598.2 761.3,606.5 759.3,614.2C828.7,575 871.7,516.3 889,520C912,524.8 918.3,617.8 852.1,721C811.8,784 768.8,830.7 741.4,857.5C724.3,874 699.7,880 677,873.1C512,822.6 315.5,733.8 202.9,652C193.8,645.2 186.8,635.9 183,625.1C171.8,593.7 161.3,548.1 168.5,495.3C179.5,414.2 222.7,352.9 245.2,335.9C261.9,323.2 267.1,362.4 274.7,394.8Z"
|
||||
android:fillColor="#FF4529"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -5,7 +5,7 @@
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 44 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 44 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 46 B |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 52 B |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 52 B |
|
Before Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,9 @@
|
||||
<!--
|
||||
~ Copyright © 2021-2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#540C00</color>
|
||||
</resources>
|
||||
@@ -7,13 +7,13 @@
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Claw.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">#FF6C0000</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_foreground</item>
|
||||
<item name="windowSplashScreenBackground">@color/ic_launcher_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
||||
<item name="windowSplashScreenAnimationDuration">1000</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.Claw</item>
|
||||
</style>
|
||||
|
||||
<style name="Platform.Theme.Claw" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<style name="Platform.Theme.Claw" parent="android:Theme.Material.NoActionBar" />
|
||||
|
||||
<style name="Base.Theme.Claw" parent="Platform.Theme.Claw">
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
~ Copyright © 2023 Harsh Shandilya.
|
||||
~ Use of this source code is governed by an MIT-style
|
||||
~ license that can be found in the LICENSE file or at
|
||||
~ https://opensource.org/licenses/MIT.
|
||||
-->
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="349dp"
|
||||
android:minHeight="110dp"
|
||||
android:minResizeWidth="349dp"
|
||||
android:minResizeHeight="110dp"
|
||||
android:resizeMode="vertical|horizontal"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen" />
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
|
||||
#
|
||||
versioning-plugin.versionCode=13500
|
||||
versioning-plugin.versionName=1.35.0
|
||||
versioning-plugin.versionCode=13800
|
||||
versioning-plugin.versionName=1.38.0
|
||||
|
||||
@@ -27,7 +27,9 @@ dependencies {
|
||||
implementation(libs.jsoup)
|
||||
|
||||
testImplementation(testFixtures(libs.eithernet))
|
||||
testImplementation(libs.testcontainers)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.kotlinx.serialization.json)
|
||||
testImplementation(libs.retrofit.kotlinxSerializationConverter)
|
||||
addTestDependencies(project)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -12,16 +12,15 @@ import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
/** Simple interface defining an API for lobste.rs */
|
||||
interface LobstersApi {
|
||||
|
||||
@GET("hottest.json")
|
||||
suspend fun getHottestPosts(@Query("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
@GET("page/{page}.json")
|
||||
suspend fun getHottestPosts(@Path("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
|
||||
@GET("newest.json")
|
||||
suspend fun getNewestPosts(@Query("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
@GET("newest/page/{page}.json")
|
||||
suspend fun getNewestPosts(@Path("page") page: Int): ApiResult<List<LobstersPost>, Unit>
|
||||
|
||||
@GET("s/{postId}.json")
|
||||
suspend fun getPostDetails(@Path("postId") postId: String): ApiResult<LobstersPostDetails, Unit>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.api
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.msfjarvis.claw.model.shiori.AuthRequest
|
||||
import dev.msfjarvis.claw.model.shiori.AuthResponse
|
||||
import dev.msfjarvis.claw.model.shiori.Bookmark
|
||||
import dev.msfjarvis.claw.model.shiori.BookmarkRequest
|
||||
import dev.msfjarvis.claw.model.shiori.BookmarksResponse
|
||||
import dev.msfjarvis.claw.model.shiori.EditedBookmark
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.HTTP
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
|
||||
private const val SESSION_ID_HEADER = "X-Session-Id"
|
||||
|
||||
interface ShioriApi {
|
||||
@POST("/api/login") suspend fun login(@Body body: AuthRequest): ApiResult<AuthResponse, Unit>
|
||||
|
||||
@HTTP(method = "POST", path = "/api/logout", hasBody = false)
|
||||
suspend fun logout(@Header(SESSION_ID_HEADER) sessionId: String): ApiResult<Unit, Unit>
|
||||
|
||||
@GET("/api/bookmarks")
|
||||
suspend fun getBookmarks(
|
||||
@Header(SESSION_ID_HEADER) sessionId: String,
|
||||
): ApiResult<BookmarksResponse, Unit>
|
||||
|
||||
@POST("/api/bookmarks")
|
||||
suspend fun addBookmark(
|
||||
@Header(SESSION_ID_HEADER) sessionId: String,
|
||||
@Body bookmarkRequest: BookmarkRequest,
|
||||
): ApiResult<Bookmark, Unit>
|
||||
|
||||
@PUT("/api/bookmarks")
|
||||
suspend fun editBookmark(
|
||||
@Header(SESSION_ID_HEADER) sessionId: String,
|
||||
@Body bookmark: EditedBookmark,
|
||||
): ApiResult<Bookmark, Unit>
|
||||
|
||||
@HTTP(method = "DELETE", path = "/api/bookmarks", hasBody = true)
|
||||
suspend fun deleteBookmark(
|
||||
@Header(SESSION_ID_HEADER) sessionId: String,
|
||||
@Body ids: List<Int>,
|
||||
): ApiResult<Int, Unit>
|
||||
}
|
||||
@@ -16,8 +16,10 @@ import dagger.multibindings.IntKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.api.ShioriApi
|
||||
import dev.msfjarvis.claw.api.converters.CSRFTokenConverter
|
||||
import dev.msfjarvis.claw.api.converters.SearchConverter
|
||||
import javax.inject.Named
|
||||
import javax.inject.Qualifier
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.CallAdapter
|
||||
@@ -40,7 +42,7 @@ object RetrofitModule {
|
||||
client: OkHttpClient,
|
||||
converterFactories: Map<Int, @JvmSuppressWildcards Converter.Factory>,
|
||||
callAdapterFactories: Map<Int, @JvmSuppressWildcards CallAdapter.Factory>,
|
||||
@BaseUrl baseUrl: String,
|
||||
@Named("LobstersURL") baseUrl: String,
|
||||
): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
@@ -56,7 +58,7 @@ object RetrofitModule {
|
||||
client: OkHttpClient,
|
||||
@SearchApi converterFactories: List<@JvmSuppressWildcards Converter.Factory>,
|
||||
callAdapterFactories: Map<Int, @JvmSuppressWildcards CallAdapter.Factory>,
|
||||
@BaseUrl baseUrl: String,
|
||||
@Named("LobstersURL") baseUrl: String,
|
||||
): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
@@ -72,6 +74,8 @@ object RetrofitModule {
|
||||
@Provides
|
||||
fun provideSearchApi(@SearchApi retrofit: Retrofit): LobstersSearchApi = retrofit.create()
|
||||
|
||||
@Provides fun provideShioriApi(retrofit: Retrofit): ShioriApi = retrofit.create()
|
||||
|
||||
@Provides
|
||||
@IntKey(0)
|
||||
@IntoMap
|
||||
@@ -95,10 +99,8 @@ object RetrofitModule {
|
||||
SearchConverter.Factory,
|
||||
)
|
||||
|
||||
@Provides @BaseUrl fun provideBaseUrl(): String = LobstersApi.BASE_URL
|
||||
@Provides @Named("LobstersURL") fun provideLobstersUrl(): String = LobstersApi.BASE_URL
|
||||
}
|
||||
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class BaseUrl
|
||||
|
||||
/** Internal Dagger [Qualifier] to identify dependencies exclusive to [LobstersSearchApi]. */
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) private annotation class SearchApi
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.api
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import com.slack.eithernet.ApiResultCallAdapterFactory
|
||||
import com.slack.eithernet.ApiResultConverterFactory
|
||||
import com.slack.eithernet.successOrNull
|
||||
import dev.msfjarvis.claw.model.shiori.AuthRequest
|
||||
import dev.msfjarvis.claw.model.shiori.AuthResponse
|
||||
import dev.msfjarvis.claw.model.shiori.Bookmark
|
||||
import dev.msfjarvis.claw.model.shiori.BookmarkRequest
|
||||
import dev.msfjarvis.claw.model.shiori.BookmarksResponse
|
||||
import dev.msfjarvis.claw.model.shiori.EditedBookmark
|
||||
import dev.msfjarvis.claw.model.shiori.Tag
|
||||
import dev.msfjarvis.claw.util.TestUtils.assertIs
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.testcontainers.containers.GenericContainer
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
|
||||
class ShioriApiTest {
|
||||
|
||||
private lateinit var credentials: AuthResponse
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
container.start()
|
||||
runBlocking {
|
||||
credentials =
|
||||
requireNotNull(api.login(AuthRequest(USER, PASSWORD)).successOrNull()) { "Login failed" }
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
container.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getBookmarks() = runTest {
|
||||
val response = api.getBookmarks(credentials.session).successOrNull()
|
||||
assertIs<BookmarksResponse>(response)
|
||||
assertThat(response.page).isEqualTo(1)
|
||||
assertThat(response.bookmarks).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addBookmark() = runTest {
|
||||
val response =
|
||||
api
|
||||
.addBookmark(
|
||||
credentials.session,
|
||||
BookmarkRequest(
|
||||
"https://example.com",
|
||||
false,
|
||||
0,
|
||||
emptyList(),
|
||||
"Example Domain",
|
||||
"""
|
||||
This domain is for use in illustrative examples in documents.
|
||||
You may use this domain in literature without prior coordination or asking for permission.
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
)
|
||||
.successOrNull()
|
||||
assertIs<Bookmark>(response)
|
||||
assertThat(response.url).isEqualTo("https://example.com")
|
||||
assertThat(response.title).isEqualTo("Example Domain")
|
||||
assertThat(response.excerpt)
|
||||
.isEqualTo(
|
||||
"""
|
||||
This domain is for use in illustrative examples in documents.
|
||||
You may use this domain in literature without prior coordination or asking for permission.
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
assertThat(response.id).isAtLeast(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun editBookmark() = runTest {
|
||||
val response =
|
||||
api
|
||||
.addBookmark(
|
||||
credentials.session,
|
||||
BookmarkRequest(
|
||||
"https://example.com",
|
||||
false,
|
||||
0,
|
||||
emptyList(),
|
||||
"Example Domain",
|
||||
"""
|
||||
This domain is for use in illustrative examples in documents.
|
||||
You may use this domain in literature without prior coordination or asking for permission.
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
)
|
||||
.successOrNull()
|
||||
assertIs<Bookmark>(response)
|
||||
assertThat(response.tags).isEmpty()
|
||||
val newBookmark =
|
||||
EditedBookmark(
|
||||
id = response.id,
|
||||
url = response.url,
|
||||
title = response.title,
|
||||
tags = listOf(Tag("examples")),
|
||||
)
|
||||
val edited = api.editBookmark(credentials.session, newBookmark).successOrNull()
|
||||
assertIs<Bookmark>(edited)
|
||||
assertThat(edited.tags).isNotEmpty()
|
||||
assertThat(edited.tags).containsExactly(Tag("examples"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteBookmark() = runTest {
|
||||
val response =
|
||||
api
|
||||
.addBookmark(
|
||||
credentials.session,
|
||||
BookmarkRequest(
|
||||
"https://example.com",
|
||||
false,
|
||||
0,
|
||||
emptyList(),
|
||||
"Example Domain",
|
||||
"""
|
||||
This domain is for use in illustrative examples in documents.
|
||||
You may use this domain in literature without prior coordination or asking for permission.
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
)
|
||||
.successOrNull()
|
||||
assertIs<Bookmark>(response)
|
||||
val count = api.deleteBookmark(credentials.session, listOf(response.id)).successOrNull()
|
||||
assertThat(count).isEqualTo(1)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Default settings for the container
|
||||
private const val USER = "shiori"
|
||||
private const val PASSWORD = "gopher"
|
||||
|
||||
private val container =
|
||||
GenericContainer("ghcr.io/go-shiori/shiori:v1.5.5").withExposedPorts(8080)
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
private val api
|
||||
get() =
|
||||
Retrofit.Builder()
|
||||
.baseUrl("http://${container.host}:${container.firstMappedPort}")
|
||||
.addConverterFactory(ApiResultConverterFactory)
|
||||
.addConverterFactory(json.asConverterFactory(MediaType.get("application/json")))
|
||||
.addCallAdapterFactory(ApiResultCallAdapterFactory)
|
||||
.build()
|
||||
.create<ShioriApi>()
|
||||
}
|
||||
}
|
||||
@@ -65,10 +65,12 @@ dependencies {
|
||||
implementation(libs.build.agp)
|
||||
implementation(libs.build.cachefix)
|
||||
implementation(libs.build.kotlin.gradle)
|
||||
implementation(libs.build.r8)
|
||||
implementation(libs.build.semver)
|
||||
implementation(libs.build.sentry)
|
||||
implementation(libs.build.spotless)
|
||||
implementation(libs.build.vcu)
|
||||
implementation(libs.build.versions)
|
||||
|
||||
// Expose the generated version catalog API to the plugin.
|
||||
implementation(files(libs::class.java.superclass.protectionDomain.codeSource.location))
|
||||
}
|
||||
|
||||
@@ -43,10 +43,6 @@ dependencyResolutionManagement {
|
||||
includeModule("gradle.plugin.org.gradle.android", "android-cache-fix-gradle-plugin")
|
||||
}
|
||||
}
|
||||
exclusiveContent {
|
||||
forRepository { maven("https://storage.googleapis.com/r8-releases/raw/main") }
|
||||
filter { includeModule("com.android.tools", "r8") }
|
||||
}
|
||||
mavenCentral()
|
||||
}
|
||||
versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } }
|
||||
|
||||
@@ -15,14 +15,13 @@ import com.android.build.api.variant.HasUnitTestBuilder
|
||||
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
||||
import com.android.build.gradle.BaseExtension
|
||||
import dev.msfjarvis.claw.gradle.LintConfig.configureLint
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.gradle.android.AndroidCacheFixPlugin
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.findByType
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
class AndroidCommonPlugin : Plugin<Project> {
|
||||
|
||||
@@ -59,9 +58,9 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
}
|
||||
project.extensions.findByType<ApplicationExtension>()?.lint?.configureLint(project)
|
||||
project.extensions.findByType<LibraryExtension>()?.lint?.configureLint(project)
|
||||
val catalog = project.extensions.getByType<VersionCatalogsExtension>()
|
||||
val libs = catalog.named("libs")
|
||||
project.dependencies.addProvider("lintChecks", libs.findLibrary("slack-compose-lints").get())
|
||||
val libs = project.extensions.getByName("libs") as LibrariesForLibs
|
||||
project.dependencies.addProvider("lintChecks", libs.slack.compose.lints)
|
||||
project.dependencies.addProvider("lintChecks", libs.slack.lints)
|
||||
}
|
||||
|
||||
private fun Project.configureSlimTests() {
|
||||
@@ -73,7 +72,7 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
beforeVariants(selector().all()) {
|
||||
if (it.name == "debug") return@beforeVariants
|
||||
(it as HasUnitTestBuilder).enableUnitTest = false
|
||||
it.enableAndroidTest = false
|
||||
it.androidTest.enable = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +81,7 @@ class AndroidCommonPlugin : Plugin<Project> {
|
||||
beforeVariants(selector().all()) {
|
||||
if (it.name == "debug") return@beforeVariants
|
||||
(it as HasUnitTestBuilder).enableUnitTest = false
|
||||
it.enableAndroidTest = false
|
||||
it.androidTest.enable = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ class ApplicationPlugin : Plugin<Project> {
|
||||
)
|
||||
)
|
||||
}
|
||||
named("release") { isMinifyEnabled = true }
|
||||
named("release") {
|
||||
isMinifyEnabled = !project.providers.environmentVariable("DISABLE_MINIFY").isPresent
|
||||
}
|
||||
named("debug") {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
|
||||
@@ -31,7 +31,7 @@ class KotlinCommonPlugin : Plugin<Project> {
|
||||
compilerOptions {
|
||||
allWarningsAsErrors.set(true)
|
||||
freeCompilerArgs.addAll(ADDITIONAL_COMPILER_ARGS)
|
||||
languageVersion.set(KotlinVersion.KOTLIN_1_8)
|
||||
languageVersion.set(KotlinVersion.KOTLIN_1_9)
|
||||
}
|
||||
}
|
||||
withType<Test>().configureEach {
|
||||
|
||||
@@ -31,10 +31,15 @@ object LintConfig {
|
||||
if (!isJVM) {
|
||||
enable += "ComposeM2Api"
|
||||
error += "ComposeM2Api"
|
||||
// False-positives in the TestContainers library
|
||||
disable += "DeprecatedCall"
|
||||
}
|
||||
baseline = project.file("lint-baseline.xml")
|
||||
// This is extremely annoying
|
||||
disable += "NewerVersionAvailable"
|
||||
disable += "GradleDependency"
|
||||
// Jetpack Compose lints are incompatible with AGP 8.3.0-alpha06
|
||||
disable += "ObsoleteLintCustomCheck"
|
||||
}
|
||||
|
||||
fun configureRootProject(project: Project) {
|
||||
|
||||
@@ -10,20 +10,18 @@ import com.android.build.api.variant.ApplicationAndroidComponentsExtension
|
||||
import io.sentry.android.gradle.extensions.InstrumentationFeature
|
||||
import io.sentry.android.gradle.extensions.SentryPluginExtension
|
||||
import java.util.EnumSet
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
@Suppress("Unused")
|
||||
class SentryPlugin : Plugin<Project> {
|
||||
|
||||
override fun apply(project: Project) {
|
||||
project.pluginManager.withPlugin("com.android.application") {
|
||||
val catalog = project.extensions.getByType<VersionCatalogsExtension>()
|
||||
val libs = catalog.named("libs")
|
||||
val libs = project.extensions.getByName("libs") as LibrariesForLibs
|
||||
project.extensions.configure<ApplicationAndroidComponentsExtension> {
|
||||
onVariants(selector().all()) { variant ->
|
||||
val sentryDsn = project.providers.environmentVariable(SENTRY_DSN_PROPERTY)
|
||||
@@ -52,13 +50,11 @@ class SentryPlugin : Plugin<Project> {
|
||||
experimentalGuardsquareSupport.set(false)
|
||||
autoInstallation {
|
||||
enabled.set(true)
|
||||
sentryVersion.set(libs.findVersion("sentry-sdk").get().requiredVersion)
|
||||
sentryVersion.set(libs.versions.sentry.sdk)
|
||||
}
|
||||
includeDependenciesReport.set(true)
|
||||
}
|
||||
with(project.dependencies) {
|
||||
addProvider("implementation", platform(libs.findLibrary("sentry-bom").get()))
|
||||
}
|
||||
with(project.dependencies) { addProvider("implementation", platform(libs.sentry.bom)) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@ package dev.msfjarvis.claw.gradle
|
||||
|
||||
import com.diffplug.gradle.spotless.SpotlessExtension
|
||||
import com.diffplug.gradle.spotless.SpotlessPlugin
|
||||
import com.diffplug.gradle.spotless.SpotlessTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
|
||||
@Suppress("Unused")
|
||||
class SpotlessPlugin : Plugin<Project> {
|
||||
@@ -43,13 +45,16 @@ class SpotlessPlugin : Plugin<Project> {
|
||||
endWithNewline()
|
||||
licenseHeaderFile(
|
||||
project.file("spotless/license.xml"),
|
||||
"<manifest|<resources|<full-backup-content|<data-extraction-rules|<adaptive-icon"
|
||||
"<(adaptive-icon|appwidget-provider|data-extraction-rules|full-backup-content|manifest|vector|resources)"
|
||||
)
|
||||
}
|
||||
}
|
||||
project.tasks.withType<SpotlessTask>().configureEach {
|
||||
notCompatibleWithConfigurationCache("https://github.com/diffplug/spotless/issues/987")
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val KTFMT_VERSION = "0.45"
|
||||
private const val KTFMT_VERSION = "0.46"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -12,6 +12,7 @@ import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import kotlin.io.path.createDirectory
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
@@ -21,6 +22,7 @@ import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.Optional
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
import org.gradle.api.tasks.PathSensitive
|
||||
import org.gradle.api.tasks.PathSensitivity
|
||||
@@ -33,6 +35,7 @@ abstract class CollectApksTask : DefaultTask() {
|
||||
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.NONE)
|
||||
@get:Optional
|
||||
abstract val mappingFile: RegularFileProperty
|
||||
|
||||
@get:Input abstract val variantName: Property<String>
|
||||
@@ -41,27 +44,30 @@ abstract class CollectApksTask : DefaultTask() {
|
||||
|
||||
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
|
||||
|
||||
override fun getGroup(): String {
|
||||
return "artifact collection"
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
fun run() {
|
||||
val outputDir = outputDirectory.asFile.get()
|
||||
val outputDirStream =
|
||||
Files.walk(outputDir.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile)
|
||||
outputDirStream.forEach(File::delete)
|
||||
outputDirStream.close()
|
||||
outputDir.mkdirs()
|
||||
val outputDir = outputDirectory.asFile.get().toPath()
|
||||
Files.walk(outputDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete)
|
||||
outputDir.createDirectory()
|
||||
val builtArtifacts =
|
||||
builtArtifactsLoader.get().load(apkFolder.get()) ?: error("Cannot load APKs")
|
||||
builtArtifacts.elements.forEach { artifact ->
|
||||
Files.copy(
|
||||
Paths.get(artifact.outputFile),
|
||||
outputDir.resolve("Claw-${variantName.get()}-${artifact.versionName}.apk").toPath(),
|
||||
outputDir.resolve("Claw-${variantName.get()}-${artifact.versionName}.apk"),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
mappingFile.orNull?.apply {
|
||||
Files.copy(
|
||||
asFile.toPath(),
|
||||
outputDir.resolve("mapping.txt"),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
Files.copy(
|
||||
mappingFile.get().asFile.toPath(),
|
||||
outputDir.resolve("mapping.txt").toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
@@ -10,19 +10,28 @@ import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption
|
||||
import kotlin.io.path.createDirectory
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.Optional
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
import org.gradle.api.tasks.PathSensitive
|
||||
import org.gradle.api.tasks.PathSensitivity
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
abstract class CollectBundleTask : DefaultTask() {
|
||||
@get:InputFile abstract val bundleFile: RegularFileProperty
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.NONE)
|
||||
abstract val bundleFile: RegularFileProperty
|
||||
|
||||
@get:InputFile abstract val mappingFile: RegularFileProperty
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.NONE)
|
||||
@get:Optional
|
||||
abstract val mappingFile: RegularFileProperty
|
||||
|
||||
@get:Input abstract val variantName: Property<String>
|
||||
|
||||
@@ -30,23 +39,26 @@ abstract class CollectBundleTask : DefaultTask() {
|
||||
|
||||
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
|
||||
|
||||
override fun getGroup(): String {
|
||||
return "artifact collection"
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
fun taskAction() {
|
||||
val outputDir = outputDirectory.asFile.get()
|
||||
val outputDirStream =
|
||||
Files.walk(outputDir.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile)
|
||||
outputDirStream.forEach(File::delete)
|
||||
outputDirStream.close()
|
||||
outputDir.mkdirs()
|
||||
val outputDir = outputDirectory.asFile.get().toPath()
|
||||
Files.walk(outputDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete)
|
||||
outputDir.createDirectory()
|
||||
Files.copy(
|
||||
bundleFile.get().asFile.toPath(),
|
||||
outputDir.resolve("Claw-${variantName.get()}-${versionName.get()}.aab").toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
Files.copy(
|
||||
mappingFile.get().asFile.toPath(),
|
||||
outputDir.resolve("mapping.txt").toPath(),
|
||||
outputDir.resolve("Claw-${variantName.get()}-${versionName.get()}.aab"),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
mappingFile.orNull?.apply {
|
||||
Files.copy(
|
||||
asFile.toPath(),
|
||||
outputDir.resolve("mapping.txt"),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,29 +6,27 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.ExternalModuleDependency
|
||||
import org.gradle.api.artifacts.MinimalExternalModuleDependency
|
||||
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||
import org.gradle.kotlin.dsl.DependencyHandlerScope
|
||||
import org.gradle.kotlin.dsl.exclude
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
/** Extension function to configure JUnit5 dependencies with the Truth assertion library. */
|
||||
fun DependencyHandlerScope.addTestDependencies(project: Project) {
|
||||
val catalog = project.extensions.getByType<VersionCatalogsExtension>()
|
||||
val libs = catalog.named("libs")
|
||||
addProvider("testImplementation", libs.findLibrary("junit-jupiter-api").get())
|
||||
val libs = project.extensions.getByName("libs") as LibrariesForLibs
|
||||
addProvider("testImplementation", libs.junit.jupiter.api)
|
||||
addProvider<MinimalExternalModuleDependency, ExternalModuleDependency>(
|
||||
"testImplementation",
|
||||
libs.findLibrary("truth").get(),
|
||||
libs.truth,
|
||||
) {
|
||||
exclude(group = "junit", module = "junit")
|
||||
}
|
||||
addProvider("testRuntimeOnly", libs.findLibrary("junit-jupiter-engine").get())
|
||||
addProvider("testRuntimeOnly", libs.junit.jupiter.engine)
|
||||
addProvider<MinimalExternalModuleDependency, ExternalModuleDependency>(
|
||||
"testRuntimeOnly",
|
||||
libs.findLibrary("junit-legacy").get(),
|
||||
libs.junit.legacy,
|
||||
) {
|
||||
// See https://github.com/google/truth/issues/333
|
||||
because("Truth needs it")
|
||||
|
||||
@@ -4,19 +4,8 @@
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
import dev.iurysouza.modulegraph.Orientation
|
||||
import dev.iurysouza.modulegraph.Theme
|
||||
|
||||
plugins {
|
||||
id("dev.msfjarvis.claw.spotless")
|
||||
id("dev.msfjarvis.claw.versions")
|
||||
id("dev.msfjarvis.claw.kotlin-common")
|
||||
alias(libs.plugins.modulegraph)
|
||||
}
|
||||
|
||||
moduleGraphConfig {
|
||||
heading.set("## Dependency Diagram")
|
||||
orientation.set(Orientation.TOP_TO_BOTTOM)
|
||||
readmePath.set(project.layout.projectDirectory.file("README.md").asFile.absolutePath)
|
||||
theme.set(Theme.DARK)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
dependencies {
|
||||
api(projects.core)
|
||||
api(projects.database)
|
||||
api(projects.database.core)
|
||||
api(projects.model)
|
||||
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
@@ -50,6 +50,7 @@ dependencies {
|
||||
implementation(libs.compose.richtext.ui)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinResult.coroutines)
|
||||
implementation(libs.napier)
|
||||
|
||||
compileOnly(libs.androidx.compose.ui.tooling.preview)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -37,6 +37,8 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.michaelbull.result.coroutines.runSuspendCatching
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.posts.PostTitle
|
||||
import dev.msfjarvis.claw.common.posts.Submitter
|
||||
@@ -61,7 +63,7 @@ internal fun CommentsHeader(
|
||||
produceState(
|
||||
initialValue = LinkMetadata(postDetails.url, null),
|
||||
) {
|
||||
runCatching { postActions.getLinkMetadata(postDetails.url) }
|
||||
runSuspendCatching { postActions.getLinkMetadata(postDetails.url) }
|
||||
.onSuccess { metadata -> value = metadata }
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ internal data class CommentNode(
|
||||
child.parent = this
|
||||
} else {
|
||||
child.indentLevel += 1
|
||||
children.last().addChild(child)
|
||||
children.lastOrNull()?.addChild(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ internal fun createListNode(
|
||||
)
|
||||
)
|
||||
} else {
|
||||
commentNodes.last().let {
|
||||
commentNodes.lastOrNull()?.let {
|
||||
it.addChild(
|
||||
CommentNode(
|
||||
comment = comments[i],
|
||||
|
||||
@@ -25,6 +25,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.michaelbull.result.coroutines.runSuspendCatching
|
||||
import com.github.michaelbull.result.fold
|
||||
import dev.msfjarvis.claw.common.NetworkState
|
||||
import dev.msfjarvis.claw.common.NetworkState.Error
|
||||
import dev.msfjarvis.claw.common.NetworkState.Loading
|
||||
@@ -109,10 +111,10 @@ fun CommentsPage(
|
||||
) {
|
||||
val postDetails by
|
||||
produceState<NetworkState>(Loading) {
|
||||
runCatching { postActions.getComments(postId) }
|
||||
runSuspendCatching { postActions.getComments(postId) }
|
||||
.fold(
|
||||
onSuccess = { details -> value = Success(details) },
|
||||
onFailure = { value = Error(error = it, description = "Failed to load comments") }
|
||||
success = { details -> value = Success(details) },
|
||||
failure = { value = Error(error = it, description = "Failed to load comments") },
|
||||
)
|
||||
}
|
||||
val commentState by
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.absoluteOffset
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -30,7 +31,6 @@ import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -186,7 +186,6 @@ private fun SaveButton(
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun CommentsButton(
|
||||
commentCount: Int?,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -197,6 +196,8 @@ private fun CommentsButton(
|
||||
if (commentCount != null) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
// Required to make the badge label look visually okay
|
||||
modifier = Modifier.absoluteOffset(y = (-8).dp),
|
||||
) {
|
||||
Text(
|
||||
text = commentCount.toString(),
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -16,10 +17,15 @@ import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ProvidedValue
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.halilibo.richtext.ui.material3.SetupMaterial3RichText
|
||||
|
||||
private val LightThemeColors =
|
||||
val LightThemeColors =
|
||||
lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
@@ -48,7 +54,7 @@ private val LightThemeColors =
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
)
|
||||
|
||||
private val DarkThemeColors =
|
||||
val DarkThemeColors =
|
||||
darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
@@ -97,6 +103,14 @@ fun LobstersTheme(
|
||||
}
|
||||
else -> if (darkTheme) DarkThemeColors else LightThemeColors
|
||||
}
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = Color.Transparent.toArgb()
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
||||
}
|
||||
}
|
||||
CompositionLocalProvider(*providedValues) {
|
||||
MaterialTheme(colorScheme = colorScheme, typography = AppTypography) {
|
||||
SetupMaterial3RichText { content() }
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.clickable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Icon
|
||||
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.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
|
||||
@Composable
|
||||
fun PasswordField(
|
||||
value: String,
|
||||
label: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var isPasswordVisible by remember { mutableStateOf(false) }
|
||||
TextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = { Text(label) },
|
||||
visualTransformation =
|
||||
if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
painter =
|
||||
if (isPasswordVisible) rememberVectorPainter(Icons.Filled.VisibilityOff)
|
||||
else rememberVectorPainter(Icons.Filled.Visibility),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.clickable { isPasswordVisible = !isPasswordVisible }
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun PasswordFieldPreview() {
|
||||
LobstersTheme {
|
||||
var value by remember { mutableStateOf("") }
|
||||
PasswordField(
|
||||
value = value,
|
||||
label = "Password",
|
||||
onValueChange = { value = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -22,17 +22,21 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
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
|
||||
import dev.msfjarvis.claw.common.ui.preview.DevicePreviews
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
@@ -41,6 +45,7 @@ fun SearchBar(
|
||||
onSearch: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
TextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
@@ -59,11 +64,14 @@ fun SearchBar(
|
||||
imeAction = ImeAction.Search,
|
||||
),
|
||||
singleLine = true,
|
||||
modifier = modifier.focusable(),
|
||||
modifier = modifier.focusable().focusRequester(focusRequester),
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
}
|
||||
|
||||
@Preview
|
||||
@DevicePreviews
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun SearchBarPreview() {
|
||||
LobstersTheme {
|
||||
@@ -73,7 +81,7 @@ fun SearchBarPreview() {
|
||||
value = value,
|
||||
onValueChange = { value = it },
|
||||
onSearch = {},
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
modifier = Modifier.align(Alignment.TopCenter).fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.michaelbull.result.coroutines.runSuspendCatching
|
||||
import com.github.michaelbull.result.fold
|
||||
import dev.msfjarvis.claw.common.NetworkState
|
||||
import dev.msfjarvis.claw.common.NetworkState.Error
|
||||
import dev.msfjarvis.claw.common.NetworkState.Loading
|
||||
@@ -44,10 +46,10 @@ fun UserProfile(
|
||||
) {
|
||||
val user by
|
||||
produceState<NetworkState>(Loading) {
|
||||
runCatching { getProfile(username) }
|
||||
runSuspendCatching { getProfile(username) }
|
||||
.fold(
|
||||
onSuccess = { profile -> value = Success(profile) },
|
||||
onFailure = {
|
||||
success = { profile -> value = Success(profile) },
|
||||
failure = {
|
||||
value =
|
||||
Error(
|
||||
error = it,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha02" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha02)" variant="all" version="8.3.0-alpha02">
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
@file:Suppress("InjectDispatcher") // False-positive
|
||||
@file:Suppress("RawDispatchersUse") // False-positive
|
||||
|
||||
package dev.msfjarvis.claw.core.coroutines
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -28,7 +28,7 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
interface CoroutineDispatcherModule {
|
||||
|
||||
@Binds fun DefaultDispatcherProvider.bind(): DispatcherProvider
|
||||
@Binds fun bindDispatcherProvider(impl: DefaultDispatcherProvider): DispatcherProvider
|
||||
|
||||
companion object {
|
||||
@[Provides IODispatcher]
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import dev.msfjarvis.claw.gradle.addTestDependencies
|
||||
|
||||
plugins {
|
||||
id("dev.msfjarvis.claw.kotlin-jvm")
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.sqldelight)
|
||||
}
|
||||
|
||||
sqldelight {
|
||||
databases {
|
||||
create("LobstersDatabase") {
|
||||
packageName.set("dev.msfjarvis.claw.database")
|
||||
schemaOutputDirectory.set(file("src/main/sqldelight/databases"))
|
||||
verifyMigrations.set(true)
|
||||
dialect(libs.sqldelight.dialect338)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
testImplementation(libs.kotlinx.serialization.json)
|
||||
addTestDependencies(project)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14">
|
||||
|
||||
</issues>
|
||||