Compare commits

...

104 commits

Author SHA1 Message Date
10c1f42ca3 Update dependency com.autonomousapps.dependency-analysis to v2.5.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 12s
Build & Test / Run Unit Tests (pull_request) Successful in 8m57s
Build & Test / Run UI Tests (pull_request) Successful in 18m1s
2024-11-21 07:03:08 +00:00
6fe4d99dfd Update dependency org.robolectric:robolectric to v4.14.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 12s
Build & Test / Run Unit Tests (pull_request) Successful in 14m7s
Build & Test / Run UI Tests (pull_request) Successful in 13m31s
2024-11-21 04:03:32 +00:00
dd76557232 Update dependency gradle to v8.11.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 16s
Build & Test / Run Unit Tests (pull_request) Successful in 15m46s
Build & Test / Run UI Tests (pull_request) Successful in 19m41s
2024-11-20 18:09:17 +00:00
092fb5bf3a Update dependency org.robolectric:robolectric to v4.14
All checks were successful
Build & Test / Validate (pull_request) Successful in 25s
Build & Test / Run Unit Tests (pull_request) Successful in 13m45s
Build & Test / Run UI Tests (pull_request) Successful in 18m8s
2024-11-15 08:02:31 +00:00
1ab9c3db55 Update dependency com.github.triplet.play to v3.12.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 26s
Build & Test / Run Unit Tests (pull_request) Successful in 33m2s
Build & Test / Run UI Tests (pull_request) Successful in 36m29s
2024-11-15 02:03:26 +00:00
49de5ab1f3 Update dependency androidx.compose:compose-bom to v2024.11.00
All checks were successful
Build & Test / Validate (pull_request) Successful in 19s
Build & Test / Run Unit Tests (pull_request) Successful in 13m31s
Build & Test / Run UI Tests (pull_request) Successful in 18m2s
2024-11-13 19:02:59 +00:00
d8b5f84e78 Update navigation to v2.8.4
All checks were successful
Build & Test / Validate (pull_request) Successful in 24s
Build & Test / Run Unit Tests (pull_request) Successful in 13m55s
Build & Test / Run UI Tests (pull_request) Successful in 18m21s
2024-11-13 18:02:34 +00:00
8b87ca0dd7 Update dependency gradle to v8.11
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 13m28s
Build & Test / Run UI Tests (pull_request) Successful in 19m0s
2024-11-12 17:07:59 +00:00
90a2550b0d Update dependency com.github.triplet.play to v3.12.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 13m21s
Build & Test / Run UI Tests (pull_request) Successful in 18m48s
2024-11-12 03:03:26 +00:00
6248f464d5
Fix issues with reloading files when navigating between app screens
All checks were successful
Build & Test / Validate (pull_request) Successful in 16s
Build & Test / Run Unit Tests (pull_request) Successful in 16m20s
Build & Test / Run UI Tests (pull_request) Successful in 20m21s
2024-11-06 22:26:31 -07:00
3b7a6fd57c
Fix scroll position not updating while typing in edit pane 2024-11-06 22:26:31 -07:00
2f5ebb28f6
Update changelog
Some checks failed
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Failing after 14m43s
Build & Test / Run UI Tests (pull_request) Has been skipped
2024-11-06 21:12:46 -07:00
6569ac64b2
Fix opening files from external apps
I somehow missed this when migrating to compose and worse yet, didn't have any tests covering it. That's been remedied now so hopefully it continues to work well into the future

Closes #90
2024-11-06 21:12:20 -07:00
13962a11d7
Improve animations for predictive back 2024-11-06 21:01:36 -07:00
380280686a
Consolidate androidx navigation versions 2024-11-06 20:50:37 -07:00
53df7da0aa Update dependency androidx.navigation:navigation-runtime-ktx to v2.8.3 2024-11-07 03:49:15 +00:00
9e8c65396e
Disable ObsoleteLintCustomCheck 2024-11-06 20:48:29 -07:00
730bf3f7d8
Opt in to predictive back gestures 2024-11-06 20:47:16 -07:00
80b18b242e Update dependency com.autonomousapps.dependency-analysis to v2.4.2
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 16m8s
Build & Test / Run UI Tests (pull_request) Successful in 19m42s
2024-11-04 00:03:17 +00:00
1a4e5b8e9e Update acra to v5.12.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 13m55s
Build & Test / Run UI Tests (pull_request) Successful in 18m0s
2024-11-03 18:02:40 +00:00
29469df9c6 Update androidGradlePlugin to v8.7.2
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 15m7s
Build & Test / Run UI Tests (pull_request) Successful in 19m18s
2024-11-02 02:03:05 +00:00
0d30c2b40d Update dependency com.autonomousapps.dependency-analysis to v2.4.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 16s
Build & Test / Run Unit Tests (pull_request) Successful in 15m6s
Build & Test / Run UI Tests (pull_request) Successful in 19m42s
2024-11-02 01:03:05 +00:00
055437c052 Update dependency com.autonomousapps.dependency-analysis to v2.4.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 12m40s
Build & Test / Run UI Tests (pull_request) Successful in 18m21s
2024-10-31 16:03:39 +00:00
9bc75b475e Update dependency androidx.core:core-ktx to v1.15.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 13m14s
Build & Test / Run UI Tests (pull_request) Successful in 17m51s
2024-10-31 14:03:47 +00:00
1de349e161 Update dependency androidx.lifecycle:lifecycle-viewmodel-ktx to v2.8.7
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 13m40s
Build & Test / Run UI Tests (pull_request) Successful in 18m7s
2024-10-31 12:03:06 +00:00
20d688d110 Update dependency androidx.compose:compose-bom to v2024.10.01
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 13m19s
Build & Test / Run UI Tests (pull_request) Successful in 21m8s
2024-10-31 09:03:05 +00:00
554937ae03 Update dependency androidx.compose.material:material-icons-core to v1.7.5
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 13m13s
Build & Test / Run UI Tests (pull_request) Successful in 18m2s
2024-10-31 05:03:00 +00:00
8e6e305ecf Update dependency androidx.compose.material3:material3-window-size-class-android to v1.3.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 15s
Build & Test / Run Unit Tests (pull_request) Successful in 13m33s
Build & Test / Run UI Tests (pull_request) Successful in 18m1s
2024-10-31 00:03:16 +00:00
c1ba9dcf91 Update animationCore to v1.7.5
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 14m21s
Build & Test / Run UI Tests (pull_request) Successful in 17m42s
2024-10-30 17:02:53 +00:00
9bc220d5bb Update commonMarkVersion to v0.24.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 14m4s
Build & Test / Run UI Tests (pull_request) Successful in 18m51s
2024-10-24 20:02:59 +00:00
bb158f0120 Update dependency com.autonomousapps.dependency-analysis to v2.3.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 14m51s
Build & Test / Run UI Tests (pull_request) Successful in 18m42s
2024-10-24 19:03:03 +00:00
8809d295c4 Update dependency androidx.compose:compose-bom to v2024.10.00
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 15m49s
Build & Test / Run UI Tests (pull_request) Successful in 19m17s
2024-10-19 01:04:28 +00:00
7de4996816 Update dependency androidx.test:orchestrator to v1.5.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 16s
Build & Test / Run Unit Tests (pull_request) Successful in 15m47s
Build & Test / Run UI Tests (pull_request) Successful in 19m26s
2024-10-18 23:03:48 +00:00
a4ef52f2cc Update dependency com.google.android.play:review-ktx to v2.0.2
All checks were successful
Build & Test / Validate (pull_request) Successful in 24s
Build & Test / Run Unit Tests (pull_request) Successful in 13m52s
Build & Test / Run UI Tests (pull_request) Successful in 17m38s
2024-10-18 21:09:23 +00:00
ce14205c4f Update dependency com.autonomousapps.dependency-analysis to v2.2.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 14m6s
Build & Test / Run UI Tests (pull_request) Successful in 19m32s
2024-10-18 20:06:49 +00:00
41607fba52 Update dependency androidx.compose.material:material-icons-core to v1.7.4
All checks were successful
Build & Test / Validate (pull_request) Successful in 22s
Build & Test / Run Unit Tests (pull_request) Successful in 13m48s
Build & Test / Run UI Tests (pull_request) Successful in 20m50s
2024-10-17 02:06:52 +00:00
ca83a92158 Update androidGradlePlugin to v8.7.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 26s
Build & Test / Run Unit Tests (pull_request) Successful in 15m41s
Build & Test / Run UI Tests (pull_request) Successful in 19m33s
2024-10-16 23:06:57 +00:00
c59faea4d4 Update dependency androidx.activity:activity-ktx to v1.9.3
All checks were successful
Build & Test / Validate (pull_request) Successful in 15s
Build & Test / Run Unit Tests (pull_request) Successful in 14m3s
Build & Test / Run UI Tests (pull_request) Successful in 18m18s
2024-10-16 19:03:08 +00:00
d028c8cb2f Update dependency androidx.compose.animation:animation to v1.7.4
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 14m33s
Build & Test / Run UI Tests (pull_request) Successful in 19m33s
2024-10-16 17:02:48 +00:00
f2f92b2318 Update kotlin to v2.0.21
All checks were successful
Build & Test / Validate (pull_request) Successful in 24s
Build & Test / Run Unit Tests (pull_request) Successful in 15m33s
Build & Test / Run UI Tests (pull_request) Successful in 20m4s
2024-10-10 10:11:20 +00:00
e94b5a67c1 Update dependency com.autonomousapps.dependency-analysis to v2.1.4
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 14m29s
Build & Test / Run UI Tests (pull_request) Successful in 19m13s
2024-10-04 20:07:45 +00:00
e386fcd82f Update dependency com.autonomousapps.dependency-analysis to v2.1.3
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 13m4s
Build & Test / Run UI Tests (pull_request) Successful in 18m1s
2024-10-04 04:03:21 +00:00
cbbef5cf6f
Update changelog 2024-10-03 20:57:15 -06:00
e8eb71e18b
Bump version for release
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 24m51s
Build & Test / Run UI Tests (pull_request) Successful in 21m35s
2024-10-03 16:25:04 -06:00
5abfe147f3 Update dependency com.android.billingclient:billing to v7.1.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 14m7s
Build & Test / Run UI Tests (pull_request) Successful in 17m54s
2024-10-03 20:01:53 +00:00
24ed864645 Update dependency com.autonomousapps.dependency-analysis to v2.1.2
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 13m21s
Build & Test / Run UI Tests (pull_request) Successful in 18m1s
2024-10-03 16:01:53 +00:00
4f36b2f54c Update dependency androidx.compose:compose-bom to v2024.09.03
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 13m23s
Build & Test / Run UI Tests (pull_request) Successful in 18m19s
2024-10-02 20:02:54 +00:00
b6ee357407 Update dependency androidx.compose.material:material-icons-core to v1.7.3
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 14m29s
Build & Test / Run UI Tests (pull_request) Successful in 18m30s
2024-10-02 18:03:12 +00:00
154ccad9f1 Update animationCore to v1.7.3
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 13m46s
Build & Test / Run UI Tests (pull_request) Successful in 18m4s
2024-10-02 17:03:22 +00:00
d9032fb686 Update androidGradlePlugin to v8.7.0 2024-10-02 14:46:41 +00:00
9898e09ac6 Update dependency com.autonomousapps.dependency-analysis to v2.1.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 13m26s
Build & Test / Run UI Tests (pull_request) Successful in 19m20s
2024-10-02 13:03:41 +00:00
4932ac15f8 Fix deprecated usage of gradle-build action 2024-10-02 12:38:20 +00:00
8fd4c8f8fd Update gradle/gradle-build-action action to v3 2024-10-02 12:38:20 +00:00
79b0f9996f Remove dependency on hamcrest core 2024-10-02 12:37:56 +00:00
7024ffc6d6 Update dependency org.hamcrest:hamcrest-core to v3 2024-10-02 12:37:56 +00:00
2f30d2fd6f Only set publishCredentialsFile if not blank 2024-10-02 12:37:17 +00:00
0ade9be784 Only set publishCredentialsFile if present 2024-10-02 12:37:17 +00:00
bf01006004 Update release notes 2024-10-02 12:37:17 +00:00
e5e072e4fe Add publish credentials to keystore.properties.sample 2024-10-02 12:37:17 +00:00
6d34bb6f94 Fix changelog task to use git tag as input 2024-10-02 12:37:17 +00:00
9d26752d2b Opt-in to ExperimentalCoroutinesApi for MarkdownViewModelTest 2024-10-02 12:36:04 +00:00
3f5c6b7ebf Suppress unchecked cast for FakePreferenceHelper#observe
I know it's not great but it works for now
2024-10-02 12:36:04 +00:00
79d609f138 Bump Java version to 11 2024-10-02 12:36:04 +00:00
4271ded6aa Suppress unchecked cast for PreferenceHelper#observe
I know it's not great but it works for now
2024-10-02 12:36:04 +00:00
7f945ba5fe Update HorizontalPager parameters to reflect API changes 2024-10-02 12:36:04 +00:00
12d5fcb834 Update dependency androidx.compose:compose-bom to v2024.09.02 2024-10-02 12:36:04 +00:00
3ac064c02c
Fix deprecated usage of setup-gradle action
All checks were successful
Build & Test / Validate (pull_request) Successful in 27s
Build & Test / Run Unit Tests (pull_request) Successful in 16m36s
Build & Test / Run UI Tests (pull_request) Successful in 20m12s
2024-10-01 20:49:57 -06:00
b0105dc5a4 Update gradle/actions action to v4
Some checks failed
Build & Test / Validate (pull_request) Successful in 16s
Build & Test / Run Unit Tests (pull_request) Failing after 1m1s
Build & Test / Run UI Tests (pull_request) Has been skipped
2024-09-24 02:02:47 +00:00
ee5db64532 Update dependency gradle to v8.10.2
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 13m58s
Build & Test / Run UI Tests (pull_request) Successful in 17m52s
2024-09-23 22:11:50 +00:00
9b9ab9b971 Update dependency com.autonomousapps.dependency-analysis to v2.1.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 24s
Build & Test / Run Unit Tests (pull_request) Successful in 13m29s
Build & Test / Run UI Tests (pull_request) Successful in 22m20s
2024-09-23 19:03:38 +00:00
428b6ec76b Update dependency com.android.billingclient:billing to v7.1.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 22s
Build & Test / Run Unit Tests (pull_request) Successful in 12m59s
Build & Test / Run UI Tests (pull_request) Successful in 17m56s
2024-09-19 22:02:17 +00:00
caade0c0d1 Update dependency androidx.lifecycle:lifecycle-viewmodel-ktx to v2.8.6
All checks were successful
Build & Test / Validate (pull_request) Successful in 19s
Build & Test / Run Unit Tests (pull_request) Successful in 12m58s
Build & Test / Run UI Tests (pull_request) Successful in 17m0s
2024-09-18 23:01:48 +00:00
362a54c653 Update dependency androidx.compose.material:material-icons-core to v1.7.2
All checks were successful
Build & Test / Validate (pull_request) Successful in 21s
Build & Test / Run Unit Tests (pull_request) Successful in 13m33s
Build & Test / Run UI Tests (pull_request) Successful in 18m13s
2024-09-18 20:02:16 +00:00
d1a9b114b6 Update animationCore to v1.7.2
All checks were successful
Build & Test / Validate (pull_request) Successful in 19s
Build & Test / Run Unit Tests (pull_request) Successful in 13m32s
Build & Test / Run UI Tests (pull_request) Successful in 18m7s
2024-09-18 18:02:39 +00:00
cb973ba0ec Update coroutines to v1.9.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 19s
Build & Test / Run Unit Tests (pull_request) Successful in 12m43s
Build & Test / Run UI Tests (pull_request) Successful in 18m23s
2024-09-17 10:03:01 +00:00
8cb0f7c94d Update commonMarkVersion to v0.23.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 12m24s
Build & Test / Run UI Tests (pull_request) Successful in 18m5s
2024-09-17 07:03:12 +00:00
5035c287d2 Update dependency com.autonomousapps.dependency-analysis to v2.0.2
All checks were successful
Build & Test / Validate (pull_request) Successful in 23s
Build & Test / Run Unit Tests (pull_request) Successful in 12m53s
Build & Test / Run UI Tests (pull_request) Successful in 17m35s
2024-09-17 04:03:54 +00:00
5c2807ff9a Update dependency androidx.compose.material:material-icons-core to v1.7.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 20s
Build & Test / Run Unit Tests (pull_request) Successful in 12m58s
Build & Test / Run UI Tests (pull_request) Successful in 17m18s
2024-09-17 01:02:38 +00:00
a1b3672472 Update animationCore to v1.7.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 21s
Build & Test / Run Unit Tests (pull_request) Successful in 12m40s
Build & Test / Run UI Tests (pull_request) Successful in 17m13s
2024-09-16 23:02:40 +00:00
3033c65a8f Update acra to v5.11.4
All checks were successful
Build & Test / Validate (pull_request) Successful in 32s
Build & Test / Run Unit Tests (pull_request) Successful in 14m43s
Build & Test / Run UI Tests (pull_request) Successful in 18m41s
2024-09-16 20:04:18 +00:00
7055229805 Update dependency gradle to v8.10.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 23s
Build & Test / Run Unit Tests (pull_request) Successful in 13m0s
Build & Test / Run UI Tests (pull_request) Successful in 18m15s
2024-09-09 09:07:21 +00:00
0dbcdb51b9 Update dependency androidx.compose.material:material-icons-core to v1.7.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 20s
Build & Test / Run Unit Tests (pull_request) Successful in 12m21s
Build & Test / Run UI Tests (pull_request) Successful in 17m10s
2024-09-05 10:01:53 +00:00
39451e67c2 Update dependency androidx.compose.material3:material3-window-size-class-android to v1.3.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 20s
Build & Test / Run Unit Tests (pull_request) Successful in 12m3s
Build & Test / Run UI Tests (pull_request) Successful in 17m39s
2024-09-05 07:02:39 +00:00
7ee7687117 Update animationCore to v1.7.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 19s
Build & Test / Run Unit Tests (pull_request) Successful in 12m28s
Build & Test / Run UI Tests (pull_request) Successful in 17m1s
2024-09-05 03:02:25 +00:00
ef0ca65ee6 Update dependency androidx.lifecycle:lifecycle-viewmodel-ktx to v2.8.5
All checks were successful
Build & Test / Validate (pull_request) Successful in 22s
Build & Test / Run Unit Tests (pull_request) Successful in 12m29s
Build & Test / Run UI Tests (pull_request) Successful in 17m48s
2024-09-05 00:02:37 +00:00
756251a64f Update dependency androidx.activity:activity-ktx to v1.9.2
All checks were successful
Build & Test / Validate (pull_request) Successful in 23s
Build & Test / Run Unit Tests (pull_request) Successful in 13m13s
Build & Test / Run UI Tests (pull_request) Successful in 19m20s
2024-09-04 22:02:24 +00:00
c7a888a413 Update dependency com.github.triplet.play to v3.11.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 13m49s
Build & Test / Run UI Tests (pull_request) Successful in 19m23s
2024-08-29 19:01:44 +00:00
97181f8190 Update dependency com.autonomousapps.dependency-analysis to v2
All checks were successful
Build & Test / Validate (pull_request) Successful in 23s
Build & Test / Run Unit Tests (pull_request) Successful in 13m42s
Build & Test / Run UI Tests (pull_request) Successful in 19m19s
2024-08-29 18:01:45 +00:00
0195e4ee58
Run tests only on pull requests and wait for unit tests before running UI tests
All checks were successful
Build & Test / Validate (pull_request) Successful in 22s
Build & Test / Run Unit Tests (pull_request) Successful in 11m8s
Build & Test / Run UI Tests (pull_request) Successful in 16m38s
2024-08-23 22:15:08 -06:00
8ac6eb24a0
Bump version for release 2024-08-23 17:33:28 -06:00
a1a8bb794e
Fix freeRelease builds 2024-08-23 17:33:09 -06:00
caf485ec61 Update kotlin to v2.0.20
All checks were successful
Build & Test / Validate (pull_request) Successful in 23s
Build & Test / Run Unit Tests (pull_request) Successful in 11m17s
Build & Test / Run UI Tests (pull_request) Successful in 16m7s
Build & Test / Validate (push) Successful in 25s
Build & Test / Run Unit Tests (push) Successful in 17m15s
Build & Test / Run UI Tests (push) Successful in 19m22s
2024-08-23 04:01:47 +00:00
50f0c299f5
Bump version for release
All checks were successful
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Validate (push) Successful in 16s
Build & Test / Run Unit Tests (pull_request) Successful in 10m13s
Build & Test / Run UI Tests (pull_request) Successful in 19m39s
Build & Test / Run Unit Tests (push) Successful in 13m8s
Build & Test / Run UI Tests (push) Successful in 19m11s
2024-08-22 21:37:04 -06:00
09d073c5f3 Bump version for release
Some checks are pending
Build & Test / Validate (push) Waiting to run
Build & Test / Run Unit Tests (push) Blocked by required conditions
Build & Test / Run UI Tests (push) Blocked by required conditions
2024-08-23 03:04:23 +00:00
51e5652475 Move changelog task to standalone file 2024-08-23 03:04:23 +00:00
01978548c6
Add LocalOnlyException
Some checks are pending
Build & Test / Validate (push) Waiting to run
Build & Test / Run Unit Tests (push) Blocked by required conditions
Build & Test / Run UI Tests (push) Blocked by required conditions
Build & Test / Validate (pull_request) Successful in 24s
Build & Test / Run Unit Tests (pull_request) Successful in 9m48s
Build & Test / Run UI Tests (pull_request) Successful in 9m48s
This is intended to enable local logging of some exceptions for debugging purposes without overrunning the remote crash reporter with issues that can't really be actioned upon
2024-08-22 21:01:53 -06:00
b42b949bdb Fix crash on markdown preview
Some checks are pending
Build & Test / Validate (push) Waiting to run
Build & Test / Run Unit Tests (push) Blocked by required conditions
Build & Test / Run UI Tests (push) Blocked by required conditions
2024-08-23 03:00:09 +00:00
040777b99a Rewrite UI tests to use robots 2024-08-23 03:00:09 +00:00
307e7642b9 Fix StrictMode disk access violations
Some checks are pending
Build & Test / Validate (push) Waiting to run
Build & Test / Run Unit Tests (push) Blocked by required conditions
Build & Test / Run UI Tests (push) Blocked by required conditions
2024-08-23 02:59:14 +00:00
617035c424
fixup! Switch to ARM emulator for FTL and bump version for tests
Some checks failed
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 9m12s
Build & Test / Run UI Tests (pull_request) Failing after 8m2s
Build & Test / Validate (push) Waiting to run
Build & Test / Run Unit Tests (push) Blocked by required conditions
Build & Test / Run UI Tests (push) Blocked by required conditions
2024-08-22 20:38:42 -06:00
9ce5e129a6
Switch to ARM emulator for FTL and bump version for tests
Some checks failed
Build & Test / Validate (pull_request) Successful in 21s
Build & Test / Run Unit Tests (pull_request) Successful in 8m59s
Build & Test / Run UI Tests (pull_request) Failing after 8m15s
2024-08-22 13:44:37 -06:00
b1e698c9c9 Update dependency androidx.compose:compose-bom to v2024.08.00
All checks were successful
Build & Test / Validate (push) Successful in 19s
Build & Test / Run Unit Tests (push) Successful in 9m31s
Build & Test / Run UI Tests (push) Successful in 15m4s
2024-08-22 09:30:58 +00:00
c53ac549dc Update dependency androidx.test:monitor to v1.7.2
Some checks are pending
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 8m23s
Build & Test / Run UI Tests (pull_request) Successful in 12m24s
Build & Test / Validate (push) Waiting to run
Build & Test / Run Unit Tests (push) Blocked by required conditions
Build & Test / Run UI Tests (push) Blocked by required conditions
2024-08-22 07:04:08 +00:00
b6203cd6d1 Update dependency androidx.test:runner to v1.6.2
All checks were successful
Build & Test / Validate (push) Successful in 22s
Build & Test / Run Unit Tests (push) Successful in 11m18s
Build & Test / Run UI Tests (push) Successful in 14m53s
2024-08-22 06:48:10 +00:00
35 changed files with 770 additions and 515 deletions

View file

@ -2,8 +2,6 @@ name: Build & Test
on: on:
pull_request: pull_request:
push:
branches: [ main ]
jobs: jobs:
validate: validate:
@ -17,7 +15,7 @@ jobs:
distribution: 'zulu' distribution: 'zulu'
java-version: '17' java-version: '17'
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: https://git.wbrawner.com/gradle/actions/wrapper-validation@v3 uses: https://git.wbrawner.com/gradle/actions/wrapper-validation@v4
unit_tests: unit_tests:
name: Run Unit Tests name: Run Unit Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -32,10 +30,10 @@ jobs:
java-version: '17' java-version: '17'
- name: Setup Android SDK - name: Setup Android SDK
uses: https://git.wbrawner.com/android-actions/setup-android@v3 uses: https://git.wbrawner.com/android-actions/setup-android@v3
- name: Setup Gradle
uses: https://git.wbrawner.com/gradle/actions/setup-gradle@v4
- name: Run unit tests - name: Run unit tests
uses: https://git.wbrawner.com/gradle/actions/setup-gradle@v3 run: ./gradlew check
with:
arguments: check
- name: Publish JUnit Results - name: Publish JUnit Results
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
if: always() if: always()
@ -47,7 +45,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Run UI Tests name: Run UI Tests
needs: needs:
- validate - unit_tests
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: set up JDK - name: set up JDK
@ -57,10 +55,10 @@ jobs:
java-version: '17' java-version: '17'
- name: Setup Android SDK - name: Setup Android SDK
uses: https://git.wbrawner.com/android-actions/setup-android@v3 uses: https://git.wbrawner.com/android-actions/setup-android@v3
- name: Build with Gradle - name: Setup Gradle
uses: https://git.wbrawner.com/gradle/gradle-build-action@v2 uses: https://git.wbrawner.com/gradle/actions/setup-gradle@v4
with: - name: Build APKs
arguments: assemblePlayDebug assemblePlayDebugAndroidTest run: ./gradlew assemblePlayDebug assemblePlayDebugAndroidTest
- name: Grant execute permission for flank_auth.sh - name: Grant execute permission for flank_auth.sh
run: chmod +x flank_auth.sh run: chmod +x flank_auth.sh
- name: Add auth for flank - name: Add auth for flank
@ -69,6 +67,4 @@ jobs:
run: | run: |
./flank_auth.sh ./flank_auth.sh
- name: Run UI tests - name: Run UI tests
uses: https://git.wbrawner.com/gradle/gradle-build-action@v2 run: ./gradlew runFlank
with:
arguments: runFlank

View file

@ -37,7 +37,7 @@ jobs:
distribution: 'zulu' distribution: 'zulu'
java-version: '17' java-version: '17'
- name: Run unit tests - name: Run unit tests
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v3
with: with:
arguments: testPlayDebugUnitTest arguments: testPlayDebugUnitTest
- name: Publish JUnit Results - name: Publish JUnit Results
@ -61,7 +61,7 @@ jobs:
distribution: 'zulu' distribution: 'zulu'
java-version: '17' java-version: '17'
- name: Build with Gradle - name: Build with Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v3
with: with:
arguments: assemblePlayDebug assemblePlayDebugAndroidTest arguments: assemblePlayDebug assemblePlayDebugAndroidTest
- name: Grant execute permission for flank_auth.sh - name: Grant execute permission for flank_auth.sh
@ -72,6 +72,6 @@ jobs:
run: | run: |
./flank_auth.sh ./flank_auth.sh
- name: Run UI tests - name: Run UI tests
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v3
with: with:
arguments: runFlank arguments: runFlank

View file

@ -21,6 +21,7 @@ try {
keystoreProperties["keyPassword"] = "" keystoreProperties["keyPassword"] = ""
keystoreProperties["storeFile"] = File.createTempFile("temp", ".tmp").absolutePath keystoreProperties["storeFile"] = File.createTempFile("temp", ".tmp").absolutePath
keystoreProperties["storePassword"] = "" keystoreProperties["storePassword"] = ""
keystoreProperties["publishCredentialsFile"] = ""
} }
android { android {
@ -37,18 +38,18 @@ android {
} }
compileSdk = libs.versions.maxSdk.get().toInt() compileSdk = libs.versions.maxSdk.get().toInt()
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "11"
} }
defaultConfig { defaultConfig {
applicationId = "com.wbrawner.simplemarkdown" applicationId = "com.wbrawner.simplemarkdown"
minSdk = libs.versions.minSdk.get().toInt() minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.maxSdk.get().toInt() targetSdk = libs.versions.maxSdk.get().toInt()
versionCode = 41 versionCode = 45
versionName = "0.8.16" versionName = "2024.10.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true" testInstrumentationRunnerArguments["clearPackageData"] = "true"
buildConfigField("boolean", "ENABLE_CUSTOM_CSS", "true") buildConfigField("boolean", "ENABLE_CUSTOM_CSS", "true")
@ -98,7 +99,11 @@ android {
} }
} }
lint { lint {
disable += listOf("AndroidGradlePluginVersion", "GradleDependency") disable += listOf(
"AndroidGradlePluginVersion",
"GradleDependency",
"ObsoleteLintCustomCheck"
)
warningsAsErrors = true warningsAsErrors = true
} }
} }
@ -108,6 +113,9 @@ play {
enabled.set(false) enabled.set(false)
track.set("production") track.set("production")
defaultToAppBundles.set(true) defaultToAppBundles.set(true)
(keystoreProperties["publishCredentialsFile"] as? String)?.ifBlank { null }?.let {
serviceAccountCredentials.set(file(it))
}
} }
dependencies { dependencies {
@ -154,7 +162,6 @@ dependencies {
androidTestImplementation(libs.androidx.core) androidTestImplementation(libs.androidx.core)
androidTestImplementation(libs.androidx.monitor) androidTestImplementation(libs.androidx.monitor)
androidTestImplementation(libs.junit) androidTestImplementation(libs.junit)
androidTestImplementation(libs.hamcrest.core)
implementation(libs.androidx.activity.ktx) implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.activity.ktx) implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.animation.core) implementation(libs.androidx.animation.core)
@ -181,7 +188,7 @@ fladle {
environmentVariables.put("clearPackageData", "true") environmentVariables.put("clearPackageData", "true")
testTimeout.set("7m") testTimeout.set("7m")
devices.add( devices.add(
mapOf("model" to "NexusLowRes", "version" to "29") mapOf("model" to "Pixel2.arm", "version" to "33")
) )
projectId.set("simplemarkdown") projectId.set("simplemarkdown")
} }

View file

@ -0,0 +1,27 @@
package com.wbrawner.simplemarkdown
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.test.core.app.ActivityScenario
import com.wbrawner.simplemarkdown.robot.onMainScreen
import org.junit.Rule
import org.junit.Test
class HelpTest {
@get:Rule
val composeRule = createEmptyComposeRule()
@Test
fun openHelpPageTest() {
ActivityScenario.launch(MainActivity::class.java)
onMainScreen(composeRule) {
checkTitleEquals("Untitled.md")
checkMarkdownEquals("")
openDrawer()
} onNavigationDrawer {
openHelpPage()
} onHelpScreen {
checkTitleEquals("Help")
verifyH1("Headings/Titles")
}
}
}

View file

@ -5,43 +5,17 @@ import android.app.Instrumentation
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.webkit.WebView
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.hasAnyDescendant
import androidx.compose.ui.test.hasAnySibling
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextReplacement
import androidx.compose.ui.test.printToLog
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.intent.Intents.intending import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.rule.IntentsRule import androidx.test.espresso.intent.rule.IntentsRule
import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
import androidx.test.espresso.web.webdriver.DriverAtoms.getText
import androidx.test.espresso.web.webdriver.Locator
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.wbrawner.simplemarkdown.robot.onMainScreen
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.hamcrest.CoreMatchers.containsString
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
@ -79,13 +53,13 @@ class MarkdownTests {
@Test @Test
fun editAndPreviewMarkdownTest() { fun editAndPreviewMarkdownTest() {
ActivityScenario.launch(MainActivity::class.java) ActivityScenario.launch(MainActivity::class.java)
composeRule.typeMarkdown("# Header test") onMainScreen(composeRule) {
composeRule.checkMarkdownEquals("# Header test") typeMarkdown("# Header test")
composeRule.openPreview() checkMarkdownEquals("# Header test")
onWebView(isAssignableFrom(WebView::class.java)) openPreview()
.forceJavascriptEnabled() } onPreview {
.withElement(findElement(Locator.TAG_NAME, "h1")) verifyH1("Header test")
.check(webMatches(getText(), containsString("Header test"))) }
} }
@Test @Test
@ -98,104 +72,138 @@ class MarkdownTests {
}) })
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult) intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
ActivityScenario.launch(MainActivity::class.java) ActivityScenario.launch(MainActivity::class.java)
composeRule.openMenu() onMainScreen(composeRule) {
composeRule.clickOpenMenuItem() openMenu()
composeRule.checkMarkdownEquals(markdownText) clickOpenMenuItem()
composeRule.openMenu() checkMarkdownEquals(markdownText)
composeRule.clickNewMenuItem() openMenu()
composeRule.verifyDialogIsNotShown() clickNewMenuItem()
composeRule.checkMarkdownEquals("") verifyDialogIsNotShown()
checkMarkdownEquals("")
}
} }
@Test @Test
fun editThenNewMarkdownTest() { fun editThenNewMarkdownTest() {
ActivityScenario.launch(MainActivity::class.java) ActivityScenario.launch(MainActivity::class.java)
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog." val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
composeRule.typeMarkdown(markdownText) onMainScreen(composeRule) {
composeRule.openMenu() typeMarkdown(markdownText)
composeRule.clickNewMenuItem() openMenu()
composeRule.onNode(isDialog()).printToLog("TestDebugging") clickNewMenuItem()
composeRule.verifyDialogIsShown("Would you like to save your changes?") verifyDialogIsShown("Would you like to save your changes?")
composeRule.discardChanges() discardChanges()
composeRule.checkMarkdownEquals("") checkMarkdownEquals("")
}
} }
@Test @Test
fun saveMarkdownWithFileUriTest() = runTest { fun saveMarkdownWithFileUriTest() = runTest {
ActivityScenario.launch(MainActivity::class.java) ActivityScenario.launch(MainActivity::class.java)
composeRule.checkTitleEquals("Untitled.md") onMainScreen(composeRule) {
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog." checkTitleEquals("Untitled.md")
composeRule.typeMarkdown(markdownText) val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply { typeMarkdown(markdownText)
data = Uri.fromFile(file) val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
}) data = Uri.fromFile(file)
intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult) })
composeRule.openMenu() intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult)
composeRule.clickSaveMenuItem() openMenu()
composeRule.awaitIdle() clickSaveMenuItem()
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText)) awaitIdle()
composeRule.checkTitleEquals("temp.md") assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
checkTitleEquals("temp.md")
}
} }
@Test @Test
fun saveMarkdownWithContentUriTest() = runTest { fun saveMarkdownWithContentUriTest() = runTest {
ActivityScenario.launch(MainActivity::class.java) ActivityScenario.launch(MainActivity::class.java)
composeRule.checkTitleEquals("Untitled.md") onMainScreen(composeRule) {
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog." checkTitleEquals("Untitled.md")
composeRule.typeMarkdown(markdownText) val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply { typeMarkdown(markdownText)
data = FileProvider.getUriForFile( val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
getApplicationContext(), data = FileProvider.getUriForFile(
"${BuildConfig.APPLICATION_ID}.fileprovider", getApplicationContext(),
file "${BuildConfig.APPLICATION_ID}.fileprovider",
) file
}) )
intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult) })
composeRule.openMenu() intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult)
composeRule.clickSaveMenuItem() openMenu()
composeRule.awaitIdle() clickSaveMenuItem()
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText)) awaitIdle()
composeRule.checkTitleEquals("temp.md") assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
checkTitleEquals("temp.md")
}
} }
@Test @Test
fun loadMarkdownWithFileUriTest() = runTest { fun loadMarkdownWithFileUriTest() = runTest {
ActivityScenario.launch(MainActivity::class.java) ActivityScenario.launch(MainActivity::class.java)
composeRule.checkTitleEquals("Untitled.md") onMainScreen(composeRule) {
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog." checkTitleEquals("Untitled.md")
file.outputStream().writer().use { it.write(markdownText) } val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply { file.outputStream().writer().use { it.write(markdownText) }
data = Uri.fromFile(file) val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
}) data = Uri.fromFile(file)
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult) })
composeRule.openMenu() intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
composeRule.clickOpenMenuItem() openMenu()
composeRule.awaitIdle() clickOpenMenuItem()
composeRule.checkMarkdownEquals(markdownText) awaitIdle()
composeRule.checkTitleEquals("temp.md") checkMarkdownEquals(markdownText)
checkTitleEquals("temp.md")
}
} }
@Test @Test
fun loadMarkdownWithContentUriTest() = runTest { fun loadMarkdownWithContentUriTest() = runTest {
ActivityScenario.launch(MainActivity::class.java) ActivityScenario.launch(MainActivity::class.java)
composeRule.checkTitleEquals("Untitled.md") onMainScreen(composeRule) {
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog." checkTitleEquals("Untitled.md")
file.outputStream().writer().use { it.write(markdownText) } val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply { file.outputStream().writer().use { it.write(markdownText) }
data = FileProvider.getUriForFile( val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
getApplicationContext(), data = FileProvider.getUriForFile(
"${BuildConfig.APPLICATION_ID}.fileprovider", getApplicationContext(),
file "${BuildConfig.APPLICATION_ID}.fileprovider",
) file
}) )
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult) })
composeRule.openMenu() intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
composeRule.clickOpenMenuItem() openMenu()
composeRule.awaitIdle() clickOpenMenuItem()
composeRule.checkMarkdownEquals(markdownText) awaitIdle()
composeRule.checkTitleEquals("temp.md") checkMarkdownEquals(markdownText)
checkTitleEquals("temp.md")
}
} }
@Test
fun launchWithContentUriTest() = runTest {
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
file.outputStream().writer().use { it.write(markdownText) }
val fileUri = FileProvider.getUriForFile(
getApplicationContext(),
"${BuildConfig.APPLICATION_ID}.fileprovider",
file
)
ActivityScenario.launch<MainActivity>(
Intent(
Intent.ACTION_VIEW,
fileUri,
getInstrumentation().targetContext,
MainActivity::class.java
)
)
onMainScreen(composeRule) {
awaitIdle()
checkMarkdownEquals(markdownText)
checkTitleEquals("temp.md")
}
}
@Test @Test
fun openEditAndSaveMarkdownTest() = runTest { fun openEditAndSaveMarkdownTest() = runTest {
@ -206,84 +214,41 @@ class MarkdownTests {
}) })
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult) intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
ActivityScenario.launch(MainActivity::class.java) ActivityScenario.launch(MainActivity::class.java)
composeRule.checkTitleEquals("Untitled.md") onMainScreen(composeRule) {
composeRule.openMenu() checkTitleEquals("Untitled.md")
composeRule.clickOpenMenuItem() openMenu()
composeRule.awaitIdle() clickOpenMenuItem()
composeRule.verifyTextIsShown("Successfully loaded temp.md") awaitIdle()
composeRule.checkMarkdownEquals(markdownText) verifyTextIsShown("Successfully loaded temp.md")
composeRule.checkTitleEquals("temp.md") checkMarkdownEquals(markdownText)
val additionalText = "# More info\n\nThis is some additional text" checkTitleEquals("temp.md")
composeRule.typeMarkdown(additionalText) val additionalText = "# More info\n\nThis is some additional text"
composeRule.openMenu() typeMarkdown(additionalText)
composeRule.clickSaveMenuItem() openMenu()
composeRule.awaitIdle() clickSaveMenuItem()
composeRule.verifyTextIsShown("Successfully saved temp.md") awaitIdle()
assertEquals(additionalText, file.inputStream().reader().use(Reader::readText)) verifyTextIsShown("Successfully saved temp.md")
composeRule.checkTitleEquals("temp.md") assertEquals(additionalText, file.inputStream().reader().use(Reader::readText))
} checkTitleEquals("temp.md")
private fun ComposeTestRule.checkTitleEquals(title: String) =
onNode(hasAnySibling(hasContentDescription("Main Menu")).and(hasText(title)))
.waitUntilIsDisplayed()
private fun ComposeTestRule.typeMarkdown(markdown: String) =
onNode(hasSetTextAction()).performTextReplacement(markdown)
private fun ComposeTestRule.checkMarkdownEquals(markdown: String) {
val markdownMatcher = SemanticsMatcher("Markdown = [$markdown]") {
it.config.getOrNull(SemanticsProperties.EditableText)?.text == markdown
}
onNode(hasSetTextAction()).waitUntil {
assert(markdownMatcher)
} }
} }
private fun ComposeTestRule.openPreview() = onNodeWithText("Preview").performClick() @Test
fun editAndViewHelpMarkdownTest() = runTest {
private fun ComposeTestRule.openMenu() = ActivityScenario.launch(MainActivity::class.java)
onNodeWithContentDescription("Editor Actions").performClick() onMainScreen(composeRule) {
checkTitleEquals("Untitled.md")
private fun ComposeTestRule.clickOpenMenuItem() = onNodeWithText("Open").performClick() typeMarkdown("# Header test")
checkMarkdownEquals("# Header test")
private fun ComposeTestRule.clickNewMenuItem() = onNodeWithText("New").performClick() openDrawer()
} onNavigationDrawer {
private fun ComposeTestRule.clickSaveMenuItem() = onNodeWithText("Save").performClick() openHelpPage()
} onHelpScreen {
private fun ComposeTestRule.verifyDialogIsShown(text: String) = checkTitleEquals("Help")
onNode(isDialog().and(hasAnyDescendant(hasText(text)))).waitUntilIsDisplayed() verifyH1("Headings/Titles")
pressBack()
private fun ComposeTestRule.verifyDialogIsNotShown() = } onMainScreen {
onNode(isDialog()).waitUntilIsNotDisplayed() checkMarkdownEquals("# Header test")
private fun ComposeTestRule.discardChanges() = onNodeWithText("No").performClick()
private fun ComposeTestRule.verifyTextIsShown(text: String) =
onNodeWithText(text).waitUntilIsDisplayed()
private val ASSERTION_TIMEOUT = 5_000L
private fun SemanticsNodeInteraction.waitUntil(assertion: SemanticsNodeInteraction.() -> Unit) {
val start = System.currentTimeMillis()
lateinit var assertionError: AssertionError
while (System.currentTimeMillis() - start < ASSERTION_TIMEOUT) {
try {
assertion()
return
} catch (e: AssertionError) {
assertionError = e
Thread.sleep(10)
}
} }
throw assertionError
}
private fun SemanticsNodeInteraction.waitUntilIsDisplayed() = waitUntil {
assertIsDisplayed()
}
private fun SemanticsNodeInteraction.waitUntilIsNotDisplayed() = waitUntil {
assertIsNotDisplayed()
} }
} }

View file

@ -0,0 +1,29 @@
package com.wbrawner.simplemarkdown
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
private const val ASSERTION_TIMEOUT = 5_000L
fun SemanticsNodeInteraction.waitUntilIsDisplayed() = waitUntil {
assertIsDisplayed()
}
fun SemanticsNodeInteraction.waitUntilIsNotDisplayed() = waitUntil {
assertIsNotDisplayed()
}
fun <T> SemanticsNodeInteraction.waitUntil(assertion: SemanticsNodeInteraction.() -> T): T {
val start = System.currentTimeMillis()
lateinit var assertionError: AssertionError
while (System.currentTimeMillis() - start < ASSERTION_TIMEOUT) {
try {
return assertion()
} catch (e: AssertionError) {
assertionError = e
Thread.sleep(10)
}
}
throw assertionError
}

View file

@ -0,0 +1,80 @@
package com.wbrawner.simplemarkdown.robot
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.hasAnyDescendant
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextReplacement
import com.wbrawner.simplemarkdown.waitUntil
import com.wbrawner.simplemarkdown.waitUntilIsDisplayed
import com.wbrawner.simplemarkdown.waitUntilIsNotDisplayed
import kotlinx.coroutines.CoroutineScope
fun onMainScreen(composeRule: ComposeTestRule, block: MainScreenRobot.() -> Unit) =
MainScreenRobot(composeRule).apply(block)
@Suppress("UnusedReceiverParameter") // Used to avoid import ambiguity for tests
suspend fun CoroutineScope.onMainScreen(
composeRule: ComposeTestRule,
block: suspend MainScreenRobot.() -> Unit
): MainScreenRobot {
val mainScreenRobot = MainScreenRobot(composeRule)
block.invoke(mainScreenRobot)
return mainScreenRobot
}
class MainScreenRobot(private val composeRule: ComposeTestRule) :
TopAppBarRobot by ComposeTopAppBarRobot(composeRule) {
fun typeMarkdown(markdown: String) = composeRule.onNode(hasSetTextAction())
.performTextReplacement(markdown)
fun checkMarkdownEquals(markdown: String) {
val markdownMatcher = SemanticsMatcher("Markdown = [$markdown]") {
it.config.getOrNull(SemanticsProperties.EditableText)?.text == markdown
}
composeRule.onNode(hasSetTextAction()).waitUntil {
assert(markdownMatcher)
}
}
fun openPreview() = composeRule.onNodeWithText("Preview").performClick()
fun openMenu() = composeRule.onNodeWithContentDescription("Editor Actions").performClick()
fun clickOpenMenuItem() = composeRule.onNodeWithText("Open").performClick()
fun clickNewMenuItem() = composeRule.onNodeWithText("New").performClick()
fun clickSaveMenuItem() = composeRule.onNodeWithText("Save").performClick()
fun verifyDialogIsShown(text: String) =
composeRule.onNode(isDialog().and(hasAnyDescendant(hasText(text)))).waitUntilIsDisplayed()
fun verifyDialogIsNotShown() = composeRule.onNode(isDialog()).waitUntilIsNotDisplayed()
fun discardChanges() = composeRule.onNodeWithText("No").performClick()
fun verifyTextIsShown(text: String) = composeRule.onNodeWithText(text).waitUntilIsDisplayed()
fun openDrawer() = composeRule.onNode(hasClickAction() and hasContentDescription("Main Menu"))
.waitUntilIsDisplayed()
.performClick()
suspend fun awaitIdle() = composeRule.awaitIdle()
infix fun onPreview(block: WebViewRobot.() -> Unit) = EspressoWebViewRobot().apply(block)
infix fun onNavigationDrawer(block: NavigationDrawerRobot.() -> Unit): NavigationDrawerRobot =
NavigationDrawerRobot(composeRule).apply(block = block)
}

View file

@ -0,0 +1,14 @@
package com.wbrawner.simplemarkdown.robot
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.performClick
class MarkdownInfoScreenRobot(private val composeTestRule: ComposeTestRule) :
TopAppBarRobot by ComposeTopAppBarRobot(composeTestRule),
WebViewRobot by EspressoWebViewRobot() {
fun pressBack() = composeTestRule.onNodeWithContentDescription("Back").performClick()
infix fun onMainScreen(block: MainScreenRobot.() -> Unit) =
MainScreenRobot(composeTestRule).apply(block)
}

View file

@ -0,0 +1,16 @@
package com.wbrawner.simplemarkdown.robot
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.performClick
import com.wbrawner.simplemarkdown.waitUntilIsDisplayed
class NavigationDrawerRobot(private val composeTestRule: ComposeTestRule) {
fun openHelpPage() = composeTestRule.onNode(hasClickAction() and hasText("Help"))
.waitUntilIsDisplayed()
.performClick()
infix fun onHelpScreen(block: MarkdownInfoScreenRobot.() -> Unit) =
MarkdownInfoScreenRobot(composeTestRule).apply(block)
}

View file

@ -0,0 +1,24 @@
package com.wbrawner.simplemarkdown.robot
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.hasAnySibling
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeTestRule
import com.wbrawner.simplemarkdown.waitUntilIsDisplayed
interface TopAppBarRobot {
fun checkTitleEquals(title: String): SemanticsNodeInteraction
}
class ComposeTopAppBarRobot(private val composeTestRule: ComposeTestRule) : TopAppBarRobot {
override fun checkTitleEquals(title: String) =
composeTestRule.onNode(
hasAnySibling(
hasContentDescription("Main Menu") or hasContentDescription(
"Back"
)
).and(hasText(title))
)
.waitUntilIsDisplayed()
}

View file

@ -0,0 +1,24 @@
package com.wbrawner.simplemarkdown.robot
import android.webkit.WebView
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
import androidx.test.espresso.web.webdriver.DriverAtoms.getText
import androidx.test.espresso.web.webdriver.Locator
import org.hamcrest.CoreMatchers.containsString
interface WebViewRobot {
fun verifyH1(text: String)
}
class EspressoWebViewRobot : WebViewRobot {
private fun findWebView() = onWebView(isAssignableFrom(WebView::class.java))
.forceJavascriptEnabled()
override fun verifyH1(text: String) {
findWebView().withElement(findElement(Locator.TAG_NAME, "h1"))
.check(webMatches(getText(), containsString(text)))
}
}

View file

@ -7,6 +7,7 @@
<application <application
android:name=".MarkdownApplication" android:name=".MarkdownApplication"
android:allowBackup="true" android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:resizeableActivity="true" android:resizeableActivity="true"
@ -14,7 +15,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.App.Starting" android:theme="@style/Theme.App.Starting"
tools:ignore="AllowBackup" tools:ignore="AllowBackup"
tools:targetApi="n"> tools:targetApi="tiramisu">
<activity android:name=".MainActivity" <activity android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name_short"> android:label="@string/app_name_short">

View file

@ -1,5 +1,7 @@
package com.wbrawner.simplemarkdown package com.wbrawner.simplemarkdown
import android.app.ComponentCaller
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -8,12 +10,11 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.core.EaseIn import androidx.compose.animation.core.EaseIn
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleOut
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Help import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
@ -27,11 +28,13 @@ import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
@ -43,6 +46,7 @@ import com.wbrawner.simplemarkdown.ui.SettingsScreen
import com.wbrawner.simplemarkdown.ui.SupportScreen import com.wbrawner.simplemarkdown.ui.SupportScreen
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
import com.wbrawner.simplemarkdown.utility.Preference import com.wbrawner.simplemarkdown.utility.Preference
import kotlinx.coroutines.launch
import org.acra.ACRA import org.acra.ACRA
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback { class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback {
@ -61,8 +65,6 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
setContent { setContent {
val autosaveEnabled by preferenceHelper.observe<Boolean>(Preference.AUTOSAVE_ENABLED) val autosaveEnabled by preferenceHelper.observe<Boolean>(Preference.AUTOSAVE_ENABLED)
.collectAsState() .collectAsState()
val readabilityEnabled by preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED)
.collectAsState()
val darkModePreference by preferenceHelper.observe<String>(Preference.DARK_MODE) val darkModePreference by preferenceHelper.observe<String>(Preference.DARK_MODE)
.collectAsState() .collectAsState()
LaunchedEffect(darkModePreference) { LaunchedEffect(darkModePreference) {
@ -92,6 +94,10 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
LaunchedEffect(errorReporterPreference) { LaunchedEffect(errorReporterPreference) {
ACRA.errorReporter.setEnabled(errorReporterPreference) ACRA.errorReporter.setEnabled(errorReporterPreference)
} }
val intentData = remember(intent) { intent?.data }
LaunchedEffect(intentData) {
viewModel.load(intentData?.toString())
}
val windowSizeClass = calculateWindowSizeClass(this) val windowSizeClass = calculateWindowSizeClass(this)
SimpleMarkdownTheme { SimpleMarkdownTheme {
val navController = rememberNavController() val navController = rememberNavController()
@ -108,13 +114,9 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
towards = AnimatedContentTransitionScope.SlideDirection.Start towards = AnimatedContentTransitionScope.SlideDirection.Start
) )
}, },
popEnterTransition = { EnterTransition.None }, popEnterTransition = { fadeIn() },
popExitTransition = { popExitTransition = {
fadeOut( scaleOut(targetScale = 0.9f) + slideOutOfContainer(
animationSpec = tween(
300, easing = LinearEasing
)
) + slideOutOfContainer(
animationSpec = tween(300, easing = EaseIn), animationSpec = tween(300, easing = EaseIn),
towards = AnimatedContentTransitionScope.SlideDirection.End towards = AnimatedContentTransitionScope.SlideDirection.End
) )
@ -126,7 +128,6 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
viewModel = viewModel, viewModel = viewModel,
enableWideLayout = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded, enableWideLayout = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded,
enableAutosave = autosaveEnabled, enableAutosave = autosaveEnabled,
enableReadability = readabilityEnabled
) )
} }
composable(Route.SETTINGS.path) { composable(Route.SETTINGS.path) {
@ -160,6 +161,15 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
} }
} }
} }
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
super.onNewIntent(intent, caller)
lifecycleScope.launch {
intent.data?.let {
viewModel.load(it.toString())
}
}
}
} }
enum class Route( enum class Route(

View file

@ -38,7 +38,9 @@ class MarkdownApplication : Application() {
} }
} }
} }
Timber.plant(ErrorReporterTree.create(this)) coroutineScope.launch {
Timber.plant(ErrorReporterTree.create(this@MarkdownApplication))
}
super.onCreate() super.onCreate()
ReviewHelper.init(this) ReviewHelper.init(this)
fileHelper = AndroidFileHelper(this) fileHelper = AndroidFileHelper(this)

View file

@ -1,10 +1,16 @@
package com.wbrawner.simplemarkdown package com.wbrawner.simplemarkdown
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import com.wbrawner.simplemarkdown.core.LocalOnlyException
import com.wbrawner.simplemarkdown.model.Readability
import com.wbrawner.simplemarkdown.utility.FileHelper import com.wbrawner.simplemarkdown.utility.FileHelper
import com.wbrawner.simplemarkdown.utility.Preference import com.wbrawner.simplemarkdown.utility.Preference
import com.wbrawner.simplemarkdown.utility.PreferenceHelper import com.wbrawner.simplemarkdown.utility.PreferenceHelper
@ -13,7 +19,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -23,21 +28,17 @@ import java.net.URI
data class EditorState( data class EditorState(
val fileName: String = "Untitled.md", val fileName: String = "Untitled.md",
val markdown: String = "", val markdown: TextFieldValue = TextFieldValue(""),
val path: URI? = null, val path: URI? = null,
val toast: ParameterizedText? = null, val toast: ParameterizedText? = null,
val alert: AlertDialogModel? = null, val alert: AlertDialogModel? = null,
val saveCallback: (() -> Unit)? = null, val saveCallback: (() -> Unit)? = null,
/**
* Used to signal to the view that it should reload due to an external change, like loading
* a new file
*/
val reloadToggle: Int = 0,
val lockSwiping: Boolean = false, val lockSwiping: Boolean = false,
private val initialMarkdown: String = "", val enableReadability: Boolean = false,
val initialMarkdown: String = "",
) { ) {
val dirty: Boolean val dirty: Boolean
get() = markdown != initialMarkdown get() = markdown.text != initialMarkdown
} }
class MarkdownViewModel( class MarkdownViewModel(
@ -49,32 +50,43 @@ class MarkdownViewModel(
private val saveMutex = Mutex() private val saveMutex = Mutex()
init { init {
viewModelScope.launch {
load(null)
}
preferenceHelper.observe<Boolean>(Preference.LOCK_SWIPING) preferenceHelper.observe<Boolean>(Preference.LOCK_SWIPING)
.onEach { .onEach {
_state.value = _state.value.copy(lockSwiping = it) updateState { copy(lockSwiping = it) }
}
.launchIn(viewModelScope)
preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED)
.onEach {
updateState {
copy(
enableReadability = it,
markdown = markdown.copy(annotatedString = markdown.text.annotate(it)),
)
}
} }
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
fun updateMarkdown(markdown: String?) { fun updateMarkdown(markdown: String?) = updateMarkdown(TextFieldValue(markdown.orEmpty()))
_state.value = _state.value.copy(
markdown = markdown ?: "", fun updateMarkdown(markdown: TextFieldValue) {
) updateState {
copy(
markdown = markdown.copy(annotatedString = markdown.text.annotate(enableReadability)),
)
}
} }
fun dismissToast() { fun dismissToast() {
_state.value = _state.value.copy(toast = null) updateState { copy(toast = null) }
} }
fun dismissAlert() { fun dismissAlert() {
_state.value = _state.value.copy(alert = null) updateState { copy(alert = null) }
} }
private fun unsetSaveCallback() { private fun unsetSaveCallback() {
_state.value = _state.value.copy(saveCallback = null) updateState { copy(saveCallback = null) }
} }
suspend fun load(loadPath: String?) { suspend fun load(loadPath: String?) {
@ -97,25 +109,30 @@ class MarkdownViewModel(
val uri = URI.create(actualLoadPath) val uri = URI.create(actualLoadPath)
fileHelper.open(uri) fileHelper.open(uri)
?.let { (name, content) -> ?.let { (name, content) ->
val currentState = _state.value updateState {
_state.value = currentState.copy( copy(
path = uri, path = uri,
fileName = name, fileName = name,
markdown = content, markdown = TextFieldValue(content),
initialMarkdown = content, initialMarkdown = content,
reloadToggle = currentState.reloadToggle.inv(), toast = ParameterizedText(R.string.file_loaded, arrayOf(name))
toast = ParameterizedText(R.string.file_loaded, arrayOf(name)) )
) }
preferenceHelper[Preference.AUTOSAVE_URI] = actualLoadPath preferenceHelper[Preference.AUTOSAVE_URI] = actualLoadPath
} ?: throw IllegalStateException("Opened file was null") } ?: throw IllegalStateException("Opened file was null")
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to open file at path: $actualLoadPath") Timber.e(LocalOnlyException(e), "Failed to open file at path: $actualLoadPath")
_state.value = _state.value.copy( updateState {
alert = AlertDialogModel( copy(
text = ParameterizedText(R.string.file_load_error), alert = AlertDialogModel(
confirmButton = AlertDialogModel.ButtonModel(ParameterizedText(R.string.ok), onClick = ::dismissAlert) text = ParameterizedText(R.string.file_load_error),
confirmButton = AlertDialogModel.ButtonModel(
ParameterizedText(R.string.ok),
onClick = ::dismissAlert
)
)
) )
) }
} }
} }
} }
@ -127,35 +144,44 @@ class MarkdownViewModel(
?: run { ?: run {
Timber.w("Attempted to save file with empty path") Timber.w("Attempted to save file with empty path")
if (interactive) { if (interactive) {
_state.value = _state.value.copy(saveCallback = ::unsetSaveCallback) updateState {
copy(saveCallback = ::unsetSaveCallback)
}
} }
return@withLock false return@withLock false
} }
try { try {
Timber.i("Saving file to $actualSavePath...") Timber.i("Saving file to $actualSavePath...")
val currentState = _state.value val currentState = _state.value
val name = fileHelper.save(actualSavePath, currentState.markdown) val name = fileHelper.save(actualSavePath, currentState.markdown.text)
_state.value = currentState.copy( updateState {
fileName = name, currentState.copy(
path = actualSavePath, fileName = name,
initialMarkdown = currentState.markdown, path = actualSavePath,
toast = if (interactive) ParameterizedText(R.string.file_saved, arrayOf(name)) else null initialMarkdown = currentState.markdown.text,
) toast = if (interactive) ParameterizedText(
R.string.file_saved,
arrayOf(name)
) else null
)
}
Timber.i("Saved file $name to uri $actualSavePath") Timber.i("Saved file $name to uri $actualSavePath")
Timber.i("Persisting autosave uri in shared prefs: $actualSavePath") Timber.i("Persisting autosave uri in shared prefs: $actualSavePath")
preferenceHelper[Preference.AUTOSAVE_URI] = actualSavePath preferenceHelper[Preference.AUTOSAVE_URI] = actualSavePath
true true
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to save file to $actualSavePath") Timber.e(e, "Failed to save file to $actualSavePath")
_state.value = _state.value.copy( updateState {
alert = AlertDialogModel( copy(
text = ParameterizedText(R.string.file_save_error), alert = AlertDialogModel(
confirmButton = AlertDialogModel.ButtonModel( text = ParameterizedText(R.string.file_save_error),
text = ParameterizedText(R.string.ok), confirmButton = AlertDialogModel.ButtonModel(
onClick = ::dismissAlert text = ParameterizedText(R.string.ok),
onClick = ::dismissAlert
)
) )
) )
) }
false false
} }
} }
@ -187,7 +213,7 @@ class MarkdownViewModel(
// to an internal storage location, thus marking it as not dirty, but no longer able to // to an internal storage location, thus marking it as not dirty, but no longer able to
// access the file if the accidentally go to create a new file without properly saving // access the file if the accidentally go to create a new file without properly saving
// the current one // the current one
fileHelper.save(file, _state.value.markdown) fileHelper.save(file, _state.value.markdown.text)
preferenceHelper[Preference.AUTOSAVE_URI] = file preferenceHelper[Preference.AUTOSAVE_URI] = file
} }
} }
@ -196,33 +222,35 @@ class MarkdownViewModel(
fun reset(untitledFileName: String, force: Boolean = false) { fun reset(untitledFileName: String, force: Boolean = false) {
Timber.i("Resetting view model to default state") Timber.i("Resetting view model to default state")
if (!force && _state.value.dirty) { if (!force && _state.value.dirty) {
_state.value = _state.value.copy(alert = AlertDialogModel( updateState {
text = ParameterizedText(R.string.prompt_save_changes), copy(alert = AlertDialogModel(
confirmButton = AlertDialogModel.ButtonModel( text = ParameterizedText(R.string.prompt_save_changes),
text = ParameterizedText(R.string.yes), confirmButton = AlertDialogModel.ButtonModel(
onClick = { text = ParameterizedText(R.string.yes),
_state.value = _state.value.copy( onClick = {
saveCallback = { _state.value = _state.value.copy(
reset(untitledFileName, false) saveCallback = {
} reset(untitledFileName, false)
) }
} )
), }
dismissButton = AlertDialogModel.ButtonModel( ),
text = ParameterizedText(R.string.no), dismissButton = AlertDialogModel.ButtonModel(
onClick = { text = ParameterizedText(R.string.no),
reset(untitledFileName, true) onClick = {
} reset(untitledFileName, true)
) }
)) )
))
}
return return
} }
_state.value = updateState {
EditorState( EditorState(
fileName = untitledFileName, fileName = untitledFileName,
reloadToggle = _state.value.reloadToggle.inv(),
lockSwiping = preferenceHelper[Preference.LOCK_SWIPING] as Boolean lockSwiping = preferenceHelper[Preference.LOCK_SWIPING] as Boolean
) )
}
Timber.i("Removing autosave uri from shared prefs") Timber.i("Removing autosave uri from shared prefs")
preferenceHelper[Preference.AUTOSAVE_URI] = null preferenceHelper[Preference.AUTOSAVE_URI] = null
} }
@ -231,6 +259,10 @@ class MarkdownViewModel(
preferenceHelper[Preference.LOCK_SWIPING] = enabled preferenceHelper[Preference.LOCK_SWIPING] = enabled
} }
private fun updateState(block: EditorState.() -> EditorState) {
_state.value = _state.value.block()
}
companion object { companion object {
fun factory( fun factory(
fileHelper: FileHelper, fileHelper: FileHelper,
@ -273,4 +305,17 @@ data class ParameterizedText(@StringRes val text: Int, val params: Array<Any> =
result = 31 * result + params.contentHashCode() result = 31 * result + params.contentHashCode()
return result return result
} }
}
private fun String.annotate(enableReadability: Boolean): AnnotatedString {
if (!enableReadability) return AnnotatedString(this)
val readability = Readability(this)
val annotated = AnnotatedString.Builder(this)
for (sentence in readability.sentences()) {
var color = Color.Transparent
if (sentence.syllableCount() > 25) color = Color(229, 232, 42, 100)
if (sentence.syllableCount() > 35) color = Color(193, 66, 66, 100)
annotated.addStyle(SpanStyle(background = color), sentence.start(), sentence.end())
}
return annotated.toAnnotatedString()
} }

View file

@ -4,7 +4,6 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -53,6 +52,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity import androidx.core.content.ContextCompat.startActivity
import androidx.navigation.NavController import androidx.navigation.NavController
@ -76,13 +76,10 @@ fun MainScreen(
viewModel: MarkdownViewModel, viewModel: MarkdownViewModel,
enableWideLayout: Boolean, enableWideLayout: Boolean,
enableAutosave: Boolean, enableAutosave: Boolean,
enableReadability: Boolean
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val fileName by viewModel.collectAsState(EditorState::fileName, "") val fileName by viewModel.collectAsState(EditorState::fileName, "")
val initialMarkdown by viewModel.collectAsState(EditorState::markdown, "") val markdown by viewModel.collectAsState(EditorState::markdown, TextFieldValue(""))
val reloadToggle by viewModel.collectAsState(EditorState::reloadToggle, 0)
val markdown by viewModel.collectAsState(EditorState::markdown, "")
val dirty by viewModel.collectAsState(EditorState::dirty, false) val dirty by viewModel.collectAsState(EditorState::dirty, false)
val alert by viewModel.collectAsState(EditorState::alert, null) val alert by viewModel.collectAsState(EditorState::alert, null)
val saveCallback by viewModel.collectAsState(EditorState::saveCallback, null) val saveCallback by viewModel.collectAsState(EditorState::saveCallback, null)
@ -98,8 +95,6 @@ fun MainScreen(
MainScreen( MainScreen(
dirty = dirty, dirty = dirty,
fileName = fileName, fileName = fileName,
reloadToggle = reloadToggle,
initialMarkdown = initialMarkdown,
markdown = markdown, markdown = markdown,
setMarkdown = viewModel::updateMarkdown, setMarkdown = viewModel::updateMarkdown,
lockSwiping = lockSwiping, lockSwiping = lockSwiping,
@ -127,7 +122,6 @@ fun MainScreen(
viewModel.reset("Untitled.md") viewModel.reset("Untitled.md")
}, },
enableWideLayout = enableWideLayout, enableWideLayout = enableWideLayout,
enableReadability = enableReadability,
) )
} }
@ -136,10 +130,8 @@ fun MainScreen(
private fun MainScreen( private fun MainScreen(
fileName: String = "Untitled.md", fileName: String = "Untitled.md",
dirty: Boolean = false, dirty: Boolean = false,
reloadToggle: Int = 0, markdown: TextFieldValue = TextFieldValue(""),
initialMarkdown: String = "", setMarkdown: (TextFieldValue) -> Unit = {},
markdown: String = "",
setMarkdown: (String) -> Unit = {},
lockSwiping: Boolean, lockSwiping: Boolean,
toggleLockSwiping: (Boolean) -> Unit, toggleLockSwiping: (Boolean) -> Unit,
message: String? = null, message: String? = null,
@ -153,7 +145,6 @@ private fun MainScreen(
saveCallback: (() -> Unit)? = null, saveCallback: (() -> Unit)? = null,
reset: () -> Unit = {}, reset: () -> Unit = {},
enableWideLayout: Boolean = false, enableWideLayout: Boolean = false,
enableReadability: Boolean = false
) { ) {
val openFileLauncher = val openFileLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
@ -214,7 +205,7 @@ private fun MainScreen(
actions = { actions = {
IconButton(onClick = { IconButton(onClick = {
val shareIntent = Intent(Intent.ACTION_SEND) val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_TEXT, markdown) shareIntent.putExtra(Intent.EXTRA_TEXT, markdown.text)
shareIntent.type = "text/plain" shareIntent.type = "text/plain"
startActivity( startActivity(
context, Intent.createChooser( context, Intent.createChooser(
@ -287,10 +278,8 @@ private fun MainScreen(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.weight(1f), .weight(1f),
reload = reloadToggle,
markdown = markdown, markdown = markdown,
setMarkdown = setMarkdown, setMarkdown = setMarkdown,
enableReadability = enableReadability,
) )
Spacer( Spacer(
modifier = Modifier modifier = Modifier
@ -302,7 +291,7 @@ private fun MainScreen(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.weight(1f), .weight(1f),
markdown = markdown markdown = markdown.text
) )
} }
} else { } else {
@ -312,12 +301,9 @@ private fun MainScreen(
.padding(paddingValues) .padding(paddingValues)
) { ) {
TabbedMarkdownEditor( TabbedMarkdownEditor(
initialMarkdown = initialMarkdown,
markdown = markdown, markdown = markdown,
setMarkdown = setMarkdown, setMarkdown = setMarkdown,
lockSwiping = lockSwiping, lockSwiping = lockSwiping,
enableReadability = enableReadability,
reloadToggle = reloadToggle,
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
} }
@ -327,14 +313,11 @@ private fun MainScreen(
} }
@Composable @Composable
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
private fun TabbedMarkdownEditor( private fun TabbedMarkdownEditor(
initialMarkdown: String, markdown: TextFieldValue,
markdown: String, setMarkdown: (TextFieldValue) -> Unit,
setMarkdown: (String) -> Unit,
lockSwiping: Boolean, lockSwiping: Boolean,
enableReadability: Boolean,
reloadToggle: Int,
scrollBehavior: TopAppBarScrollBehavior scrollBehavior: TopAppBarScrollBehavior
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -349,7 +332,7 @@ private fun TabbedMarkdownEditor(
} }
HorizontalPager( HorizontalPager(
modifier = Modifier.fillMaxSize(1f), state = pagerState, modifier = Modifier.fillMaxSize(1f), state = pagerState,
beyondBoundsPageCount = 1, beyondViewportPageCount = 1,
userScrollEnabled = !lockSwiping userScrollEnabled = !lockSwiping
) { page -> ) { page ->
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
@ -364,17 +347,15 @@ private fun TabbedMarkdownEditor(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection), .nestedScroll(scrollBehavior.nestedScrollConnection),
markdown = initialMarkdown, markdown = markdown,
setMarkdown = setMarkdown, setMarkdown = setMarkdown,
enableReadability = enableReadability,
reload = reloadToggle,
) )
} else { } else {
MarkdownText( MarkdownText(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection), .nestedScroll(scrollBehavior.nestedScrollConnection),
markdown markdown.text
) )
} }
} }

View file

@ -1,8 +1,10 @@
package com.wbrawner.simplemarkdown.ui package com.wbrawner.simplemarkdown.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.Color.TRANSPARENT
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.WebView import android.webkit.WebView
import android.widget.FrameLayout
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -15,7 +17,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import com.wbrawner.simplemarkdown.BuildConfig import com.wbrawner.simplemarkdown.BuildConfig
@ -91,22 +92,34 @@ fun HtmlText(html: String, modifier: Modifier = Modifier) {
AndroidView( AndroidView(
modifier = modifier, modifier = modifier,
factory = { context -> factory = { context ->
WebView(context).apply { FrameLayout(context).apply {
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT ViewGroup.LayoutParams.WRAP_CONTENT
) )
setBackgroundColor(Color.Transparent.toArgb()) addView(
isNestedScrollingEnabled = false WebView(context).apply {
settings.javaScriptEnabled = true tag = WEBVIEW_TAG
loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null) WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
setBackgroundColor(TRANSPARENT)
isNestedScrollingEnabled = false
settings.javaScriptEnabled = true
loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null)
}
)
} }
}, },
update = { webView -> update = { frameLayout ->
webView.loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null) frameLayout.findViewWithTag<WebView>(WEBVIEW_TAG)
.loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null)
} }
) )
} }
private const val WEBVIEW_TAG = "com.wbrawner.simplemarkdown.MarkdownText#WebView"
private fun String.wrapTag(tag: String) = "<$tag>$this</$tag>" private fun String.wrapTag(tag: String) = "<$tag>$this</$tag>"

View file

@ -1,33 +1,23 @@
package com.wbrawner.simplemarkdown.ui package com.wbrawner.simplemarkdown.ui
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
@ -35,28 +25,14 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.wbrawner.simplemarkdown.R import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.model.Readability
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MarkdownTextField( fun MarkdownTextField(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
markdown: String, markdown: TextFieldValue,
setMarkdown: (String) -> Unit, setMarkdown: (TextFieldValue) -> Unit,
reload: Int = 0,
enableReadability: Boolean = false,
) { ) {
val (selection, setSelection) = remember { mutableStateOf(TextRange.Zero) }
val (composition, setComposition) = remember { mutableStateOf<TextRange?>(null) }
val (textFieldValue, setTextFieldValue) = remember(reload) {
mutableStateOf(TextFieldValue(markdown.annotate(enableReadability), selection, composition))
}
val setTextFieldAndViewModelValues: (TextFieldValue) -> Unit = {
setSelection(it.selection)
setComposition(it.composition)
setTextFieldValue(it.copy(annotatedString = it.text.annotate(enableReadability)))
setMarkdown(it.text)
}
val colors = TextFieldDefaults.colors( val colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface, focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface, unfocusedContainerColor = MaterialTheme.colorScheme.surface,
@ -70,57 +46,37 @@ fun MarkdownTextField(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
Column( CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
modifier = modifier BasicTextField(
.fillMaxSize() value = markdown,
.imePadding() modifier = modifier.imePadding(),
.verticalScroll(rememberScrollState()) onValueChange = setMarkdown,
) { enabled = true,
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) { readOnly = false,
BasicTextField( textStyle = textStyle,
value = textFieldValue, cursorBrush = SolidColor(colors.cursorColor),
modifier = Modifier.fillMaxSize(), keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
onValueChange = setTextFieldAndViewModelValues, keyboardActions = KeyboardActions.Default,
enabled = true, interactionSource = interactionSource,
readOnly = false, singleLine = false,
textStyle = textStyle, maxLines = Int.MAX_VALUE,
cursorBrush = SolidColor(colors.cursorColor), minLines = 1,
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences), decorationBox = @Composable { innerTextField ->
keyboardActions = KeyboardActions.Default, // places leading icon, text field with label and placeholder, trailing icon
interactionSource = interactionSource, TextFieldDefaults.DecorationBox(
singleLine = false, value = markdown.text,
maxLines = Int.MAX_VALUE, visualTransformation = VisualTransformation.None,
minLines = 1, innerTextField = innerTextField,
decorationBox = @Composable { innerTextField -> placeholder = {
// places leading icon, text field with label and placeholder, trailing icon Text(stringResource(R.string.markdown_here))
TextFieldDefaults.DecorationBox( },
value = textFieldValue.text, singleLine = false,
visualTransformation = VisualTransformation.None, enabled = true,
innerTextField = innerTextField, interactionSource = interactionSource,
placeholder = { colors = colors,
Text(stringResource(R.string.markdown_here)) contentPadding = PaddingValues(8.dp)
}, )
singleLine = false, },
enabled = true, )
interactionSource = interactionSource,
colors = colors,
contentPadding = PaddingValues(8.dp)
)
}
)
}
} }
} }
private fun String.annotate(enableReadability: Boolean): AnnotatedString {
if (!enableReadability) return AnnotatedString(this)
val readability = Readability(this)
val annotated = AnnotatedString.Builder(this)
for (sentence in readability.sentences()) {
var color = Color.Transparent
if (sentence.syllableCount() > 25) color = Color(229, 232, 42, 100)
if (sentence.syllableCount() > 35) color = Color(193, 66, 66, 100)
annotated.addStyle(SpanStyle(background = color), sentence.start(), sentence.end())
}
return annotated.toAnnotatedString()
}

View file

@ -1,11 +1,11 @@
package com.wbrawner.simplemarkdown.utility package com.wbrawner.simplemarkdown.utility
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.content.res.AssetManager import android.content.res.AssetManager
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.view.View
import android.view.inputmethod.InputMethodManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.Reader import java.io.Reader
@ -22,11 +22,11 @@ suspend fun Uri.getName(context: Context): String {
if ("content" == scheme) { if ("content" == scheme) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
context.contentResolver.query( context.contentResolver.query(
this@getName, this@getName,
null, null,
null, null,
null, null,
null null
)?.use { )?.use {
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
it.moveToFirst() it.moveToFirst()
@ -41,3 +41,11 @@ suspend fun Uri.getName(context: Context): String {
} }
return fileName ?: "Untitled.md" return fileName ?: "Untitled.md"
} }
@Suppress("RecursivePropertyAccessor")
val Context.activity: Activity?
get() = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.activity
else -> null
}

View file

@ -3,7 +3,6 @@ package com.wbrawner.simplemarkdown.utility
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Environment import android.os.Environment
import android.provider.MediaStore
import androidx.core.net.toUri import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -34,8 +33,10 @@ interface FileHelper {
} }
class AndroidFileHelper(private val context: Context) : FileHelper { class AndroidFileHelper(private val context: Context) : FileHelper {
override val defaultDirectory: File = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) override val defaultDirectory: File by lazy {
?: context.filesDir context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
?: context.filesDir
}
override suspend fun open(source: URI): Pair<String, String>? = withContext(Dispatchers.IO) { override suspend fun open(source: URI): Pair<String, String>? = withContext(Dispatchers.IO) {
val uri = source.toString().toUri() val uri = source.toString().toUri()

View file

@ -19,10 +19,17 @@ interface PreferenceHelper {
} }
class AndroidPreferenceHelper(context: Context, private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)): PreferenceHelper { class AndroidPreferenceHelper(context: Context, private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)): PreferenceHelper {
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) private val sharedPreferences by lazy {
private val states = Preference.entries.associateWith { MutableStateFlow(get(it)) } PreferenceManager.getDefaultSharedPreferences(context)
}
private val states by lazy {
val allPrefs: Map<String, Any?> = sharedPreferences.all
Preference.entries.associateWith { preference ->
MutableStateFlow(allPrefs[preference.key] ?: preference.default)
}
}
override fun get(preference: Preference): Any? = sharedPreferences.all[preference.key]?: preference.default override fun get(preference: Preference): Any? = states[preference]?.value
override fun set(preference: Preference, value: Any?) { override fun set(preference: Preference, value: Any?) {
sharedPreferences.edit { sharedPreferences.edit {
@ -40,6 +47,7 @@ class AndroidPreferenceHelper(context: Context, private val coroutineScope: Coro
} }
} }
@Suppress("UNCHECKED_CAST")
override fun <T> observe(preference: Preference): StateFlow<T> = states[preference]!!.asStateFlow() as StateFlow<T> override fun <T> observe(preference: Preference): StateFlow<T> = states[preference]!!.asStateFlow() as StateFlow<T>
} }

View file

@ -0,0 +1,2 @@
- Fix opening files from external apps
- Update dependencies

View file

@ -1,2 +0,0 @@
- Add support for themed icon
- Fix crashes

View file

@ -23,6 +23,7 @@ class FakePreferenceHelper: PreferenceHelper {
preferences[preference] = value preferences[preference] = value
} }
@Suppress("UNCHECKED_CAST")
override fun <T> observe(preference: Preference): StateFlow<T> = override fun <T> observe(preference: Preference): StateFlow<T> =
preferenceFlow(preference) as StateFlow<T> preferenceFlow(preference) as StateFlow<T>
} }

View file

@ -1,5 +1,6 @@
package com.wbrawner.simplemarkdown package com.wbrawner.simplemarkdown
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import com.wbrawner.simplemarkdown.utility.Preference import com.wbrawner.simplemarkdown.utility.Preference
@ -23,6 +24,7 @@ import timber.log.Timber
import java.io.File import java.io.File
import java.net.URI import java.net.URI
@OptIn(ExperimentalCoroutinesApi::class)
class MarkdownViewModelTest { class MarkdownViewModelTest {
private lateinit var fileHelper: FakeFileHelper private lateinit var fileHelper: FakeFileHelper
private lateinit var preferenceHelper: FakePreferenceHelper private lateinit var preferenceHelper: FakePreferenceHelper
@ -51,9 +53,9 @@ class MarkdownViewModelTest {
@Test @Test
fun testMarkdownUpdate() = runTest { fun testMarkdownUpdate() = runTest {
assertEquals("", viewModel.state.value.markdown) assertEquals("".asTextFieldValue(), viewModel.state.value.markdown)
viewModel.updateMarkdown("Updated content") viewModel.updateMarkdown("Updated content")
assertEquals("Updated content", viewModel.state.value.markdown) assertEquals("Updated content".asTextFieldValue(), viewModel.state.value.markdown)
} }
@Test @Test
@ -67,11 +69,11 @@ class MarkdownViewModelTest {
val uri = URI.create("file:///home/user/Untitled.md") val uri = URI.create("file:///home/user/Untitled.md")
preferenceHelper[Preference.AUTOSAVE_URI] = uri.toString() preferenceHelper[Preference.AUTOSAVE_URI] = uri.toString()
viewModel = viewModelFactory.create(MarkdownViewModel::class.java, CreationExtras.Empty) viewModel = viewModelFactory.create(MarkdownViewModel::class.java, CreationExtras.Empty)
viewModelScope.advanceUntilIdle() viewModel.load(null)
assertEquals(uri, fileHelper.openedUris.firstOrNull()) assertEquals(uri, fileHelper.openedUris.firstOrNull())
val (fileName, contents) = fileHelper.file val (fileName, contents) = fileHelper.file
assertEquals(fileName, viewModel.state.value.fileName) assertEquals(fileName, viewModel.state.value.fileName)
assertEquals(contents, viewModel.state.value.markdown) assertEquals(contents.asTextFieldValue(), viewModel.state.value.markdown)
} }
@Test @Test
@ -81,7 +83,7 @@ class MarkdownViewModelTest {
assertEquals(uri, fileHelper.openedUris.firstOrNull()) assertEquals(uri, fileHelper.openedUris.firstOrNull())
val (fileName, contents) = fileHelper.file val (fileName, contents) = fileHelper.file
assertEquals(fileName, viewModel.state.value.fileName) assertEquals(fileName, viewModel.state.value.fileName)
assertEquals(contents, viewModel.state.value.markdown) assertEquals(contents.asTextFieldValue(), viewModel.state.value.markdown)
} }
@Test @Test
@ -125,7 +127,7 @@ class MarkdownViewModelTest {
val uri = URI.create("file:///home/user/Saved.md") val uri = URI.create("file:///home/user/Saved.md")
val testMarkdown = "# Test" val testMarkdown = "# Test"
viewModel.updateMarkdown(testMarkdown) viewModel.updateMarkdown(testMarkdown)
assertEquals(testMarkdown, viewModel.state.value.markdown) assertEquals(testMarkdown.asTextFieldValue(), viewModel.state.value.markdown)
assertTrue(viewModel.save(uri)) assertTrue(viewModel.save(uri))
assertEquals("Saved.md", viewModel.state.value.fileName) assertEquals("Saved.md", viewModel.state.value.fileName)
assertEquals(uri, fileHelper.savedData.last().uri) assertEquals(uri, fileHelper.savedData.last().uri)
@ -138,7 +140,7 @@ class MarkdownViewModelTest {
val uri = URI.create("file:///home/user/Untitled.md") val uri = URI.create("file:///home/user/Untitled.md")
val testMarkdown = "# Test" val testMarkdown = "# Test"
viewModel.updateMarkdown(testMarkdown) viewModel.updateMarkdown(testMarkdown)
assertEquals(testMarkdown, viewModel.state.value.markdown) assertEquals(testMarkdown.asTextFieldValue(), viewModel.state.value.markdown)
fileHelper.errorOnSave = true fileHelper.errorOnSave = true
assertNull(viewModel.state.value.alert) assertNull(viewModel.state.value.alert)
assertFalse(viewModel.save(uri)) assertFalse(viewModel.save(uri))
@ -158,7 +160,7 @@ class MarkdownViewModelTest {
assertNull(viewModel.state.value.alert) assertNull(viewModel.state.value.alert)
with(viewModel.state.value) { with(viewModel.state.value) {
assertEquals("New.md", fileName) assertEquals("New.md", fileName)
assertEquals("", markdown) assertEquals("".asTextFieldValue(), markdown)
assertNull(path) assertNull(path)
assertNull(saveCallback) assertNull(saveCallback)
assertNull(alert) assertNull(alert)
@ -180,7 +182,7 @@ class MarkdownViewModelTest {
requireNotNull(onClick) requireNotNull(onClick)
onClick.invoke() onClick.invoke()
} }
assertEquals(viewModel.state.value, EditorState(reloadToggle = 0.inv())) assertEquals(viewModel.state.value, EditorState())
} }
@Test @Test
@ -199,7 +201,7 @@ class MarkdownViewModelTest {
viewModel.save(uri) viewModel.save(uri)
assertNotNull(viewModel.state.value.saveCallback) assertNotNull(viewModel.state.value.saveCallback)
requireNotNull(viewModel.state.value.saveCallback).invoke() requireNotNull(viewModel.state.value.saveCallback).invoke()
assertEquals(viewModel.state.value, EditorState(reloadToggle = 0.inv())) assertEquals(viewModel.state.value, EditorState())
} }
@Test @Test
@ -216,7 +218,7 @@ class MarkdownViewModelTest {
assertNull(viewModel.state.value.alert) assertNull(viewModel.state.value.alert)
with(viewModel.state.value) { with(viewModel.state.value) {
assertEquals("Unsaved.md", fileName) assertEquals("Unsaved.md", fileName)
assertEquals("", markdown) assertEquals("".asTextFieldValue(), markdown)
assertNull(path) assertNull(path)
assertNull(saveCallback) assertNull(saveCallback)
assertNull(alert) assertNull(alert)
@ -302,4 +304,6 @@ class MarkdownViewModelTest {
assertFalse(preferenceHelper.preferences[Preference.LOCK_SWIPING] as Boolean) assertFalse(preferenceHelper.preferences[Preference.LOCK_SWIPING] as Boolean)
assertFalse(viewModel.state.value.lockSwiping) assertFalse(viewModel.state.value.lockSwiping)
} }
private fun String.asTextFieldValue() = TextFieldValue(this)
} }

View file

@ -0,0 +1,51 @@
package com.wbrawner.releasehelper
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.internal.provider.Providers
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import javax.inject.Inject
private const val CHANGELOG_PATH = "src/play/play/release-notes/en-US/production.txt"
abstract class ChangelogTask @Inject constructor(
objectFactory: ObjectFactory,
providers: ProviderFactory,
) : DefaultTask() {
@get:OutputFile
val changelogFile: RegularFileProperty = objectFactory.fileProperty()
@get:Input
@Suppress("UnstableApiUsage")
val latestTag: String = providers.exec {
commandLine("git" , "describe", "--tags", "--abbrev=0")
}.standardOutput.asText.get()
init {
changelogFile.set(project.layout.projectDirectory.file(CHANGELOG_PATH))
}
@TaskAction
fun execute() {
val changelog = "git log --format=\"%B\" ${latestTag.trim()}..".execute()
logger.info("Latest tag: $latestTag")
logger.info("Changelog: ${changelog.joinToString("\n")}")
changelogFile.get().asFile.writer().use { writer ->
writer.write(
changelog.joinToString("\n") { it.trim('"') }
)
}
}
private fun String.execute(): List<String> = ProcessBuilder()
.command(this.split(" "))
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.start()
.inputReader()
.readLines()
}

View file

@ -2,46 +2,15 @@ package com.wbrawner.releasehelper
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.tasks.Exec
import org.gradle.kotlin.dsl.extra import org.gradle.kotlin.dsl.extra
import org.gradle.kotlin.dsl.provideDelegate import org.gradle.kotlin.dsl.provideDelegate
import java.io.ByteArrayOutputStream import org.gradle.kotlin.dsl.register
import java.io.File import java.io.File
class ReleaseHelperPlugin : Plugin<Project> { class ReleaseHelperPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
target.tasks.register("getLatestTag", Exec::class.java) { target.tasks.register<ChangelogTask>("changelog")
val latestTag = ByteArrayOutputStream()
standardOutput = latestTag
commandLine("git describe --tags --abbrev=0".split(" "))
doLast {
target.project.extra["latestTag"] = latestTag.toString().trim()
logger.info("Latest tag: ${target.project.extra["latestTag"]}")
}
}
target.tasks.register("changelog") {
val changelogFile = File(target.projectDir, "src/play/play/release-notes/en-US/default.txt")
inputs.property("tag", target.provider {
target.project.extra["latestTag"]
})
outputs.file(changelogFile)
dependsOn("getLatestTag")
doLast {
val latestTag: String by target.project.extra
val changelog = ByteArrayOutputStream()
target.exec {
standardOutput = changelog
commandLine = "git log --format=\"%B\" ${latestTag.trim()}..".split(" ")
}
changelogFile.writeText(
changelog.toString()
.split("\n")
.mapNotNull { it.trim('"').ifBlank { null } }
.joinToString("\n") { "- $it" }
)
}
}
target.tasks.register("majorRelease") { target.tasks.register("majorRelease") {
dependsOn("changelog", "getLatestTag") dependsOn("changelog", "getLatestTag")

View file

@ -41,19 +41,24 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "11"
} }
lint { lint {
disable += listOf("AndroidGradlePluginVersion", "GradleDependency") disable += listOf(
"AndroidGradlePluginVersion",
"GradleDependency",
"ObsoleteLintCustomCheck"
)
warningsAsErrors = true warningsAsErrors = true
} }
} }
dependencies { dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.acra.core) implementation(libs.acra.core)
implementation(libs.acra.http) implementation(libs.acra.http)
runtimeOnly(libs.acra.limiter) runtimeOnly(libs.acra.limiter)

View file

@ -2,7 +2,8 @@ package com.wbrawner.simplemarkdown.core
import android.app.Application import android.app.Application
import android.util.Log import android.util.Log
import org.acra.ACRA import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.acra.config.httpSender import org.acra.config.httpSender
import org.acra.data.StringFormat import org.acra.data.StringFormat
import org.acra.ktx.initAcra import org.acra.ktx.initAcra
@ -13,22 +14,32 @@ import timber.log.Timber
class ErrorReporterTree private constructor(): Timber.Tree() { class ErrorReporterTree private constructor(): Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (priority != Log.ERROR) return if (priority != Log.ERROR) return
t?.sendSilentlyWithAcra() if (t !is LocalOnlyException) {
t?.sendSilentlyWithAcra()
}
} }
companion object { companion object {
fun create(application: Application): ErrorReporterTree { suspend fun create(application: Application): ErrorReporterTree {
application.createErrorReporterTree() application.createErrorReporterTree()
return ErrorReporterTree() return ErrorReporterTree()
} }
} }
} }
private fun Application.createErrorReporterTree() { /**
* An exception wrapper that prevents exceptions from being sent to an error reporter. Useful for
* logging things like IOExceptions that are useful to see locally but not so helpful if reported
*/
class LocalOnlyException(override val message: String?, override val cause: Throwable): Exception(message, cause) {
constructor(cause: Throwable): this(null, cause)
}
private suspend fun Application.createErrorReporterTree() = withContext(Dispatchers.IO) {
initAcra { initAcra {
reportFormat = StringFormat.JSON reportFormat = StringFormat.JSON
httpSender { httpSender {
uri = "${BuildConfig.ACRA_URL}/report" /*best guess, you may need to adjust this*/ uri = "${BuildConfig.ACRA_URL}/report"
basicAuthLogin = BuildConfig.ACRA_USER basicAuthLogin = BuildConfig.ACRA_USER
basicAuthPassword = BuildConfig.ACRA_PASS basicAuthPassword = BuildConfig.ACRA_PASS
httpMethod = HttpSender.Method.POST httpMethod = HttpSender.Method.POST

View file

@ -10,7 +10,7 @@ android {
defaultConfig { defaultConfig {
minSdk = libs.versions.minSdk.get().toInt() minSdk = libs.versions.minSdk.get().toInt()
consumerProguardFiles("consumer-rules.pro")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@ -27,14 +27,18 @@ android {
compose = true compose = true
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "11"
} }
lint { lint {
disable += listOf("AndroidGradlePluginVersion", "GradleDependency") disable += listOf(
"AndroidGradlePluginVersion",
"GradleDependency",
"ObsoleteLintCustomCheck"
)
warningsAsErrors = true warningsAsErrors = true
} }
} }

View file

@ -0,0 +1,2 @@
-dontwarn javax.annotation.processing.AbstractProcessor
-dontwarn javax.annotation.processing.SupportedOptions

View file

@ -1,43 +1,40 @@
[versions] [versions]
acra = "5.11.3" acra = "5.12.0"
activityKtx = "1.9.1" activityKtx = "1.9.3"
animationCore = "1.6.8" animationCore = "1.7.5"
appcompat = "1.7.0" appcompat = "1.7.0"
billing = "7.0.0" billing = "7.1.1"
browser = "1.8.0" browser = "1.8.0"
commonMarkVersion = "0.22.0" commonMarkVersion = "0.24.0"
composeBom = "2024.06.00" composeBom = "2024.11.00"
core = "1.6.1" core = "1.6.1"
coreKtx = "1.13.1" coreKtx = "1.15.0"
coreSplashscreen = "1.0.1" coreSplashscreen = "1.0.1"
coroutines = "1.8.1" coroutines = "1.9.0"
dependencyAnalysis = "1.33.0" dependencyAnalysis = "2.5.0"
espressoVersion = "3.6.1" espressoVersion = "3.6.1"
fladle = "0.17.5" fladle = "0.17.5"
googleServices = "4.4.2" googleServices = "4.4.2"
firebaseCrashlyticsGradle = "3.0.2" firebaseCrashlyticsGradle = "3.0.2"
androidGradlePlugin = "8.5.2" androidGradlePlugin = "8.7.2"
hamcrestCore = "1.3"
junit = "4.13.2" junit = "4.13.2"
kotlin = "2.0.10" kotlin = "2.0.21"
lifecycleViewmodelKtx = "2.8.4" lifecycleViewmodelKtx = "2.8.7"
material = "1.12.0" material = "1.12.0"
material3WindowSizeClassAndroid = "1.2.1" material3WindowSizeClassAndroid = "1.3.1"
materialIconsCore = "1.6.8" materialIconsCore = "1.7.5"
maxSdk = "35" maxSdk = "35"
minSdk = "23" minSdk = "23"
monitor = "1.7.1" monitor = "1.7.2"
navigationCommon = "2.7.7" navigation = "2.8.4"
navigationRuntimeKtx = "2.7.7" orchestrator = "1.5.1"
navigationVersion = "2.7.7" play = "2.0.2"
orchestrator = "1.5.0"
play = "2.0.1"
preferenceKtx = "1.2.1" preferenceKtx = "1.2.1"
robolectric = "4.13" robolectric = "4.14.1"
runner = "1.6.1" runner = "1.6.2"
syllableCounter = "4.1.0" syllableCounter = "4.1.0"
timber = "5.0.1" timber = "5.0.1"
tripletPlay = "3.10.1" tripletPlay = "3.12.1"
[libraries] [libraries]
acra-advanced-scheduler = { module = "ch.acra:acra-advanced-scheduler", version.ref = "acra" } acra-advanced-scheduler = { module = "ch.acra:acra-advanced-scheduler", version.ref = "acra" }
@ -65,9 +62,9 @@ androidx-material-icons-extended = { module = "androidx.compose.material:materia
androidx-material3 = { module = "androidx.compose.material3:material3" } androidx-material3 = { module = "androidx.compose.material3:material3" }
androidx-material3-windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class-android", version.ref = "material3WindowSizeClassAndroid" } androidx-material3-windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class-android", version.ref = "material3WindowSizeClassAndroid" }
androidx-monitor = { module = "androidx.test:monitor", version.ref = "monitor" } androidx-monitor = { module = "androidx.test:monitor", version.ref = "monitor" }
androidx-navigation-common = { module = "androidx.navigation:navigation-common", version.ref = "navigationCommon" } androidx-navigation-common = { module = "androidx.navigation:navigation-common", version.ref = "navigation" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationVersion" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
androidx-navigation-runtime-ktx = { module = "androidx.navigation:navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" } androidx-navigation-runtime-ktx = { module = "androidx.navigation:navigation-runtime-ktx", version.ref = "navigation" }
androidx-orchestrator = { module = "androidx.test:orchestrator", version.ref = "orchestrator" } androidx-orchestrator = { module = "androidx.test:orchestrator", version.ref = "orchestrator" }
androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
androidx-runner = { module = "androidx.test:runner", version.ref = "runner" } androidx-runner = { module = "androidx.test:runner", version.ref = "runner" }
@ -92,7 +89,6 @@ commonmark-ext-yaml-front-matter = { module = "org.commonmark:commonmark-ext-yam
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
foundation = { module = "androidx.compose.foundation:foundation" } foundation = { module = "androidx.compose.foundation:foundation" }
foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } foundation-layout = { module = "androidx.compose.foundation:foundation-layout" }
hamcrest-core = { module = "org.hamcrest:hamcrest-core", version.ref = "hamcrestCore" }
junit = { module = "junit:junit", version.ref = "junit" } junit = { module = "junit:junit", version.ref = "junit" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View file

@ -1,4 +1,5 @@
storePassword= storePassword=
keyPassword= keyPassword=
keyAlias= keyAlias=
storeFile= storeFile=
publishCredentialsFile=

View file

@ -24,17 +24,21 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_11
} }
buildFeatures { buildFeatures {
compose = true compose = true
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "11"
} }
lint { lint {
disable += listOf("AndroidGradlePluginVersion", "GradleDependency") disable += listOf(
"AndroidGradlePluginVersion",
"GradleDependency",
"ObsoleteLintCustomCheck"
)
warningsAsErrors = true warningsAsErrors = true
} }
} }