Compare commits

...

247 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
094ac1fccf Close navigation drawer on back press
Some checks are pending
Build & Test / Validate (pull_request) Successful in 22s
Build & Test / Run Unit Tests (pull_request) Successful in 8m20s
Build & Test / Run UI Tests (pull_request) Successful in 12m23s
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 04:44:33 +00:00
90ab7f9397 Enable gestures on nav drawer when open 2024-08-22 04:44:33 +00:00
dc97ea7b78 Persist preference for Lock Swiping 2024-08-22 04:44:33 +00:00
9b6a5ba2be
Ignore lint warnings for GradleDependency version
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 8m51s
Build & Test / Run UI Tests (pull_request) Successful in 12m40s
Build & Test / Validate (push) Successful in 17s
Build & Test / Run Unit Tests (push) Successful in 9m12s
Build & Test / Run UI Tests (push) Successful in 14m57s
I've already configured Renovate to automatically keep the dependencies up to date so I don't need the lint to be failing builds because of outdated dependencies.
2024-08-21 22:10:34 -06:00
2a9d96428a
Ignore lint warnings for Android Gradle Plugin version
Some checks failed
Build & Test / Validate (pull_request) Successful in 19s
Build & Test / Run Unit Tests (pull_request) Failing after 8m37s
Build & Test / Run UI Tests (pull_request) Successful in 12m11s
I've already configured Renovate to automatically keep the dependencies up to date so I don't need the lint to be failing builds because of outdated dependencies.
2024-08-21 20:57:57 -06:00
265d3bfcd8 Update dependency gradle to v8.10
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 12m9s
Build & Test / Run UI Tests (pull_request) Successful in 18m46s
Build & Test / Validate (push) Successful in 17s
Build & Test / Run Unit Tests (push) Successful in 15m26s
Build & Test / Run UI Tests (push) Successful in 22m14s
2024-08-14 12:07:16 +00:00
a4bcba188a Update androidGradlePlugin to v8.5.2
All checks were successful
Build & Test / Validate (pull_request) Successful in 23s
Build & Test / Run Unit Tests (pull_request) Successful in 9m6s
Build & Test / Run UI Tests (pull_request) Successful in 16m23s
Build & Test / Validate (push) Successful in 16s
Build & Test / Run Unit Tests (push) Successful in 11m4s
Build & Test / Run UI Tests (push) Successful in 18m31s
2024-08-09 01:01:45 +00:00
955d854420 Update gradle/wrapper-validation-action action to v3
All checks were successful
Build & Test / Validate (pull_request) Successful in 14s
Build & Test / Run Unit Tests (pull_request) Successful in 7m29s
Build & Test / Run UI Tests (pull_request) Successful in 11m19s
Build & Test / Validate (push) Successful in 15s
Build & Test / Run Unit Tests (push) Successful in 7m58s
Build & Test / Run UI Tests (push) Successful in 12m21s
2024-08-06 11:02:37 +00:00
9d4f48f8ae Update dependency org.jetbrains.kotlin.android to v2.0.10
All checks were successful
Build & Test / Validate (pull_request) Successful in 16s
Build & Test / Run Unit Tests (pull_request) Successful in 8m5s
Build & Test / Run UI Tests (pull_request) Successful in 15m9s
Build & Test / Validate (push) Successful in 15s
Build & Test / Run Unit Tests (push) Successful in 10m4s
Build & Test / Run UI Tests (push) Successful in 19m3s
2024-08-06 10:06:19 +00:00
c3c43dc068 Update openjdk Docker tag to v24
All checks were successful
Build & Test / Validate (pull_request) Successful in 14s
Build & Test / Run Unit Tests (pull_request) Successful in 7m39s
Build & Test / Run UI Tests (pull_request) Successful in 11m35s
Build & Test / Validate (push) Successful in 15s
Build & Test / Run Unit Tests (push) Successful in 7m54s
Build & Test / Run UI Tests (push) Successful in 13m15s
2024-08-03 10:03:57 +00:00
a89cd893db Update actions/setup-java action to v4
Some checks failed
Build & Test / Validate (push) Successful in 14s
Build & Test / Run Unit Tests (push) Successful in 8m15s
Build & Test / Run UI Tests (push) Failing after 7m32s
2024-08-03 09:51:28 +00:00
714808a0d4 Update actions/checkout action to v4
Some checks are pending
Build & Test / Validate (pull_request) Successful in 13s
Build & Test / Run Unit Tests (pull_request) Successful in 7m35s
Build & Test / Run UI Tests (pull_request) Successful in 10m46s
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-03 08:01:45 +00:00
82ebfab5c9 Update dependency org.robolectric:robolectric to v4.13
All checks were successful
Build & Test / Validate (pull_request) Successful in 13s
Build & Test / Run Unit Tests (pull_request) Successful in 7m48s
Build & Test / Run UI Tests (pull_request) Successful in 10m44s
Build & Test / Validate (push) Successful in 15s
Build & Test / Run Unit Tests (push) Successful in 9m33s
Build & Test / Run UI Tests (push) Successful in 13m1s
2024-08-03 07:02:08 +00:00
170618d961 Update dependency eu.crydee:syllable-counter to v4.1.0
All checks were successful
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 7m42s
Build & Test / Run UI Tests (pull_request) Successful in 11m49s
Build & Test / Validate (push) Successful in 15s
Build & Test / Run Unit Tests (push) Successful in 8m57s
Build & Test / Run UI Tests (push) Successful in 12m37s
2024-08-03 06:02:32 +00:00
c7b1d0ec90 Update dependency gradle to v8.9
Some checks failed
Build & Test / Validate (pull_request) Successful in 15s
Build & Test / Run Unit Tests (pull_request) Successful in 10m2s
Build & Test / Run UI Tests (pull_request) Successful in 17m51s
Build & Test / Validate (push) Successful in 15s
Build & Test / Run Unit Tests (push) Successful in 12m4s
Build & Test / Run UI Tests (push) Failing after 15m23s
2024-08-03 05:05:47 +00:00
5721b163af Update dependency com.github.triplet.play to v3.10.1
All checks were successful
Build & Test / Validate (push) Successful in 14s
Build & Test / Run Unit Tests (push) Successful in 7m45s
Build & Test / Run UI Tests (push) Successful in 16m17s
2024-08-03 04:44:10 +00:00
caec13a0e3 Update dependency com.autonomousapps.dependency-analysis to v1.33.0
Some checks failed
Build & Test / Validate (pull_request) Successful in 16s
Build & Test / Run Unit Tests (pull_request) Successful in 7m17s
Build & Test / Run UI Tests (pull_request) Successful in 14m58s
Build & Test / Run Unit Tests (push) Blocked by required conditions
Build & Test / Run UI Tests (push) Blocked by required conditions
Build & Test / Validate (push) Has been cancelled
2024-08-03 04:02:21 +00:00
44106351bc Update coroutines to v1.8.1
All checks were successful
Build & Test / Validate (push) Successful in 14s
Build & Test / Run Unit Tests (push) Successful in 7m16s
Build & Test / Run UI Tests (push) Successful in 15m3s
2024-08-03 03:41:32 +00:00
5394f8f64a Update dependency com.osacky.fladle to v0.17.5
Some checks failed
Build & Test / Validate (pull_request) Successful in 15s
Build & Test / Run Unit Tests (pull_request) Successful in 6m41s
Build & Test / Run UI Tests (pull_request) Successful in 14m9s
Build & Test / Run Unit Tests (push) Blocked by required conditions
Build & Test / Run UI Tests (push) Blocked by required conditions
Build & Test / Validate (push) Has been cancelled
2024-08-03 03:01:33 +00:00
2cf2ffc883 Add renovate.json
All checks were successful
Build & Test / Validate (pull_request) Successful in 14s
Build & Test / Run Unit Tests (pull_request) Successful in 5m47s
Build & Test / Run UI Tests (pull_request) Successful in 9m38s
Build & Test / Validate (push) Successful in 18s
Build & Test / Run Unit Tests (push) Successful in 5m54s
Build & Test / Run UI Tests (push) Successful in 10m2s
2024-08-03 00:16:22 +00:00
79a5a3809a
Fix failing unit tests
All checks were successful
Build & Test / Validate (pull_request) Successful in 14s
Build & Test / Run Unit Tests (pull_request) Successful in 9m4s
Build & Test / Run UI Tests (pull_request) Successful in 17m58s
Build & Test / Validate (push) Successful in 15s
Build & Test / Run Unit Tests (push) Successful in 9m45s
Build & Test / Run UI Tests (push) Successful in 17m39s
2024-08-02 16:46:03 -06:00
840ebc4fd1
Fix failing UI test
Some checks failed
Build & Test / Validate (pull_request) Successful in 15s
Build & Test / Run Unit Tests (pull_request) Failing after 8m52s
Build & Test / Run UI Tests (pull_request) Successful in 16m38s
2024-08-02 16:11:51 -06:00
b0e8ebbf71
Fix failing UI tests
Some checks failed
Build & Test / Validate (pull_request) Successful in 25s
Build & Test / Run Unit Tests (pull_request) Successful in 9m19s
Build & Test / Run UI Tests (pull_request) Failing after 17m13s
2024-08-01 18:47:12 -06:00
262b63cfa0
Setup Android SDK before attempting to build in UI test job
Some checks failed
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 9m29s
Build & Test / Run UI Tests (pull_request) Failing after 18m22s
2024-07-31 20:58:33 -06:00
c6e14d7d0b
Setup Android SDK for UI test workflow job
Some checks failed
Build & Test / Validate (pull_request) Successful in 15s
Build & Test / Run UI Tests (pull_request) Failing after 3m57s
Build & Test / Run Unit Tests (pull_request) Has been cancelled
2024-07-31 20:52:38 -06:00
86cd33ff5f
Address lint issues
Some checks failed
Build & Test / Validate (pull_request) Successful in 19s
Build & Test / Run Unit Tests (pull_request) Successful in 10m34s
Build & Test / Run UI Tests (pull_request) Failing after 4m1s
2024-07-31 20:17:14 -06:00
06cbc5ec31
Fix padding and link color for HtmlText
Some checks failed
Build & Test / Validate (pull_request) Successful in 16s
Build & Test / Validate (push) Successful in 15s
Build & Test / Run Unit Tests (pull_request) Failing after 8m12s
Build & Test / Run UI Tests (pull_request) Failing after 4m19s
Build & Test / Run Unit Tests (push) Failing after 5m27s
Build & Test / Run UI Tests (push) Failing after 5m27s
2024-07-11 22:33:22 -06:00
b94ba8d4c2
Update dependencies 2024-07-11 22:33:19 -06:00
880393104f
Migrate to Gradle version catalogs
I snuck a couple of other updates in here as well, like Kotlin 2.0 and Play Core 2.0.1
2024-07-11 22:00:13 -06:00
110d5402cd
Fix duplicate namespace for non-free build variant 2024-07-10 21:56:20 -06:00
ac3cd9d5c4
Add Forgejo workflow
Some checks failed
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Validate (push) Successful in 15s
Build & Test / Run Unit Tests (pull_request) Failing after 10m42s
Build & Test / Run UI Tests (pull_request) Failing after 3m53s
Build & Test / Run Unit Tests (push) Failing after 11m12s
Build & Test / Run UI Tests (push) Failing after 4m17s
2024-07-09 21:11:49 -06:00
92123d2f24
Address buildHealth issues 2024-05-18 22:30:55 -06:00
2a0cc4d889
Split free & play code into separate gradle modules
This will hopefully enable me to use the gradle build health plugin, and still be compliant with F-Droid's policies. A consequence of this is that I had to go back to ACRA for error reporting, since I couldn't find a way to keep Firebase's gradle plugins on a library module instead of the main app module
2024-05-18 22:13:39 -06:00
de9956cbf7
Fix padding on edit view 2024-05-06 19:39:16 -06:00
75e38a97dd
Implement collapsing toolbar on editor 2024-05-06 00:26:33 -06:00
c792bd106e
Move MarkdownNavigationDrawer and MarkdownTopAppBar to their own files 2024-05-05 23:42:26 -06:00
b7c2e116cf
Switch from MD4C to CommonMark for markdown parsing
I wanted to use MD4C for the performance but unfortunately there seem to be issues with how it handles UTF-8 and how the JNI handles it. CommonMark will have to do for now at least
2024-05-05 23:09:45 -06:00
7ed94aebf4
Remove padding from markdown editor and pre-render preview 2024-05-05 23:06:27 -06:00
c7f44e2b81
Fix link to crashlytics in privacy policy 2024-05-05 23:01:47 -06:00
c86d2c2f6a
Improve ergonomics of editor:
- Show indicator in title for when file has unsaved changes
- Fix issues with scrolling on preview
  - Default to disable locked swiping between tabs
  - Disable gestures on navigation drawer
 - Fix text selection and typing in editor
2024-05-05 19:44:36 -06:00
f4c7057daf
Add a couple of tests for markdown parsing
I was trying to see if I had any memory issues but at least for the few tests I added, nothing seemed to stand out
2024-05-05 19:41:26 -06:00
208e0a1a6f
Use MD4C to convert markdown to HTML 2024-05-04 10:37:33 -06:00
37f0b8bae8 Remove Plausible 2024-02-22 22:14:16 -07:00
ccc4299d10 Fix missing import in build.gradle.kts 2024-02-15 16:35:23 -07:00
e0d43d5154 Fix tests 2024-02-15 16:35:23 -07:00
a6616550dd Fix cropping on splash screen icon 2024-02-15 16:35:23 -07:00
a48f243e48 Improve widescreen layout for large devices 2024-02-15 16:35:23 -07:00
6acacf5edb Limit file titles to one line in toolbar 2024-02-15 16:35:23 -07:00
a35aa62399 Add extra padding to text field when keyboard is open
This allows users to be able to scroll all the way to the bottom of long documents without the keyboard obscuring the text
2024-02-15 16:35:23 -07:00
f891d3635d Hide keyboard when switching to preview 2024-02-15 16:35:23 -07:00
b706f5b5eb Remove AndDown code 2024-02-15 16:35:23 -07:00
32b4518bf7 Use javascript markdown engine 2024-02-15 16:35:23 -07:00
9f7142b2ab Improve build times 2024-02-15 16:35:23 -07:00
1bf01c43dd Bump AGP 2024-02-15 16:35:23 -07:00
c2cc9fbd1c Add tests for MarkdownViewModel#load 2024-02-15 16:35:23 -07:00
ae5b13dfd0 Finish compose migration and remove unnecessary assets/code 2024-02-15 16:35:23 -07:00
493444aaab Implement Readability Highlighting in compose
This could still use some performance tweaks to make it run a bit better but at least it works
2024-02-15 16:35:23 -07:00
c7e54f21d9 Migrate some more to Jetpack Compose + Material3 2024-02-15 16:35:23 -07:00
643345493e Migrate to Jetpack Compose + Material3 2024-02-15 16:35:23 -07:00
e352add928 Bump Kotlin to 1.9.10 2024-02-15 16:35:23 -07:00
ea2e949893 Fix path to unit test results 2023-12-15 19:54:40 -07:00
35d3fee732 Auto-merge with rebase from dependabot 2023-12-15 19:54:40 -07:00
0d9fe441e8 Enable automerge for PRs from me 2023-12-15 19:54:40 -07:00
9f6444524f Publish results of JUnit tests to GHA 2023-12-15 19:54:40 -07:00
8b7d83c6ae Fix UI tests 2023-12-15 19:54:40 -07:00
4133bb1d72 Fix CI workflow 2023-12-15 19:54:40 -07:00
51025d8181 Fix CI workflow 2023-12-15 19:54:40 -07:00
f542e10264 Update workflow to run unit and UI tests in parallel and execute on pull requests in addition to the main branch 2023-12-15 19:54:40 -07:00
80e323b326
Rename VersionPart to ReleaseType 2023-09-29 07:44:24 -06:00
c04e2ded8e
Fix automated release note creation 2023-09-29 07:44:08 -06:00
535bfab8db
Fix version updates in ReleaseHelperPlugin 2023-09-29 07:40:36 -06:00
f69bf81630
Implement semi-automated releases
This still requires manually running the gradle task but the next step will be getting a GitHub workflow to perform the release whenever a new tag is pushed
2023-09-28 20:15:38 -06:00
Licaon_Kter
f51d669da3 Use the F-Droid compatible syntax 2023-03-30 21:04:11 -06:00
Licaon_Kter
729758b7bc Use stable gradle 2023-03-30 21:03:58 -06:00
fe2f36f06a Add support for themed icon 2023-03-28 06:34:40 -06:00
c5355d1565 Skip permissions checking for saving and loading files
At some point I refactored to an API that doesn't require any permissions at all but never removed the permissions check. This corrects that
2023-03-27 20:23:28 -06:00
13bfe236a3
Convert .gradle files to gradle.kts 2023-01-29 20:41:15 -07:00
b9bc147bde
Bump dependency versions 2023-01-29 20:10:53 -07:00
c54f6c00ac
Disable Plausible for debug builds 2023-01-27 18:15:01 -07:00
5fe4942d84
Bump AGP 2023-01-27 18:13:53 -07:00
b8bbd267c7 Track app build info in plausible 2022-11-03 09:15:52 -06:00
e2aca68ed5
Update README.md 2022-11-03 08:58:49 -06:00
a296e98cde
Update README.md 2022-11-03 08:41:31 -06:00
c701e4db2c Update README 2022-11-03 08:35:47 -06:00
2e4787fd93 Fix CI (maybe for real this time) 2022-11-03 07:20:41 -06:00
dce178580a Only test play builds 2022-11-03 07:14:19 -06:00
8a3a6a30ba Fix CI builds 2022-11-03 07:10:51 -06:00
40e8326d14 Drop Samsung builds
I haven't updated the app in the Galaxy store for years anyways, and it probably doesn't have any active users.
2022-11-03 07:06:09 -06:00
b231a56c9a Fix tests 2022-11-03 06:57:18 -06:00
47dd47aa7e Use snapshot release of plausible-android 2022-11-03 06:10:54 -06:00
5af4d2028f Replace Firebase analytics with Plausible 2022-10-29 22:03:39 -06:00
33574ce5f5 Fix GitHub Actions workflow 2022-10-29 21:41:16 -06:00
6ac4c94937 Bump version for release 2022-10-23 23:48:54 -06:00
d0c61bea52 Update dependencies and target sdk 2022-10-23 23:45:28 -06:00
3108114b60 Improve analytics
- Automatic page view tracking wasn't helpful since it was based on the Activities viewed, and SimpleMarkdown is a single-Activity app, so page views are now tracked manually
- User preferences are now reported so I can remove preferences that aren't used and focus my time on features that are actively used
- Opting out of crash reports is no longer possible. I need crash reports to be able to improve the app. It would also simplify the code a bit to not need to take that into account. Existing opt-outs will be respected but moving forward, new users will need to download the app from Fdroid if they'd like to avoid interactions with Google.
2022-10-23 23:44:02 -06:00
c315439763 Update dependencies 2021-06-27 19:35:51 -06:00
c090076901 Add note about INTERNET permission justification for F-droid description 2021-06-27 10:46:27 -06:00
17d1bd28c5 Remove Billing permission from free variant 2021-06-27 10:46:27 -06:00
Poussinou
c9e40d19cb Update README.md 2021-04-15 08:30:01 -07:00
fffafaa33b Add fastlane metadata
Signed-off-by: William Brawner <me@wbrawner.com>
2021-03-14 11:27:35 -07:00
89d957c2d0 Bump version for release
Nothing new here, just a minor fix for F-Droid
2021-03-12 15:53:00 -07:00
94cdb70da7 Fix free flavor builds 2021-03-12 15:53:00 -07:00
62720ddd05 Bump version for release 2021-03-12 15:42:51 -07:00
50d7622172 Consolidate autosave URI persistence management 2021-02-21 18:32:53 -07:00
1522806e62 Prevent save/discard dialog from appearing when file contents haven't changed after loading 2021-02-21 17:57:41 -07:00
c2be2274e8 Prevent autosave when manual save is in progress 2021-02-21 17:56:53 -07:00
260c49d8d5 Fix PersistentTree logging to append 2021-02-21 17:29:02 -07:00
31e81f31b4 Fix URI logging in ViewModel 2021-02-21 17:14:02 -07:00
14dc55433a Use Timber for logging 2021-02-21 14:18:37 -07:00
96e7b7c6b3 Fix potential memory leak in ReviewHelper
There was a static reference to the currentActivity that was unnecessary
2021-02-21 14:18:36 -07:00
eb756e8525 Remove manual CoroutineScope implementations 2021-02-21 14:18:36 -07:00
7bf4bef8a2
Update Android CI badge link in README 2021-02-20 22:42:30 -07:00
3bf5862a13
Update badge in README
It now uses the GitHub actions badge instead of the old GitLab one
2021-02-20 22:36:43 -07:00
b056e779e0 Grant WRITE_EXTERNAL_STORAGE permission for UI tests
The absence of this was causing the tests to fail on Firebase.
2021-02-20 22:23:58 -07:00
67bf1626a0 Run UI tests on API 29
For some reason running the tests on API 30 fails with the below exception. Interestingly enough the tests run just fine on my local API 30 emulator.

java.lang.RuntimeException: Exception thrown in onCreate() of ComponentInfo{com.wbrawner.simplemarkdown.test/androidx.test.runner.AndroidJUnitRunner}: java.lang.IllegalStateException: Cannot connect to androidx.test.orchestrator.OrchestratorService
     FATAL EXCEPTION: main
Process: com.wbrawner.simplemarkdown, PID: 21960
java.lang.RuntimeException: Exception thrown in onCreate() of ComponentInfo{com.wbrawner.simplemarkdown.test/androidx.test.runner.AndroidJUnitRunner}: java.lang.IllegalStateException: Cannot connect to androidx.test.orchestrator.OrchestratorService
	at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6709)
	at android.app.ActivityThread.access$1300(ActivityThread.java:237)
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1913)
	at android.os.Handler.dispatchMessage(Handler.java:106)
	at android.os.Looper.loop(Looper.java:223)
	at android.app.ActivityThread.main(ActivityThread.java:7656)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
Caused by: java.lang.IllegalStateException: Cannot connect to androidx.test.orchestrator.OrchestratorService
	at androidx.test.internal.events.client.TestEventServiceConnectionBase.connect(TestEventServiceConnectionBase.java:91)
	at androidx.test.internal.events.client.TestEventClient.connect(TestEventClient.java:125)
	at androidx.test.runner.AndroidJUnitRunner.isOrchestratorServiceProvided(AndroidJUnitRunner.java:347)
	at androidx.test.runner.AndroidJUnitRunner.onCreate(AndroidJUnitRunner.java:319)
	at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6704)
	... 8 more
2021-02-20 22:01:43 -07:00
2b6d3ce2bc Fix flank auth in GitHub workflow 2021-02-20 21:39:36 -07:00
a5e002f61c Fix flank auth in GitHub workflow 2021-02-20 21:36:07 -07:00
29f26265f8 Fix path for GCLOUD_DIR in GitHub workflow 2021-02-20 21:13:36 -07:00
027ae86953 Add flank/fladle to run UI tests 2021-02-20 21:07:16 -07:00
0028261847 Remove JCenter
JFrog has announced their intentions to remove JCenter so it has to go
2021-02-20 21:07:16 -07:00
e75e0a07d1
Add GitHub workflow to run tests on pushes to main 2021-02-20 21:02:20 -07:00
ed57785d0a Migrate autosave logic to viewmodel and fix failing UI tests 2021-02-20 20:35:28 -07:00
f2ed687b02 Update openApp test to use launch intent programmatically
Trying to go through the launcher was a bit too buggy and flaky. In the end, this should produce basically the same result.
2021-02-20 20:00:08 -07:00
6937cc7406 Bump dependency versions 2021-02-20 19:59:06 -07:00
72c2fb5719 Fix proguard rules for Crashlytics 2020-10-13 19:54:51 -07:00
84db6b3d80 Fix (yet another) potential issue with autosave overwriting files 2020-10-08 09:20:57 -07:00
2059d54b3b Bump version for release 2020-10-05 21:05:16 -07:00
0e7cdfa2b5 Fix an issue with deleting changes upon device rotation 2020-10-05 20:43:56 -07:00
1affb7069a (Hopefully) fix an issue with accidentally overwriting files with blank content upon 2020-09-22 13:40:56 -07:00
0ff5ccdbd6 Re-implement Crashlytics 2020-09-21 15:07:50 -07:00
f830657668 Bump version for release 2020-09-16 18:48:19 -07:00
ae673fc992 Fix navigation drawer handling on MainFragment 2020-09-16 18:46:51 -07:00
d854022691 Remove error handling entirely 2020-09-16 18:39:59 -07:00
c6728f1afa Add in-app reviews for play flavor
Signed-off-by: William Brawner <me@wbrawner.com>
2020-08-31 19:09:20 -07:00
76c45bb914 Bump dependency versions
Signed-off-by: William Brawner <me@wbrawner.com>
2020-08-31 19:09:20 -07:00
c16ed3cbbe Add some keyboard shortcuts 2020-08-23 05:02:36 -07:00
a1e114b162 Bump version for release
Signed-off-by: William Brawner <me@wbrawner.com>
2020-08-22 22:19:13 -07:00
fa11c7d070 Update gitignore files
Signed-off-by: William Brawner <me@wbrawner.com>
2020-08-22 22:18:06 -07:00
56f2efbb18 Fix losing data on screen rotation and weird behavior with loading files
Signed-off-by: William Brawner <me@wbrawner.com>
2020-08-22 22:16:37 -07:00
894fea4193 Add back samsung build flavor
This partially reverts commit 16185243

Signed-off-by: William Brawner <me@wbrawner.com>
2020-08-22 18:39:28 -07:00
baacef8f65 Update icon to use SVG for foreground instead of PNG
Signed-off-by: William Brawner <me@wbrawner.com>
2020-08-18 14:26:08 -07:00
8cf07491cf Re-implement ACRA
This reverts commit 0e70364c6e.

Signed-off-by: William Brawner <me@wbrawner.com>
2020-08-18 14:26:08 -07:00
3b984e4b73 Bump AGP version 2020-08-18 01:42:45 -07:00
173 changed files with 4570 additions and 2682 deletions

View file

@ -0,0 +1,70 @@
name: Build & Test
on:
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
name: Validate
steps:
- uses: actions/checkout@v4
- name: set up JDK
uses: https://git.wbrawner.com/actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Validate Gradle Wrapper
uses: https://git.wbrawner.com/gradle/actions/wrapper-validation@v4
unit_tests:
name: Run Unit Tests
runs-on: ubuntu-latest
needs:
- validate
steps:
- uses: actions/checkout@v4
- name: set up JDK
uses: https://git.wbrawner.com/actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Setup Android SDK
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
run: ./gradlew check
- name: Publish JUnit Results
uses: actions/upload-artifact@v3
if: always()
with:
name: Unit Test Results
path: "*/build/reports/*"
if-no-files-found: error
ui_tests:
runs-on: ubuntu-latest
name: Run UI Tests
needs:
- unit_tests
steps:
- uses: actions/checkout@v4
- name: set up JDK
uses: https://git.wbrawner.com/actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Setup Android SDK
uses: https://git.wbrawner.com/android-actions/setup-android@v3
- name: Setup Gradle
uses: https://git.wbrawner.com/gradle/actions/setup-gradle@v4
- name: Build APKs
run: ./gradlew assemblePlayDebug assemblePlayDebugAndroidTest
- name: Grant execute permission for flank_auth.sh
run: chmod +x flank_auth.sh
- name: Add auth for flank
env:
GCLOUD_KEY: ${{ secrets.GCLOUD_KEY }}
run: |
./flank_auth.sh
- name: Run UI tests
run: ./gradlew runFlank

77
.github/workflows/android.yml vendored Normal file
View file

@ -0,0 +1,77 @@
name: Android CI
on:
pull_request:
push:
branches: [ main ]
jobs:
validate:
runs-on: ubuntu-latest
name: Validate
steps:
- uses: actions/checkout@v4
- name: set up JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v3
- name: Enable auto-merge
if: ${{ github.event_name == 'pull_request' && github.actor == 'dependabot' }}
run: gh pr merge --auto --rebase "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
unit_test:
name: Run Unit Tests
runs-on: ubuntu-latest
needs:
- validate
steps:
- uses: actions/checkout@v4
- name: set up JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Run unit tests
uses: gradle/gradle-build-action@v3
with:
arguments: testPlayDebugUnitTest
- name: Publish JUnit Results
uses: dorny/test-reporter@v1
if: always()
with:
name: Unit Test Results
path: "*/build/test-results/*/*.xml"
reporter: java-junit
fail-on-error: true
ui_tests:
runs-on: ubuntu-latest
name: Run UI Tests
needs:
- validate
steps:
- uses: actions/checkout@v4
- name: set up JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Build with Gradle
uses: gradle/gradle-build-action@v3
with:
arguments: assemblePlayDebug assemblePlayDebugAndroidTest
- name: Grant execute permission for flank_auth.sh
run: chmod +x flank_auth.sh
- name: Add auth for flank
env:
GCLOUD_KEY: ${{ secrets.GCLOUD_KEY }}
run: |
./flank_auth.sh
- name: Run UI tests
uses: gradle/gradle-build-action@v3
with:
arguments: runFlank

1
.gitignore vendored
View file

@ -12,4 +12,3 @@
*.log *.log
keystore.properties keystore.properties
*.jks *.jks
sentry.properties

View file

@ -1,4 +1,4 @@
image: openjdk:8-jdk image: openjdk:24-jdk
variables: variables:
ANDROID_COMPILE_SDK: "28" ANDROID_COMPILE_SDK: "28"

View file

@ -1,44 +1,36 @@
# [Simple Markdown](https://wbrawner.com/portfolio/simple-markdown/) # Simple Markdown
[![pipeline status](https://gitlab.com/billybrawner/SimpleMarkdown/badges/master/pipeline.svg)](https://gitlab.com/billybrawner/SimpleMarkdown/commits/master) [![pipeline status](https://github.com/wbrawner/SimpleMarkdown/actions/workflows/android.yml/badge.svg)](https://github.com/wbrawner/SimpleMarkdown/actions/workflows/android.yml)
[![coverage report](https://gitlab.com/billybrawner/SimpleMarkdown/badges/master/coverage.svg)](https://gitlab.com/billybrawner/SimpleMarkdown/commits/master)
Simple Markdown is simply a Markdown editor :) I wrote it to offer up an open source alternative to <p>
the other Markdown editors available on the Play Store. I also wanted to get some practice in <img alt="" src="./fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" style="width: 24%" />
creating Android apps and have a little something to put into my portfolio. <img alt="" src="./fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" style="width: 24%" />
<img alt="" src="./fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" style="width: 24%" />
<img alt="" src="./fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" style="width: 24%" />
</p>
## Roadmap Simple Markdown is an open source Markdown editor.
* [x] Auto-save <a href='https://play.google.com/store/apps/details?id=com.wbrawner.simplemarkdown&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' height="80"/></a>
* [x] Night mode [<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
* [x] Save to cloud (Dropbox, Google Drive, OneDrive) alt="Get it on F-Droid"
* [x] Custom CSS for Markdown preview height="80">](https://f-droid.org/packages/com.wbrawner.simplemarkdown.free/)
* [ ] Better insert for tables/images/links
* [ ] Quick-insert toolbar for common Markdown syntax characters
* [ ] Auto-scroll preview to match edit view in landscape mode
* [ ] Disable live preview in landscape mode
* [ ] Disable preview tab for better performance in large files
## Building ## Building
Using Android Studio is the preferred way to build the project. To build from the command line, you can run Using Android Studio is the preferred way to build the project. To build from the command line, you can run
./gradlew assembleDebug ./gradlew assembleFreeDebug
### Crashlytics ### Crashlytics
SimpleMarkdown makes use of Firebase Crashlytics for error reporting. You'll need to follow the SimpleMarkdown makes use of Firebase Crashlytics for error reporting. You'll need to follow the
[Get started with Firebase Crashlytics](https://firebase.google.com/docs/crashlytics/get-started?platform=android) guide in order to build the project. [Get started with Firebase Crashlytics](https://firebase.google.com/docs/crashlytics/get-started?platform=android) guide in order to build the project.
## Contributing
I'd love any contributions, particularly in improving the existing code. Please just fork the
repository, make your changes, squash your commits, and submit a pull request :)
## License ## License
``` ```
Copyright 2017-2019 William Brawner Copyright 2017-2022 William Brawner
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

1
app/.gitignore vendored
View file

@ -1,3 +1,4 @@
/build /build
*.apk *.apk
*.aab
/release /release

View file

@ -1,166 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
apply plugin: 'io.sentry.android.gradle'
def keystoreProperties = new Properties()
try {
def keystorePropertiesFile = rootProject.file("keystore.properties")
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
} catch (FileNotFoundException ignored) {
logger.warn("Unable to load keystore properties. Automatic signing won't be available")
keystoreProperties['keyAlias'] = ""
keystoreProperties['keyPassword'] = ""
keystoreProperties['storeFile'] = File.createTempFile("temp", ".tmp").absolutePath
keystoreProperties['storePassword'] = ""
}
android {
configurations.all {
resolutionStrategy.force 'com.google.code.findbugs:jsr305:3.0.1'
}
packagingOptions {
exclude 'META-INF/LICENSE-LGPL-2.1.txt'
exclude 'META-INF/LICENSE-LGPL-3.txt'
exclude 'META-INF/LICENSE-W3C-TEST'
exclude 'META-INF/LICENSE'
exclude 'META-INF/DEPENDENCIES'
}
compileSdkVersion 29
buildToolsVersion '28.0.3'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
defaultConfig {
applicationId "com.wbrawner.simplemarkdown"
minSdkVersion 21
targetSdkVersion 29
versionCode 27
versionName "0.8.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
sentryDsn: "https://399270639b2e4b10a028a2be9192d1d3@sentry.brawner.dev/2"
]
buildConfigField "boolean", "ENABLE_CUSTOM_CSS", "false"
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
debug {
applicationIdSuffix ".debug"
testCoverageEnabled true
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
buildConfigField "boolean", "ENABLE_CUSTOM_CSS", "false"
}
}
flavorDimensions "freedom"
productFlavors {
play {}
free {
applicationIdSuffix ".free"
versionNameSuffix "-free"
}
}
dexOptions {
jumboMode true
}
testOptions {
unitTests {
includeAndroidResources = true
}
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
}
dependencies {
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:4.2.1'
implementation fileTree(include: ['*.jar'], dir: 'libs')
def espresso_version = '3.2.0'
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
androidTestImplementation "androidx.test.espresso:espresso-web:$espresso_version"
androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version"
def android_test = '1.2.0'
androidTestImplementation "androidx.test:runner:$android_test"
androidTestImplementation "androidx.test:rules:$android_test"
androidTestUtil "androidx.test:orchestrator:$android_test"
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference:1.1.1'
implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'com.commonsware.cwac:anddown:0.3.0'
playImplementation 'com.android.billingclient:billing:3.0.0'
playImplementation 'com.google.firebase:firebase-core:17.4.3'
implementation "androidx.core:core-ktx:1.3.0"
implementation 'androidx.browser:browser:1.2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'io.sentry:sentry-android:2.1.6'
def coroutines_version = "1.3.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation 'eu.crydee:syllable-counter:4.0.2'
}
android.productFlavors.each { flavor ->
if (getGradle().getStartParameter().getTaskRequests().toString().toLowerCase().contains(flavor.name)
&& flavor.name == 'play') {
apply plugin: 'com.google.gms.google-services'
}
}
repositories {
mavenCentral()
jcenter()
}
jacoco {
toolVersion = '0.8.0'
}
tasks.withType(Test) {
jacoco.includeNoLocationClasses = true
}
task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {
reports {
xml.enabled = true
html.enabled = true
}
def fileFilter = [ '**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*' ]
def javaDebugTree = fileTree(dir: "$project.buildDir/intermediates/javac/debug/compileDebugJavaWithJavac/classes", excludes: fileFilter)
def kotlinDebugTree = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/debug", excludes: fileFilter)
def mainSrc = "$project.projectDir/src/main/java"
sourceDirectories.setFrom(files([mainSrc]))
classDirectories.setFrom(files([javaDebugTree, kotlinDebugTree]))
executionData.setFrom(fileTree(dir: project.buildDir, includes: [
'jacoco/testDebugUnitTest.exec',
'outputs/code-coverage/connected/*coverage.ec'
]))
}

201
app/build.gradle.kts Normal file
View file

@ -0,0 +1,201 @@
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.fladle)
alias(libs.plugins.triplet.play)
id("com.wbrawner.releasehelper")
}
val keystoreProperties = Properties()
try {
val keystorePropertiesFile = rootProject.file("keystore.properties")
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
} catch (ignored: FileNotFoundException) {
logger.warn("Unable to load keystore properties. Automatic signing won't be available")
keystoreProperties["keyAlias"] = ""
keystoreProperties["keyPassword"] = ""
keystoreProperties["storeFile"] = File.createTempFile("temp", ".tmp").absolutePath
keystoreProperties["storePassword"] = ""
keystoreProperties["publishCredentialsFile"] = ""
}
android {
packaging {
resources {
excludes += listOf(
"META-INF/LICENSE-LGPL-2.1.txt",
"META-INF/LICENSE-LGPL-3.txt",
"META-INF/LICENSE-W3C-TEST",
"META-INF/LICENSE",
"META-INF/DEPENDENCIES"
)
}
}
compileSdk = libs.versions.maxSdk.get().toInt()
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
defaultConfig {
applicationId = "com.wbrawner.simplemarkdown"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.maxSdk.get().toInt()
versionCode = 45
versionName = "2024.10.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
buildConfigField("boolean", "ENABLE_CUSTOM_CSS", "true")
}
signingConfigs {
create("playRelease") {
keyAlias = keystoreProperties["keyAlias"].toString()
keyPassword = keystoreProperties["keyPassword"].toString()
storeFile = file(keystoreProperties["storeFile"].toString())
storePassword = keystoreProperties["storePassword"].toString()
}
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
buildConfigField("boolean", "ENABLE_CUSTOM_CSS", "false")
}
}
flavorDimensions.add("platform")
productFlavors {
create("free") {
applicationIdSuffix = ".free"
versionNameSuffix = "-free"
}
create("play") {
signingConfig = signingConfigs["playRelease"]
}
}
testOptions {
unitTests {
isIncludeAndroidResources = true
}
execution = "ANDROIDX_TEST_ORCHESTRATOR"
}
namespace = "com.wbrawner.simplemarkdown"
buildFeatures {
compose = true
}
playConfigs {
register("play") {
enabled.set(true)
commit.set(true)
}
}
lint {
disable += listOf(
"AndroidGradlePluginVersion",
"GradleDependency",
"ObsoleteLintCustomCheck"
)
warningsAsErrors = true
}
}
play {
commit.set(false)
enabled.set(false)
track.set("production")
defaultToAppBundles.set(true)
(keystoreProperties["publishCredentialsFile"] as? String)?.ifBlank { null }?.let {
serviceAccountCredentials.set(file(it))
}
}
dependencies {
"freeImplementation"(project(":free"))
"playImplementation"(project(":non-free"))
implementation(libs.androidx.material3.windowsizeclass)
implementation(libs.androidx.navigation.compose)
testImplementation(libs.junit)
testRuntimeOnly(libs.robolectric)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.espresso.web)
androidTestImplementation(libs.androidx.espresso.intents)
androidTestRuntimeOnly(libs.androidx.runner)
androidTestUtil(libs.androidx.orchestrator)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.browser)
implementation(libs.commonmark)
implementation(libs.commonmark.ext.gfm.tables)
implementation(libs.commonmark.ext.gfm.strikethrough)
implementation(libs.commonmark.ext.autolink)
implementation(libs.commonmark.ext.task.list.items)
implementation(libs.commonmark.ext.yaml.front.matter)
implementation(libs.commonmark.ext.image.attributes)
implementation(libs.commonmark.ext.heading.anchor)
val composeBom = enforcedPlatform(libs.compose.bom)
implementation(composeBom)
androidTestImplementation(composeBom)
implementation(libs.androidx.runtime)
implementation(libs.androidx.ui)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.foundation)
implementation(libs.androidx.foundation.layout)
implementation(libs.androidx.ui.tooling)
implementation(libs.androidx.material3)
implementation(libs.androidx.material.icons.extended)
androidTestImplementation(libs.androidx.ui.test.junit4)
runtimeOnly(libs.kotlinx.coroutines.android)
testImplementation(libs.kotlinx.coroutines.test)
implementation(libs.syllable.counter)
androidTestImplementation(libs.androidx.ui.test)
androidTestImplementation(libs.androidx.core)
androidTestImplementation(libs.androidx.monitor)
androidTestImplementation(libs.junit)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.animation.core)
implementation(libs.androidx.animation)
implementation(libs.androidx.material.icons.core)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.text)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.ui.unit)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.navigation.common)
implementation(libs.androidx.navigation.runtime.ktx)
implementation(libs.androidx.preference.ktx)
implementation(libs.acra.core)
implementation(libs.timber)
implementation(libs.kotlinx.coroutines.core)
implementation(project(":core"))
}
fladle {
variant.set("playDebug")
useOrchestrator.set(true)
environmentVariables.put("clearPackageData", "true")
testTimeout.set("7m")
devices.add(
mapOf("model" to "Pixel2.arm", "version" to "33")
)
projectId.set("simplemarkdown")
}
tasks.register<Exec>("pullLogFiles") {
commandLine = listOf(
"adb", "pull",
"/storage/emulated/0/Android/data/com.wbrawner.simplemarkdown/files/logs"
)
}

View file

@ -1,78 +0,0 @@
{
"project_info": {
"project_number": "318641233555",
"firebase_url": "https://simplemarkdown.firebaseio.com",
"project_id": "simplemarkdown",
"storage_bucket": "simplemarkdown.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:318641233555:android:09d865cad87e3b5f",
"android_client_info": {
"package_name": "com.wbrawner.simplemarkdown"
}
},
"oauth_client": [
{
"client_id": "318641233555-83n2k1mqhokf0b7lhccqiva9pspgripq.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.wbrawner.simplemarkdown",
"certificate_hash": "710e37c230689bc259fc6e2e032e2f56956f8d33"
}
},
{
"client_id": "318641233555-a5rm2cqqf66jc9j5nmg3ttepdt2iaeno.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBDMcXg-10NsXLDKJRtj5WnXoHrwg3m9Os"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "318641233555-a5rm2cqqf66jc9j5nmg3ttepdt2iaeno.apps.googleusercontent.com",
"client_type": 3
}
]
}
},
"admob_app_id": "ca-app-pub-3319579963502409~4576405307"
},
{
"client_info": {
"mobilesdk_app_id": "1:318641233555:android:5dfb62206717437e",
"android_client_info": {
"package_name": "com.wbrawner.simplemarkdown.samsung"
}
},
"oauth_client": [
{
"client_id": "318641233555-a5rm2cqqf66jc9j5nmg3ttepdt2iaeno.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBDMcXg-10NsXLDKJRtj5WnXoHrwg3m9Os"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "318641233555-a5rm2cqqf66jc9j5nmg3ttepdt2iaeno.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View file

@ -2,7 +2,7 @@
# By default, the flags in this file are appended to flags specified # By default, the flags in this file are appended to flags specified
# in /home/billy/Android/Sdk/tools/proguard/proguard-android.txt # in /home/billy/Android/Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles # You can edit the include path and order by changing the proguardFiles
# directive in build.gradle. # directive in build.gradle.kts.
# #
# For more details, see # For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html # http://developer.android.com/guide/developing/tools/proguard.html
@ -27,4 +27,15 @@
### Crashlytics ### ### Crashlytics ###
-keepattributes *Annotation* -keepattributes *Annotation*
-keepattributes SourceFile,LineNumberTable -keepattributes SourceFile,LineNumberTable
### End Crashlytics ### -keep public class * extends java.lang.Exception
### Crashlytics ###
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.conscrypt.Conscrypt$Version
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.conscrypt.ConscryptHostnameVerifier
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE

View file

@ -1,182 +0,0 @@
package com.wbrawner.simplemarkdown
import android.app.Activity.RESULT_OK
import android.app.Instrumentation
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.net.Uri
import android.view.KeyEvent
import androidx.core.content.FileProvider
import androidx.test.InstrumentationRegistry
import androidx.test.InstrumentationRegistry.getInstrumentation
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.rule.IntentsTestRule
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.uiautomator.UiDevice
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import com.wbrawner.simplemarkdown.view.activity.MainActivity
import org.hamcrest.Matchers.containsString
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.io.File
import java.io.Reader
class MarkdownTests {
@get:Rule
var activityRule = IntentsTestRule(MainActivity::class.java, false, false)
lateinit var file: File
@Before
fun setup() {
file = File(getApplicationContext<Context>().filesDir.absolutePath + "/tmp", "temp.md")
file.parentFile?.mkdirs()
file.delete()
activityRule.launchActivity(null)
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
@Test
@Throws(Exception::class)
fun openAppTest() {
val mDevice = UiDevice.getInstance(getInstrumentation())
mDevice.pressHome()
// Bring up the default launcher by searching for a UI component
// that matches the content description for the launcher button.
val allAppsButton = mDevice
.findObject(UiSelector().description("Apps"))
// Perform a click on the button to load the launcher.
allAppsButton.clickAndWaitForNewWindow()
// Context of the app under test.
val appContext = InstrumentationRegistry.getTargetContext()
assertEquals("com.wbrawner.simplemarkdown", appContext.packageName)
val appView = UiScrollable(UiSelector().scrollable(true))
val simpleMarkdownSelector = UiSelector()
.text(getApplicationContext<Context>().getString(R.string.app_name_short))
appView.scrollIntoView(simpleMarkdownSelector)
mDevice.findObject(simpleMarkdownSelector).clickAndWaitForNewWindow()
}
@Test
fun editAndPreviewMarkdownTest() {
onView(withId(R.id.markdown_edit)).perform(typeText("# Header test"))
onView(withText(R.string.action_preview)).perform(click())
onWebView(withId(R.id.markdown_view)).forceJavascriptEnabled()
.withElement(findElement(Locator.TAG_NAME, "h1"))
.check(webMatches(getText(), containsString("Header test")))
}
@Test
fun newMarkdownTest() {
onView(withId(R.id.markdown_edit))
.perform(typeText("# UI Testing\n\nThe quick brown fox jumped over the lazy dog."))
openActionBarOverflowOrOptionsMenu(getApplicationContext())
onView(withText(R.string.action_new)).perform(click())
onView(withText(R.string.action_discard)).perform(click())
onView(withId(R.id.markdown_edit)).check(matches(withText("")))
}
@Test
fun saveMarkdownWithFileUriTest() {
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
onView(withId(R.id.markdown_edit)).perform(typeText(markdownText))
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
data = Uri.fromFile(file)
})
intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult)
openActionBarOverflowOrOptionsMenu(getApplicationContext())
onView(withText(R.string.action_save_as)).perform(click())
Thread.sleep(500)
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
onView(withText("temp.md")).check(matches(withParent(withId(R.id.toolbar))))
}
@Test
fun saveMarkdownWithContentUriTest() {
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
onView(withId(R.id.markdown_edit)).perform(typeText(markdownText))
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
data = FileProvider.getUriForFile(getApplicationContext(), "com.wbrawner.simplemarkdown.fileprovider", file)
})
intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult)
openActionBarOverflowOrOptionsMenu(getApplicationContext())
onView(withText(R.string.action_save_as)).perform(click())
Thread.sleep(500)
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
onView(withText("temp.md")).check(matches(withParent(withId(R.id.toolbar))))
}
@Test
fun loadMarkdownWithFileUriTest() {
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
file.outputStream().writer().use { it.write(markdownText) }
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
data = Uri.fromFile(file)
})
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
openActionBarOverflowOrOptionsMenu(getApplicationContext())
onView(withText(R.string.action_open)).perform(click())
Thread.sleep(500)
onView(withId(R.id.markdown_edit)).check(matches(withText(markdownText)))
onView(withText("temp.md")).check(matches(withParent(withId(R.id.toolbar))))
}
@Test
fun loadMarkdownWithContentUriTest() {
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
file.outputStream().writer().use { it.write(markdownText) }
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
data = FileProvider.getUriForFile(getApplicationContext(), "com.wbrawner.simplemarkdown.fileprovider", file)
})
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
openActionBarOverflowOrOptionsMenu(getApplicationContext())
onView(withText(R.string.action_open)).perform(click())
Thread.sleep(500)
onView(withId(R.id.markdown_edit)).check(matches(withText(markdownText)))
onView(withText("temp.md")).check(matches(withParent(withId(R.id.toolbar))))
}
@Test
fun openEditAndSaveMarkdownTest() {
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
file.outputStream().writer().use { it.write(markdownText) }
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
data = Uri.fromFile(file)
})
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
openActionBarOverflowOrOptionsMenu(getApplicationContext())
onView(withText(R.string.action_open)).perform(click())
Thread.sleep(500)
onView(withId(R.id.markdown_edit)).check(matches(withText(markdownText)))
onView(withText("temp.md")).check(matches(withParent(withId(R.id.toolbar))))
val additionalText = "# More info\n\nThis is some additional text"
onView(withId(R.id.markdown_edit)).perform(
clearText(),
typeText(additionalText)
)
openActionBarOverflowOrOptionsMenu(getApplicationContext())
onView(withText(R.string.action_save)).perform(click())
Thread.sleep(500)
onView(withText(getApplicationContext<Context>().getString(R.string.file_saved, "temp.md")))
assertEquals(additionalText, file.inputStream().reader().use(Reader::readText))
onView(withText("temp.md")).check(matches(withParent(withId(R.id.toolbar))))
}
}

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

@ -0,0 +1,254 @@
package com.wbrawner.simplemarkdown
import android.app.Activity.RESULT_OK
import android.app.Instrumentation
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.core.content.FileProvider
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.rule.IntentsRule
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.wbrawner.simplemarkdown.robot.onMainScreen
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.io.File
import java.io.Reader
class MarkdownTests {
@get:Rule
val composeRule = createEmptyComposeRule()
@get:Rule
val intentsRule = IntentsRule()
private lateinit var file: File
@Before
fun setup() {
file = File(getApplicationContext<Context>().filesDir.absolutePath + "/tmp", "temp.md")
assertTrue(requireNotNull(file.parentFile).mkdirs())
file.delete()
}
@Test
@Throws(Exception::class)
fun openAppTest() {
val context = getInstrumentation().targetContext
context.packageManager
.getLaunchIntentForPackage(context.packageName)
.apply { context.startActivity(this) }
}
@Test
fun editAndPreviewMarkdownTest() {
ActivityScenario.launch(MainActivity::class.java)
onMainScreen(composeRule) {
typeMarkdown("# Header test")
checkMarkdownEquals("# Header test")
openPreview()
} onPreview {
verifyH1("Header test")
}
}
@Test
fun openThenNewMarkdownTest() {
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
file.outputStream().writer().use { it.write(markdownText) }
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
data = Uri.fromFile(file)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
})
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
ActivityScenario.launch(MainActivity::class.java)
onMainScreen(composeRule) {
openMenu()
clickOpenMenuItem()
checkMarkdownEquals(markdownText)
openMenu()
clickNewMenuItem()
verifyDialogIsNotShown()
checkMarkdownEquals("")
}
}
@Test
fun editThenNewMarkdownTest() {
ActivityScenario.launch(MainActivity::class.java)
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
onMainScreen(composeRule) {
typeMarkdown(markdownText)
openMenu()
clickNewMenuItem()
verifyDialogIsShown("Would you like to save your changes?")
discardChanges()
checkMarkdownEquals("")
}
}
@Test
fun saveMarkdownWithFileUriTest() = runTest {
ActivityScenario.launch(MainActivity::class.java)
onMainScreen(composeRule) {
checkTitleEquals("Untitled.md")
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
typeMarkdown(markdownText)
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
data = Uri.fromFile(file)
})
intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult)
openMenu()
clickSaveMenuItem()
awaitIdle()
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
checkTitleEquals("temp.md")
}
}
@Test
fun saveMarkdownWithContentUriTest() = runTest {
ActivityScenario.launch(MainActivity::class.java)
onMainScreen(composeRule) {
checkTitleEquals("Untitled.md")
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
typeMarkdown(markdownText)
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
data = FileProvider.getUriForFile(
getApplicationContext(),
"${BuildConfig.APPLICATION_ID}.fileprovider",
file
)
})
intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult)
openMenu()
clickSaveMenuItem()
awaitIdle()
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
checkTitleEquals("temp.md")
}
}
@Test
fun loadMarkdownWithFileUriTest() = runTest {
ActivityScenario.launch(MainActivity::class.java)
onMainScreen(composeRule) {
checkTitleEquals("Untitled.md")
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
file.outputStream().writer().use { it.write(markdownText) }
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
data = Uri.fromFile(file)
})
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
openMenu()
clickOpenMenuItem()
awaitIdle()
checkMarkdownEquals(markdownText)
checkTitleEquals("temp.md")
}
}
@Test
fun loadMarkdownWithContentUriTest() = runTest {
ActivityScenario.launch(MainActivity::class.java)
onMainScreen(composeRule) {
checkTitleEquals("Untitled.md")
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
file.outputStream().writer().use { it.write(markdownText) }
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
data = FileProvider.getUriForFile(
getApplicationContext(),
"${BuildConfig.APPLICATION_ID}.fileprovider",
file
)
})
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
openMenu()
clickOpenMenuItem()
awaitIdle()
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
fun openEditAndSaveMarkdownTest() = runTest {
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
file.outputStream().writer().use { it.write(markdownText) }
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
data = Uri.fromFile(file)
})
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
ActivityScenario.launch(MainActivity::class.java)
onMainScreen(composeRule) {
checkTitleEquals("Untitled.md")
openMenu()
clickOpenMenuItem()
awaitIdle()
verifyTextIsShown("Successfully loaded temp.md")
checkMarkdownEquals(markdownText)
checkTitleEquals("temp.md")
val additionalText = "# More info\n\nThis is some additional text"
typeMarkdown(additionalText)
openMenu()
clickSaveMenuItem()
awaitIdle()
verifyTextIsShown("Successfully saved temp.md")
assertEquals(additionalText, file.inputStream().reader().use(Reader::readText))
checkTitleEquals("temp.md")
}
}
@Test
fun editAndViewHelpMarkdownTest() = runTest {
ActivityScenario.launch(MainActivity::class.java)
onMainScreen(composeRule) {
checkTitleEquals("Untitled.md")
typeMarkdown("# Header test")
checkMarkdownEquals("# Header test")
openDrawer()
} onNavigationDrawer {
openHelpPage()
} onHelpScreen {
checkTitleEquals("Help")
verifyH1("Headings/Titles")
pressBack()
} onMainScreen {
checkMarkdownEquals("# Header test")
}
}
}

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

@ -1,9 +0,0 @@
package com.wbrawner.simplemarkdown.utility
import android.app.Activity
import androidx.lifecycle.MutableLiveData
import com.google.android.material.button.MaterialButton
class SupportLinkProvider(@Suppress("unused") private val activity: Activity) {
val supportLinks = MutableLiveData<List<MaterialButton>>()
}

View file

@ -1,26 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="com.wbrawner.simplemarkdown">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING" />
<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"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/Theme.App.Starting"
tools:ignore="AllowBackup" tools:ignore="AllowBackup"
tools:targetApi="n"> tools:targetApi="tiramisu">
<activity <activity android:name=".MainActivity"
android:name=".view.activity.SplashActivity" android:exported="true"
android:theme="@style/AppTheme.Splash"
android:label="@string/app_name_short"> android:label="@string/app_name_short">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -42,7 +39,7 @@
<data android:host="*" /> <data android:host="*" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".view.activity.MainActivity" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.fileprovider"
@ -52,13 +49,6 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<meta-data
android:name="io.sentry.dsn"
android:value="${sentryDsn}" />
<meta-data
android:name="io.sentry.auto-init"
android:value="false" />
</application> </application>
</manifest> </manifest>

View file

@ -90,11 +90,11 @@ data 1|data 2|data 3
data 1|data 2|data 3 data 1|data 2|data 3
``` ```
Left Content|Center Content|Right Content | Left Content | Center Content | Right Content |
:--------|:--------:|--------: |:-------------|:--------------:|--------------:|
data 1|data 2|data 3 | data 1 | data 2 | data 3 |
data 1|data 2|data 3 | data 1 | data 2 | data 3 |
data 1|data 2|data 3 | data 1 | data 2 | data 3 |
### Images ### Images
@ -116,7 +116,7 @@ In addition to the monospace inline element, code blocks can be created by inden
Or by wrapping the code in three backticks (\`\`\`): Or by wrapping the code in three backticks (\`\`\`):
``` ```javascript
function helloWorld() { function helloWorld() {
console.log("Hello, world!") console.log("Hello, world!")
} }

View file

@ -1,10 +1,10 @@
First and foremost, Simple Markdown DOES NOT collect any personally identifiable information. The The internet access permission is requested primarily for retrieving images from the internet in
internet access permission is requested primarily for retrieving images from the internet in
case you embed them in your markdown, but it also allows me to send automated error and crash case you embed them in your markdown, but it also allows me to send automated error and crash
reports to myself whenever the app runs into an issue. These automated reports are powered by my own reports to myself whenever the app runs into an issue. These error reports are opt-out, and are
self-hosted version of [Sentry] (https://sentry.io/), which is a free and open source error powered by [Firebase Crashlytics](https://firebase.google.com/docs/crashlytics/), which is a
reporting solution. These error reports are used exclusively for fixing problems that occur while free error reporting solution provided by Google. These error reports are used exclusively for
you're using the app. For more information on the kinds of data that may be sent in these automated fixing problems that occur while you're using the app, along with some analytics info like how
error reports, please see the [relevant documentation](https://docs.sentry.io/platforms/android/#context) long you use the app for, how often, and which features of the app you use. This helps me to
on Sentry's website. If you would like to opt-out of these error reports, please visit the in-app determine how to spend my very limited time on building out new features. I'll have to defer to
settings page to disable the toggle for error reports. [Google's Privacy Policy](https://policies.google.com/privacy) to explain how they handle the
data. As for me, I don't knowingly or willingly sell or share your data.

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,187 @@
package com.wbrawner.simplemarkdown
import android.app.ComponentCaller
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.EaseIn
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.scaleOut
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.PrivacyTip
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.core.app.ActivityCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.wbrawner.simplemarkdown.MarkdownApplication.Companion.fileHelper
import com.wbrawner.simplemarkdown.MarkdownApplication.Companion.preferenceHelper
import com.wbrawner.simplemarkdown.ui.MainScreen
import com.wbrawner.simplemarkdown.ui.MarkdownInfoScreen
import com.wbrawner.simplemarkdown.ui.SettingsScreen
import com.wbrawner.simplemarkdown.ui.SupportScreen
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
import com.wbrawner.simplemarkdown.utility.Preference
import kotlinx.coroutines.launch
import org.acra.ACRA
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback {
private val viewModel: MarkdownViewModel by viewModels {
MarkdownViewModel.factory(
fileHelper,
preferenceHelper
)
}
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
val autosaveEnabled by preferenceHelper.observe<Boolean>(Preference.AUTOSAVE_ENABLED)
.collectAsState()
val darkModePreference by preferenceHelper.observe<String>(Preference.DARK_MODE)
.collectAsState()
LaunchedEffect(darkModePreference) {
val darkMode = when {
darkModePreference.equals(
getString(R.string.pref_value_light),
ignoreCase = true
) -> AppCompatDelegate.MODE_NIGHT_NO
darkModePreference.equals(
getString(R.string.pref_value_dark),
ignoreCase = true
) -> AppCompatDelegate.MODE_NIGHT_YES
else -> {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
} else {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
}
}
AppCompatDelegate.setDefaultNightMode(darkMode)
}
val errorReporterPreference by preferenceHelper.observe<Boolean>(Preference.ERROR_REPORTS_ENABLED)
.collectAsState()
LaunchedEffect(errorReporterPreference) {
ACRA.errorReporter.setEnabled(errorReporterPreference)
}
val intentData = remember(intent) { intent?.data }
LaunchedEffect(intentData) {
viewModel.load(intentData?.toString())
}
val windowSizeClass = calculateWindowSizeClass(this)
SimpleMarkdownTheme {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Route.EDITOR.path,
enterTransition = {
fadeIn(
animationSpec = tween(
300, easing = LinearEasing
)
) + slideIntoContainer(
animationSpec = tween(300, easing = EaseIn),
towards = AnimatedContentTransitionScope.SlideDirection.Start
)
},
popEnterTransition = { fadeIn() },
popExitTransition = {
scaleOut(targetScale = 0.9f) + slideOutOfContainer(
animationSpec = tween(300, easing = EaseIn),
towards = AnimatedContentTransitionScope.SlideDirection.End
)
}
) {
composable(Route.EDITOR.path) {
MainScreen(
navController = navController,
viewModel = viewModel,
enableWideLayout = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded,
enableAutosave = autosaveEnabled,
)
}
composable(Route.SETTINGS.path) {
SettingsScreen(navController = navController, preferenceHelper)
}
composable(Route.SUPPORT.path) {
SupportScreen(navController = navController)
}
composable(Route.HELP.path) {
MarkdownInfoScreen(
title = stringResource(Route.HELP.title),
file = "Cheatsheet.md",
navController = navController
)
}
composable(Route.ABOUT.path) {
MarkdownInfoScreen(
title = stringResource(Route.ABOUT.title),
file = "Libraries.md",
navController = navController
)
}
composable(Route.PRIVACY.path) {
MarkdownInfoScreen(
title = stringResource(Route.PRIVACY.title),
file = "Privacy Policy.md",
navController = navController
)
}
}
}
}
}
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
super.onNewIntent(intent, caller)
lifecycleScope.launch {
intent.data?.let {
viewModel.load(it.toString())
}
}
}
}
enum class Route(
val path: String,
@StringRes
val title: Int,
val icon: ImageVector
) {
EDITOR("/", R.string.title_editor, Icons.Default.Edit),
SETTINGS("/settings", R.string.title_settings, Icons.Default.Settings),
SUPPORT("/support", R.string.support_title, Icons.Default.Favorite),
HELP("/help", R.string.title_help, Icons.AutoMirrored.Filled.Help),
ABOUT("/about", R.string.title_about, Icons.Default.Info),
PRIVACY("/privacy", R.string.action_privacy, Icons.Default.PrivacyTip),
}

View file

@ -2,19 +2,24 @@ package com.wbrawner.simplemarkdown
import android.app.Application import android.app.Application
import android.os.StrictMode import android.os.StrictMode
import androidx.preference.PreferenceManager import com.wbrawner.simplemarkdown.core.ErrorReporterTree
import com.wbrawner.simplemarkdown.utility.ErrorHandler import com.wbrawner.simplemarkdown.utility.AndroidFileHelper
import com.wbrawner.simplemarkdown.utility.SentryErrorHandler import com.wbrawner.simplemarkdown.utility.AndroidPreferenceHelper
import com.wbrawner.simplemarkdown.utility.FileHelper
import com.wbrawner.simplemarkdown.utility.PersistentTree
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
import com.wbrawner.simplemarkdown.utility.ReviewHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
class MarkdownApplication : Application() { class MarkdownApplication : Application() {
val errorHandler: ErrorHandler by lazy {
SentryErrorHandler() private val coroutineScope = CoroutineScope(Dispatchers.Default)
}
override fun onCreate() { override fun onCreate() {
val enableErrorReports = PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(getString(R.string.error_reports_enabled), true)
errorHandler.init(this, enableErrorReports)
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll() .detectAll()
@ -24,7 +29,28 @@ class MarkdownApplication : Application() {
.detectAll() .detectAll()
.penaltyLog() .penaltyLog()
.build()) .build())
Timber.plant(Timber.DebugTree())
coroutineScope.launch {
try {
Timber.plant(PersistentTree.create(coroutineScope, File(getExternalFilesDir(null), "logs")))
} catch (e: Exception) {
Timber.e(e, "Unable to create PersistentTree")
}
}
}
coroutineScope.launch {
Timber.plant(ErrorReporterTree.create(this@MarkdownApplication))
} }
super.onCreate() super.onCreate()
ReviewHelper.init(this)
fileHelper = AndroidFileHelper(this)
preferenceHelper = AndroidPreferenceHelper(this)
}
companion object {
lateinit var fileHelper: FileHelper
private set
lateinit var preferenceHelper: PreferenceHelper
private set
} }
} }

View file

@ -0,0 +1,321 @@
package com.wbrawner.simplemarkdown
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.ViewModelProvider
import androidx.lifecycle.viewModelScope
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.Preference
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.net.URI
data class EditorState(
val fileName: String = "Untitled.md",
val markdown: TextFieldValue = TextFieldValue(""),
val path: URI? = null,
val toast: ParameterizedText? = null,
val alert: AlertDialogModel? = null,
val saveCallback: (() -> Unit)? = null,
val lockSwiping: Boolean = false,
val enableReadability: Boolean = false,
val initialMarkdown: String = "",
) {
val dirty: Boolean
get() = markdown.text != initialMarkdown
}
class MarkdownViewModel(
private val fileHelper: FileHelper,
private val preferenceHelper: PreferenceHelper
) : ViewModel() {
private val _state = MutableStateFlow(EditorState())
val state = _state.asStateFlow()
private val saveMutex = Mutex()
init {
preferenceHelper.observe<Boolean>(Preference.LOCK_SWIPING)
.onEach {
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)
}
fun updateMarkdown(markdown: String?) = updateMarkdown(TextFieldValue(markdown.orEmpty()))
fun updateMarkdown(markdown: TextFieldValue) {
updateState {
copy(
markdown = markdown.copy(annotatedString = markdown.text.annotate(enableReadability)),
)
}
}
fun dismissToast() {
updateState { copy(toast = null) }
}
fun dismissAlert() {
updateState { copy(alert = null) }
}
private fun unsetSaveCallback() {
updateState { copy(saveCallback = null) }
}
suspend fun load(loadPath: String?) {
saveMutex.withLock {
val actualLoadPath = loadPath
?.ifBlank { null }
?: preferenceHelper[Preference.AUTOSAVE_URI]
?.let {
val autosaveUri = it as? String
if (autosaveUri.isNullOrBlank()) {
preferenceHelper[Preference.AUTOSAVE_URI] = null
null
} else {
Timber.d("Using uri from shared preferences: $it")
autosaveUri
}
} ?: return
Timber.d("Loading file at $actualLoadPath")
try {
val uri = URI.create(actualLoadPath)
fileHelper.open(uri)
?.let { (name, content) ->
updateState {
copy(
path = uri,
fileName = name,
markdown = TextFieldValue(content),
initialMarkdown = content,
toast = ParameterizedText(R.string.file_loaded, arrayOf(name))
)
}
preferenceHelper[Preference.AUTOSAVE_URI] = actualLoadPath
} ?: throw IllegalStateException("Opened file was null")
} catch (e: Exception) {
Timber.e(LocalOnlyException(e), "Failed to open file at path: $actualLoadPath")
updateState {
copy(
alert = AlertDialogModel(
text = ParameterizedText(R.string.file_load_error),
confirmButton = AlertDialogModel.ButtonModel(
ParameterizedText(R.string.ok),
onClick = ::dismissAlert
)
)
)
}
}
}
}
suspend fun save(savePath: URI? = null, interactive: Boolean = true): Boolean =
saveMutex.withLock {
val actualSavePath = savePath
?: _state.value.path
?: run {
Timber.w("Attempted to save file with empty path")
if (interactive) {
updateState {
copy(saveCallback = ::unsetSaveCallback)
}
}
return@withLock false
}
try {
Timber.i("Saving file to $actualSavePath...")
val currentState = _state.value
val name = fileHelper.save(actualSavePath, currentState.markdown.text)
updateState {
currentState.copy(
fileName = name,
path = actualSavePath,
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("Persisting autosave uri in shared prefs: $actualSavePath")
preferenceHelper[Preference.AUTOSAVE_URI] = actualSavePath
true
} catch (e: Exception) {
Timber.e(e, "Failed to save file to $actualSavePath")
updateState {
copy(
alert = AlertDialogModel(
text = ParameterizedText(R.string.file_save_error),
confirmButton = AlertDialogModel.ButtonModel(
text = ParameterizedText(R.string.ok),
onClick = ::dismissAlert
)
)
)
}
false
}
}
suspend fun autosave() {
val isAutoSaveEnabled = preferenceHelper[Preference.AUTOSAVE_ENABLED] as Boolean
if (!isAutoSaveEnabled) {
Timber.i("Ignoring autosave as autosave not enabled")
return
}
if (!_state.value.dirty) {
Timber.d("Ignoring autosave as contents haven't changed")
return
}
if (saveMutex.isLocked) {
Timber.i("Ignoring autosave since manual save is already in progress")
return
}
Timber.d("Performing autosave")
if (!save(interactive = false)) {
withContext(Dispatchers.IO) {
// The user has left the app, with autosave enabled, and we don't already have a
// Uri for them or for some reason we were unable to save to the original Uri. In
// this case, we need to just save to internal file storage so that we can recover
val file = File(fileHelper.defaultDirectory, _state.value.fileName).toURI()
Timber.i("No cached uri for autosave, saving to $file instead")
// Here we call the fileHelper directly so that the file is still registered as dirty.
// This prevents the user from ending up in a scenario where they've autosaved the file
// 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
// the current one
fileHelper.save(file, _state.value.markdown.text)
preferenceHelper[Preference.AUTOSAVE_URI] = file
}
}
}
fun reset(untitledFileName: String, force: Boolean = false) {
Timber.i("Resetting view model to default state")
if (!force && _state.value.dirty) {
updateState {
copy(alert = AlertDialogModel(
text = ParameterizedText(R.string.prompt_save_changes),
confirmButton = AlertDialogModel.ButtonModel(
text = ParameterizedText(R.string.yes),
onClick = {
_state.value = _state.value.copy(
saveCallback = {
reset(untitledFileName, false)
}
)
}
),
dismissButton = AlertDialogModel.ButtonModel(
text = ParameterizedText(R.string.no),
onClick = {
reset(untitledFileName, true)
}
)
))
}
return
}
updateState {
EditorState(
fileName = untitledFileName,
lockSwiping = preferenceHelper[Preference.LOCK_SWIPING] as Boolean
)
}
Timber.i("Removing autosave uri from shared prefs")
preferenceHelper[Preference.AUTOSAVE_URI] = null
}
fun setLockSwiping(enabled: Boolean) {
preferenceHelper[Preference.LOCK_SWIPING] = enabled
}
private fun updateState(block: EditorState.() -> EditorState) {
_state.value = _state.value.block()
}
companion object {
fun factory(
fileHelper: FileHelper,
preferenceHelper: PreferenceHelper
): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
return MarkdownViewModel(fileHelper, preferenceHelper) as T
}
}
}
}
data class AlertDialogModel(
val text: ParameterizedText,
val confirmButton: ButtonModel,
val dismissButton: ButtonModel? = null
) {
data class ButtonModel(val text: ParameterizedText, val onClick: () -> Unit)
}
data class ParameterizedText(@StringRes val text: Int, val params: Array<Any> = arrayOf()) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParameterizedText
if (text != other.text) return false
if (!params.contentEquals(other.params)) return false
return true
}
override fun hashCode(): Int {
var result = text
result = 31 * result + params.contentHashCode()
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

@ -8,7 +8,7 @@ class Readability(private val content: String) {
val list = ArrayList<Sentence>() val list = ArrayList<Sentence>()
var startOfSentance = 0 var startOfSentance = 0
var lineBuilder = StringBuilder() var lineBuilder = StringBuilder()
for (i in 0 until content.length) { for (i in content.indices) {
val c = content[i] + "" val c = content[i] + ""
if (DELIMS.contains(c)) { if (DELIMS.contains(c)) {
list.add(Sentence(content, startOfSentance, i)) list.add(Sentence(content, startOfSentance, i))

View file

@ -0,0 +1,369 @@
package com.wbrawner.simplemarkdown.ui
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity
import androidx.navigation.NavController
import com.wbrawner.simplemarkdown.AlertDialogModel
import com.wbrawner.simplemarkdown.EditorState
import com.wbrawner.simplemarkdown.MarkdownViewModel
import com.wbrawner.simplemarkdown.ParameterizedText
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.Route
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.net.URI
import kotlin.reflect.KProperty1
@Composable
fun MainScreen(
navController: NavController,
viewModel: MarkdownViewModel,
enableWideLayout: Boolean,
enableAutosave: Boolean,
) {
val coroutineScope = rememberCoroutineScope()
val fileName by viewModel.collectAsState(EditorState::fileName, "")
val markdown by viewModel.collectAsState(EditorState::markdown, TextFieldValue(""))
val dirty by viewModel.collectAsState(EditorState::dirty, false)
val alert by viewModel.collectAsState(EditorState::alert, null)
val saveCallback by viewModel.collectAsState(EditorState::saveCallback, null)
val lockSwiping by viewModel.collectAsState(EditorState::lockSwiping, false)
LaunchedEffect(enableAutosave) {
if (!enableAutosave) return@LaunchedEffect
while (isActive) {
delay(500)
viewModel.autosave()
}
}
val toast by viewModel.collectAsState(EditorState::toast, null)
MainScreen(
dirty = dirty,
fileName = fileName,
markdown = markdown,
setMarkdown = viewModel::updateMarkdown,
lockSwiping = lockSwiping,
toggleLockSwiping = viewModel::setLockSwiping,
message = toast?.stringRes(),
dismissMessage = viewModel::dismissToast,
alert = alert,
dismissAlert = viewModel::dismissAlert,
navigate = {
navController.navigate(it.path)
},
navigateBack = { navController.popBackStack() },
loadFile = {
coroutineScope.launch {
viewModel.load(it.toString())
}
},
saveFile = {
coroutineScope.launch {
viewModel.save(it)
}
},
saveCallback = saveCallback,
reset = {
viewModel.reset("Untitled.md")
},
enableWideLayout = enableWideLayout,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MainScreen(
fileName: String = "Untitled.md",
dirty: Boolean = false,
markdown: TextFieldValue = TextFieldValue(""),
setMarkdown: (TextFieldValue) -> Unit = {},
lockSwiping: Boolean,
toggleLockSwiping: (Boolean) -> Unit,
message: String? = null,
dismissMessage: () -> Unit = {},
alert: AlertDialogModel? = null,
dismissAlert: () -> Unit = {},
navigate: (Route) -> Unit = {},
navigateBack: () -> Unit = {},
loadFile: (Uri?) -> Unit = {},
saveFile: (URI?) -> Unit = {},
saveCallback: (() -> Unit)? = null,
reset: () -> Unit = {},
enableWideLayout: Boolean = false,
) {
val openFileLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
loadFile(it)
}
val saveFileLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/*")) {
it?.let { uri -> saveFile(URI.create(uri.toString())) }
}
saveCallback?.let { callback ->
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/*")) {
it?.let { uri -> saveFile(URI.create(uri.toString())) }
callback()
}
LaunchedEffect(callback) {
launcher.launch(fileName)
}
}
val snackBarState = remember { SnackbarHostState() }
LaunchedEffect(message) {
message?.let {
snackBarState.showSnackbar(it)
dismissMessage()
}
}
alert?.let {
AlertDialog(
onDismissRequest = dismissAlert,
confirmButton = {
TextButton(onClick = it.confirmButton.onClick) {
Text(stringResource(it.confirmButton.text.text))
}
},
dismissButton = {
it.dismissButton?.let { dismissButton ->
TextButton(onClick = dismissButton.onClick) {
Text(dismissButton.text.stringRes())
}
}
},
text = { Text(it.text.stringRes()) }
)
}
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
MarkdownNavigationDrawer(navigate) { drawerState ->
Scaffold(
topBar = {
val context = LocalContext.current
MarkdownTopAppBar(
title = if (dirty) "$fileName*" else fileName,
backAsUp = false,
goBack = navigateBack,
drawerState = drawerState,
actions = {
IconButton(onClick = {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_TEXT, markdown.text)
shareIntent.type = "text/plain"
startActivity(
context, Intent.createChooser(
shareIntent, context.getString(R.string.share_file)
), null
)
}) {
Icon(imageVector = Icons.Default.Share, contentDescription = stringResource(R.string.action_share))
}
Box {
var menuExpanded by remember { mutableStateOf(false) }
IconButton(onClick = { menuExpanded = true }) {
Icon(imageVector = Icons.Default.MoreVert, stringResource(R.string.action_editor_actions))
}
DropdownMenu(expanded = menuExpanded,
onDismissRequest = { menuExpanded = false }) {
DropdownMenuItem(text = { Text(stringResource(R.string.action_new)) }, onClick = {
menuExpanded = false
reset()
})
DropdownMenuItem(text = { Text(stringResource(R.string.action_open)) }, onClick = {
menuExpanded = false
openFileLauncher.launch(arrayOf("text/*"))
})
DropdownMenuItem(text = { Text(stringResource(R.string.action_save)) }, onClick = {
menuExpanded = false
saveFile(null)
})
DropdownMenuItem(text = { Text(stringResource(R.string.action_save_as )) },
onClick = {
menuExpanded = false
saveFileLauncher.launch(fileName)
})
if (!enableWideLayout) {
DropdownMenuItem(text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.action_lock_swipe))
Checkbox(
checked = lockSwiping,
onCheckedChange = toggleLockSwiping
)
}
}, onClick = {
toggleLockSwiping(!lockSwiping)
menuExpanded = false
})
}
}
}
},
scrollBehavior = scrollBehavior
)
},
snackbarHost = {
SnackbarHost(
modifier = Modifier.imePadding(),
hostState = snackBarState
) {
Snackbar(it)
}
}
) { paddingValues ->
if (enableWideLayout) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
MarkdownTextField(
modifier = Modifier
.fillMaxHeight()
.weight(1f),
markdown = markdown,
setMarkdown = setMarkdown,
)
Spacer(
modifier = Modifier
.fillMaxHeight()
.width(1.dp)
.background(color = MaterialTheme.colorScheme.primary)
)
MarkdownText(
modifier = Modifier
.fillMaxHeight()
.weight(1f),
markdown = markdown.text
)
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
TabbedMarkdownEditor(
markdown = markdown,
setMarkdown = setMarkdown,
lockSwiping = lockSwiping,
scrollBehavior = scrollBehavior
)
}
}
}
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun TabbedMarkdownEditor(
markdown: TextFieldValue,
setMarkdown: (TextFieldValue) -> Unit,
lockSwiping: Boolean,
scrollBehavior: TopAppBarScrollBehavior
) {
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState { 2 }
TabRow(selectedTabIndex = pagerState.currentPage) {
Tab(text = { Text(stringResource(R.string.action_edit)) },
selected = pagerState.currentPage == 0,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } })
Tab(text = { Text(stringResource(R.string.action_preview)) },
selected = pagerState.currentPage == 1,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } })
}
HorizontalPager(
modifier = Modifier.fillMaxSize(1f), state = pagerState,
beyondViewportPageCount = 1,
userScrollEnabled = !lockSwiping
) { page ->
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(page) {
when (page) {
0 -> keyboardController?.show()
else -> keyboardController?.hide()
}
}
if (page == 0) {
MarkdownTextField(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
markdown = markdown,
setMarkdown = setMarkdown,
)
} else {
MarkdownText(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
markdown.text
)
}
}
}
@Composable
fun <P> MarkdownViewModel.collectAsState(prop: KProperty1<EditorState, P>, initial: P): State<P> =
remember(prop) { state.map { prop.get(it) }.distinctUntilChanged() }.collectAsState(initial)
@Composable
fun ParameterizedText.stringRes() = stringResource(text, *params)

View file

@ -0,0 +1,43 @@
package com.wbrawner.simplemarkdown.ui
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController
import com.wbrawner.simplemarkdown.utility.readAssetToString
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MarkdownInfoScreen(
title: String,
file: String,
navController: NavController,
) {
Scaffold(
topBar = {
MarkdownTopAppBar(
title = title,
goBack = { navController.popBackStack() },
)
}
) { paddingValues ->
val context = LocalContext.current
val (markdown, setMarkdown) = remember { mutableStateOf("") }
LaunchedEffect(file) {
setMarkdown(context.assets.readAssetToString(file))
}
MarkdownText(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
markdown = markdown
)
}
}

View file

@ -0,0 +1,80 @@
package com.wbrawner.simplemarkdown.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.DismissibleDrawerSheet
import androidx.compose.material3.DismissibleNavigationDrawer
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.Route
import kotlinx.coroutines.launch
@Composable
fun MarkdownNavigationDrawer(
navigate: (Route) -> Unit, content: @Composable (drawerState: DrawerState) -> Unit
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope()
BackHandler(enabled = drawerState.isOpen) {
coroutineScope.launch {
drawerState.close()
}
}
DismissibleNavigationDrawer(
gesturesEnabled = drawerState.isOpen,
drawerState = drawerState,
drawerContent = {
DismissibleDrawerSheet {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(96.dp),
painter = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface
)
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.titleLarge
)
}
Route.entries.forEach { route ->
if (route == Route.EDITOR) {
return@forEach
}
NavigationDrawerItem(
icon = {
Icon(imageVector = route.icon, contentDescription = null)
},
label = { Text(stringResource(route.title)) },
selected = false,
onClick = {
navigate(route)
coroutineScope.launch {
drawerState.close()
}
}
)
}
}
}) {
content(drawerState)
}
}

View file

@ -0,0 +1,125 @@
package com.wbrawner.simplemarkdown.ui
import android.annotation.SuppressLint
import android.graphics.Color.TRANSPARENT
import android.view.ViewGroup
import android.webkit.WebView
import android.widget.FrameLayout
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.viewinterop.AndroidView
import com.wbrawner.simplemarkdown.BuildConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.commonmark.ext.autolink.AutolinkExtension
import org.commonmark.ext.front.matter.YamlFrontMatterExtension
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension
import org.commonmark.ext.gfm.tables.TablesExtension
import org.commonmark.ext.heading.anchor.HeadingAnchorExtension
import org.commonmark.ext.image.attributes.ImageAttributesExtension
import org.commonmark.ext.task.list.items.TaskListItemsExtension
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
private val markdownExtensions = listOf(
AutolinkExtension.create(),
StrikethroughExtension.create(),
TablesExtension.create(),
HeadingAnchorExtension.create(),
YamlFrontMatterExtension.create(),
ImageAttributesExtension.create(),
TaskListItemsExtension.create(),
)
private val markdownParser = Parser.builder()
.extensions(markdownExtensions)
.build()
private val renderer = HtmlRenderer.builder()
.extensions(markdownExtensions)
.build()
@Composable
fun MarkdownText(modifier: Modifier = Modifier, markdown: String) {
val (html, setHtml) = remember { mutableStateOf("") }
LaunchedEffect(markdown) {
withContext(Dispatchers.IO) {
val parsedHtml = renderer.render(
markdownParser.parse(markdown)
)
setHtml(parsedHtml)
}
}
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Top
) {
HtmlText(html = html)
}
}
@SuppressLint("SetJavaScriptEnabled")
@OptIn(ExperimentalStdlibApi::class)
@Composable
fun HtmlText(html: String, modifier: Modifier = Modifier) {
val materialColors = MaterialTheme.colorScheme
val style = remember(isSystemInDarkTheme()) {
"""body {
| background: #${materialColors.surface.toArgb().toHexString().substring(2)};
| color: #${materialColors.onSurface.toArgb().toHexString().substring(2)};
|}
|a {
| color: #${materialColors.secondary.toArgb().toHexString().substring(2)};
|}
|pre {
| background: #${materialColors.surfaceVariant.toArgb().toHexString().substring(2)};
| color: #${materialColors.onSurfaceVariant.toArgb().toHexString().substring(2)};
|}""".trimMargin().wrapTag("style")
}
AndroidView(
modifier = modifier,
factory = { context ->
FrameLayout(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
addView(
WebView(context).apply {
tag = WEBVIEW_TAG
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 = { frameLayout ->
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>"

View file

@ -0,0 +1,82 @@
package com.wbrawner.simplemarkdown.ui
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.wbrawner.simplemarkdown.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MarkdownTextField(
modifier: Modifier = Modifier,
markdown: TextFieldValue,
setMarkdown: (TextFieldValue) -> Unit,
) {
val colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
disabledIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
)
val interactionSource = remember { MutableInteractionSource() }
val textStyle = TextStyle.Default.copy(
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface
)
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
BasicTextField(
value = markdown,
modifier = modifier.imePadding(),
onValueChange = setMarkdown,
enabled = true,
readOnly = false,
textStyle = textStyle,
cursorBrush = SolidColor(colors.cursorColor),
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
keyboardActions = KeyboardActions.Default,
interactionSource = interactionSource,
singleLine = false,
maxLines = Int.MAX_VALUE,
minLines = 1,
decorationBox = @Composable { innerTextField ->
// places leading icon, text field with label and placeholder, trailing icon
TextFieldDefaults.DecorationBox(
value = markdown.text,
visualTransformation = VisualTransformation.None,
innerTextField = innerTextField,
placeholder = {
Text(stringResource(R.string.markdown_here))
},
singleLine = false,
enabled = true,
interactionSource = interactionSource,
colors = colors,
contentPadding = PaddingValues(8.dp)
)
},
)
}
}

View file

@ -0,0 +1,66 @@
package com.wbrawner.simplemarkdown.ui
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.DrawerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import com.wbrawner.simplemarkdown.R
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MarkdownTopAppBar(
title: String,
goBack: () -> Unit,
backAsUp: Boolean = true,
drawerState: DrawerState? = null,
actions: (@Composable RowScope.() -> Unit)? = null,
scrollBehavior: TopAppBarScrollBehavior? = null,
) {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
TopAppBar(
title = {
Text(text = title, maxLines = 1, overflow = TextOverflow.Ellipsis)
},
colors = topAppBarColors(scrolledContainerColor = MaterialTheme.colorScheme.surface),
navigationIcon = {
val (icon, contentDescription, onClick) = remember {
if (backAsUp) {
Triple(Icons.AutoMirrored.Filled.ArrowBack, context.getString(R.string.action_back), goBack)
} else {
Triple(
Icons.Default.Menu, context.getString(R.string.action_menu)
) {
coroutineScope.launch {
if (drawerState?.isOpen == true) {
drawerState.close()
} else {
drawerState?.open()
}
}
}
}
}
IconButton(onClick = { onClick() }) {
Icon(imageVector = icon, contentDescription = contentDescription)
}
},
actions = actions ?: {},
scrollBehavior = scrollBehavior
)
}

View file

@ -0,0 +1,258 @@
package com.wbrawner.simplemarkdown.ui
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.wbrawner.simplemarkdown.BuildConfig
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
import com.wbrawner.simplemarkdown.utility.Preference
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(navController: NavController, preferenceHelper: PreferenceHelper) {
Scaffold(topBar = {
MarkdownTopAppBar(title = "Settings", goBack = { navController.popBackStack() })
}) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
BooleanPreference(
title = stringResource(R.string.pref_title_autosave),
enabledDescription = stringResource(R.string.pref_autosave_on),
disabledDescription = stringResource(R.string.pref_autosave_off),
preference = Preference.AUTOSAVE_ENABLED,
preferenceHelper = preferenceHelper
)
ListPreference(
title = stringResource(R.string.title_dark_mode),
options = stringArrayResource(R.array.pref_values_dark_mode),
preference = Preference.DARK_MODE,
preferenceHelper = preferenceHelper
)
BooleanPreference(
title = stringResource(R.string.pref_title_error_reports),
enabledDescription = stringResource(R.string.pref_error_reports_on),
disabledDescription = stringResource(R.string.pref_error_reports_off),
preference = Preference.ERROR_REPORTS_ENABLED,
preferenceHelper = preferenceHelper
)
BooleanPreference(
title = stringResource(R.string.pref_title_analytics),
enabledDescription = stringResource(R.string.pref_analytics_on),
disabledDescription = stringResource(R.string.pref_analytics_off),
preference = Preference.ANALYTICS_ENABLED,
preferenceHelper = preferenceHelper
)
BooleanPreference(
title = stringResource(R.string.pref_title_readability),
enabledDescription = stringResource(R.string.pref_readability_on),
disabledDescription = stringResource(R.string.pref_readability_off),
preference = Preference.READABILITY_ENABLED,
preferenceHelper = preferenceHelper
)
if (BuildConfig.DEBUG) {
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
error("Forced crash")
}
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Column(verticalArrangement = Arrangement.Center) {
Text(text = stringResource(R.string.action_force_crash), style = MaterialTheme.typography.bodyLarge)
Text(
text = stringResource(R.string.description_force_crash),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
@Composable
fun BooleanPreference(
title: String,
enabledDescription: String,
disabledDescription: String,
preference: Preference,
preferenceHelper: PreferenceHelper
) {
var enabled by remember {
mutableStateOf(preferenceHelper[preference] as Boolean)
}
BooleanPreference(title = title,
enabledDescription = enabledDescription,
disabledDescription = disabledDescription,
enabled = enabled,
setEnabled = {
enabled = it
preferenceHelper[preference] = it
})
}
@Composable
fun BooleanPreference(
title: String,
enabledDescription: String,
disabledDescription: String,
enabled: Boolean,
setEnabled: (Boolean) -> Unit
) {
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
setEnabled(!enabled)
}
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Column(verticalArrangement = Arrangement.Center) {
Text(text = title, style = MaterialTheme.typography.bodyLarge)
Text(
text = if (enabled) enabledDescription else disabledDescription,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(checked = enabled, onCheckedChange = setEnabled)
}
}
@Composable
fun ListPreference(
title: String,
options: Array<String>,
preference: Preference,
preferenceHelper: PreferenceHelper
) {
var selected by remember {
mutableStateOf(preferenceHelper[preference] as String)
}
ListPreference(title = title, options = options, selected = selected, setSelected = {
selected = it
preferenceHelper[preference] = it
})
}
@Composable
fun ListPreference(
title: String, options: Array<String>, selected: String, setSelected: (String) -> Unit
) {
var dialogShowing by remember { mutableStateOf(false) }
Column(modifier = Modifier
.fillMaxWidth()
.clickable {
dialogShowing = true
}
.padding(16.dp), verticalArrangement = Arrangement.Center) {
Text(text = title, style = MaterialTheme.typography.bodyLarge)
Text(
text = selected,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (dialogShowing) {
AlertDialog(
title = {
Text(title)
},
onDismissRequest = { dialogShowing = false },
confirmButton = {
TextButton(onClick = { dialogShowing = false }) {
Text("Cancel")
}
},
text = {
Column {
options.forEach { option ->
val onClick = {
setSelected(option)
dialogShowing = false
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(selected = option == selected, onClick = onClick)
Text(option)
}
}
}
}
)
}
}
@Preview
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
fun BooleanPreference_Preview() {
val (enabled, setEnabled) = remember { mutableStateOf(true) }
SimpleMarkdownTheme {
Surface {
BooleanPreference(
"Autosave",
"Files will be saved automatically",
"Files will not be saved automatically",
enabled,
setEnabled
)
}
}
}
@Preview
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
fun ListPreference_Preview() {
val (selected, setSelected) = remember { mutableStateOf("Auto") }
SimpleMarkdownTheme {
Surface {
ListPreference(
"Dark mode", arrayOf("Light", "Dark", "Auto"), selected, setSelected
)
}
}
}

View file

@ -0,0 +1,107 @@
package com.wbrawner.simplemarkdown.ui
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_ON
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity
import androidx.navigation.NavController
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.utility.SupportLinks
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SupportScreen(navController: NavController) {
Scaffold(topBar = {
MarkdownTopAppBar(title = stringResource(R.string.support_title), goBack = { navController.popBackStack() })
}) { paddingValues ->
val context = LocalContext.current
Column(
modifier = Modifier
.padding(paddingValues)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
modifier = Modifier.size(100.dp),
imageVector = Icons.Default.Favorite,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Text(stringResource(R.string.support_info), textAlign = TextAlign.Center)
Spacer(modifier = Modifier.height(8.dp))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
CustomTabsIntent.Builder()
.setShareState(SHARE_STATE_ON)
.build()
.launchUrl(context, Uri.parse("https://github.com/wbrawner/SimpleMarkdown"))
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(context.getColor(R.color.colorBackgroundGitHub)),
contentColor = Color.White
)
) {
Text(stringResource(R.string.action_view_github))
}
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
val playStoreIntent = Intent(Intent.ACTION_VIEW)
.apply {
data = Uri.parse("market://details?id=${context.packageName}")
addFlags(
Intent.FLAG_ACTIVITY_NO_HISTORY or
Intent.FLAG_ACTIVITY_NEW_DOCUMENT or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
)
}
try {
startActivity(context, playStoreIntent, null)
} catch (ignored: ActivityNotFoundException) {
playStoreIntent.data =
Uri.parse("https://play.google.com/store/apps/details?id=${context.packageName}")
startActivity(context, playStoreIntent, null)
}
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(context.getColor(R.color.colorBackgroundPlayStore)),
contentColor = Color.White
)
) {
Text(stringResource(R.string.action_rate))
}
SupportLinks()
}
}
}

View file

@ -0,0 +1,67 @@
package com.wbrawner.simplemarkdown.ui.theme
import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFFBA1A20)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFFFDAD6)
val md_theme_light_onPrimaryContainer = Color(0xFF410003)
val md_theme_light_secondary = Color(0xFF775653)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFFFDAD6)
val md_theme_light_onSecondaryContainer = Color(0xFF2C1513)
val md_theme_light_tertiary = Color(0xFF725B2E)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFFEDEA6)
val md_theme_light_onTertiaryContainer = Color(0xFF261900)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFFFBFF)
val md_theme_light_onBackground = Color(0xFF201A19)
val md_theme_light_surface = Color(0xFFFFFBFF)
val md_theme_light_onSurface = Color(0xFF201A19)
val md_theme_light_surfaceVariant = Color(0xFFF5DDDB)
val md_theme_light_onSurfaceVariant = Color(0xFF534342)
val md_theme_light_outline = Color(0xFF857371)
val md_theme_light_inverseOnSurface = Color(0xFFFBEEEC)
val md_theme_light_inverseSurface = Color(0xFF362F2E)
val md_theme_light_inversePrimary = Color(0xFFFFB3AC)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFFBA1A20)
val md_theme_light_outlineVariant = Color(0xFFD8C2BF)
val md_theme_light_scrim = Color(0xFF000000)
val md_theme_dark_primary = Color(0xFFFFB3AC)
val md_theme_dark_onPrimary = Color(0xFF680008)
val md_theme_dark_primaryContainer = Color(0xFF930010)
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDAD6)
val md_theme_dark_secondary = Color(0xFFE7BDB8)
val md_theme_dark_onSecondary = Color(0xFF442927)
val md_theme_dark_secondaryContainer = Color(0xFF5D3F3C)
val md_theme_dark_onSecondaryContainer = Color(0xFFFFDAD6)
val md_theme_dark_tertiary = Color(0xFFE1C38C)
val md_theme_dark_onTertiary = Color(0xFF3F2D04)
val md_theme_dark_tertiaryContainer = Color(0xFF584419)
val md_theme_dark_onTertiaryContainer = Color(0xFFFEDEA6)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF201A19)
val md_theme_dark_onBackground = Color(0xFFEDE0DE)
val md_theme_dark_surface = Color(0xFF201A19)
val md_theme_dark_onSurface = Color(0xFFEDE0DE)
val md_theme_dark_surfaceVariant = Color(0xFF534342)
val md_theme_dark_onSurfaceVariant = Color(0xFFD8C2BF)
val md_theme_dark_outline = Color(0xFFA08C8A)
val md_theme_dark_inverseOnSurface = Color(0xFF201A19)
val md_theme_dark_inverseSurface = Color(0xFFEDE0DE)
val md_theme_dark_inversePrimary = Color(0xFFBA1A20)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFFFFB3AC)
val md_theme_dark_outlineVariant = Color(0xFF534342)
val md_theme_dark_scrim = Color(0xFF000000)
val seed = Color(0xFFD32F2F)

View file

@ -0,0 +1,90 @@
package com.wbrawner.simplemarkdown.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
private val LightColors = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
private val DarkColors = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)
@Composable
fun SimpleMarkdownTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (!useDarkTheme) {
LightColors
} else {
DarkColors
}
MaterialTheme(
colorScheme = colors,
content = content
)
}

View file

@ -1,43 +0,0 @@
package com.wbrawner.simplemarkdown.utility
import android.content.Context
import android.util.Log
import com.wbrawner.simplemarkdown.BuildConfig
import io.sentry.android.core.SentryAndroid
import io.sentry.core.Sentry
import java.util.concurrent.atomic.AtomicBoolean
interface ErrorHandler {
fun init(context: Context, enable: Boolean)
fun enable(enable: Boolean)
fun reportException(t: Throwable, message: String? = null)
}
class SentryErrorHandler : ErrorHandler {
private lateinit var enabled: AtomicBoolean
override fun init(context: Context, enable: Boolean) {
enabled = AtomicBoolean(enable)
SentryAndroid.init(context) { options ->
options.setBeforeSend { event, _ ->
if (enabled.get()) {
event
} else {
null
}
}
}
}
override fun enable(enable: Boolean) {
enabled.set(enable)
}
override fun reportException(t: Throwable, message: String?) {
@Suppress("ConstantConditionIf")
if (BuildConfig.DEBUG) {
Log.e("SentryErrorHandler", "Caught exception: $message", t)
}
Sentry.captureException(t)
}
}

View file

@ -1,42 +1,21 @@
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 com.commonsware.cwac.anddown.AndDown
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.Reader import java.io.Reader
fun View.showKeyboard() { suspend fun AssetManager.readAssetToString(asset: String): String {
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
requestFocus()
}
fun View.hideKeyboard() =
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.hideSoftInputFromWindow(windowToken, 0)
suspend fun AssetManager.readAssetToString(asset: String): String? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
open(asset).reader().use(Reader::readText) open(asset).reader().use(Reader::readText)
} }
} }
const val HOEDOWN_FLAGS = AndDown.HOEDOWN_EXT_STRIKETHROUGH or AndDown.HOEDOWN_EXT_TABLES or
AndDown.HOEDOWN_EXT_UNDERLINE or AndDown.HOEDOWN_EXT_SUPERSCRIPT or
AndDown.HOEDOWN_EXT_FENCED_CODE
suspend fun String.toHtml(): String {
return withContext(Dispatchers.IO) {
AndDown().markdownToHtml(this@toHtml, HOEDOWN_FLAGS, 0)
}
}
suspend fun Uri.getName(context: Context): String { suspend fun Uri.getName(context: Context): String {
var fileName: String? = null var fileName: String? = null
try { try {
@ -62,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

@ -0,0 +1,73 @@
package com.wbrawner.simplemarkdown.utility
import android.content.Context
import android.content.Intent
import android.os.Environment
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.Reader
import java.net.URI
interface FileHelper {
val defaultDirectory: File
/**
* Opens a file at the given path
* @param path The path of the file to open
* @return A [Pair] of the file name to the file's contents
*/
suspend fun open(source: URI): Pair<String, String>?
/**
* Saves the given content to the given path
* @param path
* @param content
* @return The name of the saved file
*/
suspend fun save(destination: URI, content: String): String
}
class AndroidFileHelper(private val context: Context) : FileHelper {
override val defaultDirectory: File by lazy {
context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
?: context.filesDir
}
override suspend fun open(source: URI): Pair<String, String>? = withContext(Dispatchers.IO) {
val uri = source.toString().toUri()
try {
context.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
} catch (e: SecurityException) {
// We weren't granted the persistent read/write permission for this file.
// TODO: Return whether or not we got the persistent permission in order to determine
// whether or not we should show this file in the recent files section
}
context.contentResolver.openFileDescriptor(uri, "r")
?.use {
uri.getName(context) to FileInputStream(it.fileDescriptor).reader()
.use(Reader::readText)
}
}
override suspend fun save(destination: URI, content: String): String = withContext(Dispatchers.IO) {
val uri = destination.toString().toUri()
context.contentResolver.openOutputStream(uri, "rwt")
?.writer()
?.use {
it.write(content)
}
?: run {
Timber.w("Open output stream returned null for uri: $uri")
throw IOException("Failed to save to $destination")
}
return@withContext uri.getName(context)
}
}

View file

@ -0,0 +1,80 @@
package com.wbrawner.simplemarkdown.utility
import android.util.Log
import com.wbrawner.simplemarkdown.utility.PersistentTree.Companion.create
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.PrintStream
import java.text.SimpleDateFormat
import java.util.*
/**
* A [Timber.Tree] implementation that persists all logs to disk for retrieval later. Create
* instances via [create] instead of calling the constructor directly.
*/
class PersistentTree private constructor(
private val coroutineScope: CoroutineScope,
private val logFile: File
) : Timber.Tree() {
private val dateFormat = object : ThreadLocal<SimpleDateFormat>() {
override fun initialValue(): SimpleDateFormat =
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
}
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
val timestamp = dateFormat.get()!!.format(System.currentTimeMillis())
coroutineScope.launch(Dispatchers.IO) {
val priorityLetter = when (priority) {
Log.ASSERT -> "A"
Log.DEBUG -> "D"
Log.ERROR -> "E"
Log.INFO -> "I"
Log.VERBOSE -> "V"
Log.WARN -> "W"
else -> "U"
}
FileOutputStream(logFile, true).use { stream ->
stream.bufferedWriter().use {
it.appendLine("$timestamp $priorityLetter/${tag ?: "SimpleMarkdown"}: $message")
}
t?.let {
PrintStream(stream).use { pStream ->
it.printStackTrace(pStream)
}
}
}
}
}
init {
log(Log.INFO, "Persistent logging initialized, writing contents to ${logFile.absolutePath}")
}
companion object {
/**
* Create a new instance of a [PersistentTree].
* @param logDir A [File] pointing to a directory where the log files should be stored. Will be
* created if it doesn't exist.
* @throws IllegalArgumentException if [logDir] is a file instead of a directory
* @throws IOException if the directory does not exist or cannot be
* created/written to
*/
@Throws(IllegalArgumentException::class, IOException::class)
suspend fun create(coroutineScope: CoroutineScope, logDir: File): PersistentTree = withContext(Dispatchers.IO) {
if (!logDir.mkdirs() && !logDir.isDirectory)
throw IllegalArgumentException("Unable to create log directory at ${logDir.absolutePath}")
val timestamp = SimpleDateFormat("yyyyMMddHHmmss", Locale.US).format(Date())
val logFile = File(logDir, "persistent-log-$timestamp.log")
if (!logFile.createNewFile())
throw IOException("Unable to create logFile at ${logFile.absolutePath}")
PersistentTree(coroutineScope, logFile)
}
}
}

View file

@ -0,0 +1,63 @@
package com.wbrawner.simplemarkdown.utility
import android.content.Context
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
interface PreferenceHelper {
operator fun get(preference: Preference): Any?
operator fun set(preference: Preference, value: Any?)
fun <T> observe(preference: Preference): StateFlow<T>
}
class AndroidPreferenceHelper(context: Context, private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)): PreferenceHelper {
private val sharedPreferences by lazy {
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? = states[preference]?.value
override fun set(preference: Preference, value: Any?) {
sharedPreferences.edit {
when (value) {
is Boolean -> putBoolean(preference.key, value)
is Float -> putFloat(preference.key, value)
is Int -> putInt(preference.key, value)
is Long -> putLong(preference.key, value)
is String -> putString(preference.key, value)
null -> remove(preference.key)
}
}
coroutineScope.launch {
states[preference]!!.emit(value)
}
}
@Suppress("UNCHECKED_CAST")
override fun <T> observe(preference: Preference): StateFlow<T> = states[preference]!!.asStateFlow() as StateFlow<T>
}
enum class Preference(val key: String, val default: Any?) {
ANALYTICS_ENABLED("analytics.enable", true),
AUTOSAVE_ENABLED("autosave", true),
AUTOSAVE_URI("autosave.uri", null),
CUSTOM_CSS("pref.custom_css", null),
DARK_MODE("darkMode", "Auto"),
ERROR_REPORTS_ENABLED("acra.enable", true),
LOCK_SWIPING("lockSwiping", false),
READABILITY_ENABLED("readability.enable", false)
}

View file

@ -1,28 +0,0 @@
package com.wbrawner.simplemarkdown.view
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager
class DisableableViewPager : ViewPager {
private var isSwipeLocked = false
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return !isSwipeLocked && super.onInterceptTouchEvent(ev)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent): Boolean {
return !isSwipeLocked && super.onTouchEvent(ev)
}
fun setSwipeLocked(locked: Boolean) {
this.isSwipeLocked = locked
}
}

View file

@ -1,6 +0,0 @@
package com.wbrawner.simplemarkdown.view
interface ViewPagerPage {
fun onSelected()
fun onDeselected()
}

View file

@ -1,39 +0,0 @@
package com.wbrawner.simplemarkdown.view.activity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.navigation.findNavController
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback, CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main
private val viewModel: MarkdownViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
intent?.data?.let {
launch {
viewModel.load(this@MainActivity, it)
intent?.data = null
}
}
}
override fun onBackPressed() {
findNavController(R.id.content).navigateUp()
}
override fun onDestroy() {
super.onDestroy()
coroutineContext[Job]?.let {
cancel()
}
}
}

View file

@ -1,69 +0,0 @@
package com.wbrawner.simplemarkdown.view.activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.PreferenceManager
import com.wbrawner.simplemarkdown.R
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
class SplashActivity : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launch {
val darkMode = withContext(Dispatchers.IO) {
val darkModeValue = PreferenceManager.getDefaultSharedPreferences(this@SplashActivity)
.getString(
getString(R.string.pref_key_dark_mode),
getString(R.string.pref_value_auto)
)
return@withContext when {
darkModeValue.equals(getString(R.string.pref_value_light), ignoreCase = true) -> AppCompatDelegate.MODE_NIGHT_NO
darkModeValue.equals(getString(R.string.pref_value_dark), ignoreCase = true) -> AppCompatDelegate.MODE_NIGHT_YES
else -> {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
} else {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
}
}
}
AppCompatDelegate.setDefaultNightMode(darkMode)
val uri = withContext(Dispatchers.IO) {
intent?.data
?: PreferenceManager.getDefaultSharedPreferences(this@SplashActivity)
.getString(
getString(R.string.pref_key_autosave_uri),
null
)?.let {
Uri.parse(it)
}
}
val startIntent = Intent(this@SplashActivity, MainActivity::class.java)
.apply {
data = uri
}
startActivity(startIntent)
finish()
}
}
override fun onDestroy() {
super.onDestroy()
coroutineContext[Job]?.let {
cancel()
}
}
}

View file

@ -1,70 +0,0 @@
package com.wbrawner.simplemarkdown.view.adapter
import android.content.Context
import android.content.res.Configuration
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.view.fragment.EditFragment
import com.wbrawner.simplemarkdown.view.fragment.PreviewFragment
class EditPagerAdapter(fm: FragmentManager, private val context: Context)
: FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT), ViewPager.OnPageChangeListener {
private val editFragment = EditFragment()
private val previewFragment = PreviewFragment()
override fun getItem(position: Int): Fragment {
return when (position) {
FRAGMENT_EDIT -> editFragment
FRAGMENT_PREVIEW -> previewFragment
else -> throw IllegalStateException("Attempting to get fragment for invalid page number")
}
}
override fun getCount(): Int {
return NUM_PAGES
}
override fun getPageTitle(position: Int): CharSequence? {
var stringId = 0
when (position) {
FRAGMENT_EDIT -> stringId = R.string.action_edit
FRAGMENT_PREVIEW -> stringId = R.string.action_preview
}
return context.getString(stringId)
}
override fun getPageWidth(position: Int): Float {
return if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
0.5f
} else {
super.getPageWidth(position)
}
}
override fun onPageScrollStateChanged(state: Int) {
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
}
override fun onPageSelected(position: Int) {
when (position) {
FRAGMENT_EDIT -> {
editFragment.onSelected()
}
FRAGMENT_PREVIEW -> {
editFragment.onDeselected()
}
}
}
companion object {
const val FRAGMENT_EDIT = 0
const val FRAGMENT_PREVIEW = 1
const val NUM_PAGES = 2
}
}

View file

@ -1,177 +0,0 @@
package com.wbrawner.simplemarkdown.view.fragment
import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Bundle
import android.text.Editable
import android.text.SpannableString
import android.text.TextWatcher
import android.text.style.BackgroundColorSpan
import android.util.Log
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.TextView
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.preference.PreferenceManager
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.model.Readability
import com.wbrawner.simplemarkdown.utility.hideKeyboard
import com.wbrawner.simplemarkdown.utility.showKeyboard
import com.wbrawner.simplemarkdown.view.ViewPagerPage
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.math.abs
class EditFragment : Fragment(), ViewPagerPage, CoroutineScope {
private var markdownEditor: EditText? = null
private var markdownEditorScroller: NestedScrollView? = null
private val viewModel: MarkdownViewModel by activityViewModels()
override val coroutineContext: CoroutineContext = Dispatchers.Main
private var readabilityWatcher: TextWatcher? = null
@SuppressLint("ClickableViewAccessibility")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_edit, container, false)
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
markdownEditor = view.findViewById(R.id.markdown_edit)
markdownEditorScroller = view.findViewById(R.id.markdown_edit_container)
markdownEditor?.addTextChangedListener(object : TextWatcher {
private var searchFor = ""
override fun afterTextChanged(s: Editable?) {
val searchText = s.toString().trim()
if (searchText == searchFor)
return
searchFor = searchText
launch {
delay(50)
if (searchText != searchFor)
return@launch
viewModel.updateMarkdown(searchText)
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
})
var touchDown = 0L
var oldX = 0f
var oldY = 0f
markdownEditorScroller!!.setOnTouchListener { _, event ->
// The ScrollView's onClickListener doesn't seem to be called, so I've had to
// implement a sort of custom click listener that checks that the tap was both quick
// and didn't drag.
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
touchDown = System.currentTimeMillis()
oldX = event.rawX
oldY = event.rawY
}
MotionEvent.ACTION_UP -> {
if (System.currentTimeMillis() - touchDown < 150
&& abs(event.rawX - oldX) < 25
&& abs(event.rawY - oldY) < 25)
markdownEditor?.showKeyboard()
}
}
false
}
markdownEditor?.setText(viewModel.markdownUpdates.value)
viewModel.originalMarkdown.observe(viewLifecycleOwner, Observer {
markdownEditor?.setText(it)
})
launch {
val enableReadability = withContext(Dispatchers.IO) {
context?.let {
PreferenceManager.getDefaultSharedPreferences(it)
.getBoolean(getString(R.string.readability_enabled), false)
}?: false
}
if (enableReadability) {
if (readabilityWatcher == null) {
readabilityWatcher = ReadabilityTextWatcher()
}
markdownEditor?.addTextChangedListener(readabilityWatcher)
} else {
readabilityWatcher?.let {
markdownEditor?.removeTextChangedListener(it)
}
readabilityWatcher = null
}
}
}
override fun onDestroyView() {
coroutineContext[Job]?.let {
cancel()
}
super.onDestroyView()
}
override fun onSelected() {
markdownEditor?.showKeyboard()
}
override fun onDeselected() {
markdownEditor?.hideKeyboard()
}
inner class ReadabilityTextWatcher : TextWatcher {
private var previousValue = ""
private var searchFor = ""
override fun afterTextChanged(s: Editable?) {
val searchText = s.toString().trim()
if (searchText == searchFor)
return
searchFor = searchText
launch {
delay(250)
if (searchText != searchFor)
return@launch
val start = System.currentTimeMillis()
if (searchFor.isEmpty()) return@launch
if (previousValue == searchFor) return@launch
val readability = Readability(searchFor)
val span = SpannableString(searchFor)
for (sentence in readability.sentences()) {
var color = Color.TRANSPARENT
if (sentence.syllableCount() > 25) color = Color.argb(100, 229, 232, 42)
if (sentence.syllableCount() > 35) color = Color.argb(100, 193, 66, 66)
Log.d("SimpleMarkdown", "Sentence start: ${sentence.start()} end: ${sentence.end()}")
span.setSpan(BackgroundColorSpan(color), sentence.start(), sentence.end(), 0)
}
markdownEditor?.setTextKeepState(span, TextView.BufferType.SPANNABLE)
previousValue = searchFor
val timeTakenMs = System.currentTimeMillis() - start
Log.d("SimpleMarkdown", "Handled markdown in $timeTakenMs ms")
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
}
}

View file

@ -1,301 +0,0 @@
package com.wbrawner.simplemarkdown.view.fragment
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.onNavDestinationSelected
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.wbrawner.simplemarkdown.MarkdownApplication
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.view.adapter.EditPagerAdapter
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
import kotlinx.android.synthetic.main.fragment_main.*
import kotlinx.coroutines.*
import java.io.File
import kotlin.coroutines.CoroutineContext
class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallback, CoroutineScope {
private var shouldAutoSave = true
override val coroutineContext: CoroutineContext = Dispatchers.Main
private val viewModel: MarkdownViewModel by activityViewModels()
private var appBarConfiguration: AppBarConfiguration? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_main, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
with(findNavController()) {
appBarConfiguration = AppBarConfiguration(graph, drawerLayout)
toolbar.setupWithNavController(this, appBarConfiguration!!)
toolbar.inflateMenu(R.menu.menu_edit)
toolbar.setOnMenuItemClickListener { item ->
return@setOnMenuItemClickListener when (item.itemId) {
R.id.action_save -> {
launch {
if (!viewModel.save(requireContext())) {
requestFileOp(REQUEST_SAVE_FILE)
} else {
Toast.makeText(
requireContext(),
getString(R.string.file_saved, viewModel.fileName.value),
Toast.LENGTH_SHORT
).show()
}
}
true
}
R.id.action_save_as -> {
requestFileOp(REQUEST_SAVE_FILE)
true
}
R.id.action_share -> {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_TEXT, viewModel.markdownUpdates.value)
shareIntent.type = "text/plain"
startActivity(Intent.createChooser(
shareIntent,
getString(R.string.share_file)
))
true
}
R.id.action_load -> {
requestFileOp(REQUEST_OPEN_FILE)
true
}
R.id.action_new -> {
promptSaveOrDiscardChanges()
true
}
R.id.action_lock_swipe -> {
item.isChecked = !item.isChecked
pager!!.setSwipeLocked(item.isChecked)
true
}
else -> item.onNavDestinationSelected(findNavController())
}
}
navigationView.setupWithNavController(this)
}
val adapter = EditPagerAdapter(childFragmentManager, view.context)
pager.adapter = adapter
pager.addOnPageChangeListener(adapter)
pager.pageMargin = 1
pager.setPageMarginDrawable(R.color.colorAccent)
tabLayout.setupWithViewPager(pager)
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
tabLayout!!.visibility = View.GONE
}
@Suppress("CAST_NEVER_SUCCEEDS")
viewModel.fileName.observe(viewLifecycleOwner, Observer {
toolbar?.title = it
})
}
override fun onStart() {
super.onStart()
launch {
withContext(Dispatchers.IO) {
val enableErrorReports = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getBoolean(getString(R.string.error_reports_enabled), true)
(requireActivity().application as MarkdownApplication).errorHandler.enable(enableErrorReports)
}
}
}
override fun onPause() {
super.onPause()
launch {
val context = context?.applicationContext ?: return@launch
withContext(Dispatchers.IO) {
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
val isAutoSaveEnabled = sharedPrefs.getBoolean(KEY_AUTOSAVE, true)
if (!shouldAutoSave || !isAutoSaveEnabled) {
return@withContext
}
val uri = if (viewModel.save(context)) {
viewModel.uri.value
} else {
// The user has left the app, with autosave enabled, and we don't already have a
// Uri for them or for some reason we were unable to save to the original Uri. In
// this case, we need to just save to internal file storage so that we can recover
val fileUri = Uri.fromFile(File(context.filesDir, viewModel.fileName.value!!))
if (viewModel.save(context, fileUri)) {
fileUri
} else {
null
}
} ?: return@withContext
sharedPrefs.edit()
.putString(getString(R.string.pref_key_autosave_uri), uri.toString())
.apply()
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
tabLayout!!.visibility = View.GONE
else
tabLayout!!.visibility = View.VISIBLE
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_SAVE_FILE, REQUEST_OPEN_FILE -> {
// If request is cancelled, the result arrays are empty.
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, open file save dialog
requestFileOp(requestCode)
} else {
// Permission denied, do nothing
context?.let {
Toast.makeText(it, R.string.no_permissions, Toast.LENGTH_SHORT)
.show()
}
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_OPEN_FILE -> {
if (resultCode != Activity.RESULT_OK || data?.data == null) {
return
}
launch {
val fileLoaded = context?.let {
viewModel.load(it, data.data)
}
if (fileLoaded == false) {
context?.let {
Toast.makeText(it, R.string.file_load_error, Toast.LENGTH_SHORT)
.show()
}
}
}
}
REQUEST_SAVE_FILE -> {
if (resultCode != Activity.RESULT_OK || data?.data == null) {
return
}
launch {
context?.let {
viewModel.save(it, data.data)
}
}
}
}
super.onActivityResult(requestCode, resultCode, data)
}
private fun promptSaveOrDiscardChanges() {
if (viewModel.originalMarkdown.value == viewModel.markdownUpdates.value) {
viewModel.reset("Untitled.md")
return
}
val context = context ?: return
AlertDialog.Builder(context)
.setTitle(R.string.save_changes)
.setMessage(R.string.prompt_save_changes)
.setNegativeButton(R.string.action_discard) { _, _ ->
viewModel.reset("Untitled.md")
}
.setPositiveButton(R.string.action_save) { _, _ ->
requestFileOp(REQUEST_SAVE_FILE)
}
.create()
.show()
}
private fun requestFileOp(requestType: Int) {
val context = context ?: return
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED && Build.VERSION.SDK_INT > 22) {
requestPermissions(
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
requestType
)
return
}
// If the user is going to save the file, we don't want to auto-save it for them
shouldAutoSave = false
val intent = when (requestType) {
REQUEST_SAVE_FILE -> {
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = "text/markdown"
putExtra(Intent.EXTRA_TITLE, viewModel.fileName.value)
}
}
REQUEST_OPEN_FILE -> {
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
type = "*/*"
if (MimeTypeMap.getSingleton().hasMimeType("md")) {
// If the device doesn't recognize markdown files then we're not going to be
// able to open them at all, so there's no sense in filtering them out.
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/plain", "text/markdown"))
}
}
}
else -> null
} ?: return
intent.addCategory(Intent.CATEGORY_OPENABLE)
startActivityForResult(
intent,
requestType
)
}
override fun onResume() {
super.onResume()
shouldAutoSave = true
}
override fun onDestroy() {
super.onDestroy()
coroutineContext[Job]?.let {
cancel()
}
}
companion object {
// Request codes
const val REQUEST_OPEN_FILE = 1
const val REQUEST_SAVE_FILE = 2
const val REQUEST_DARK_MODE = 4
const val KEY_AUTOSAVE = "autosave"
}
}

View file

@ -1,92 +0,0 @@
package com.wbrawner.simplemarkdown.view.fragment
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import com.wbrawner.simplemarkdown.MarkdownApplication
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.utility.readAssetToString
import com.wbrawner.simplemarkdown.utility.toHtml
import kotlinx.android.synthetic.main.fragment_markdown_info.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
class MarkdownInfoFragment : Fragment(), CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_markdown_info, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val fileName = arguments?.getString(EXTRA_FILE)
if (fileName.isNullOrBlank()) {
findNavController().navigateUp()
return
}
toolbar.setupWithNavController(findNavController())
val isNightMode = AppCompatDelegate.getDefaultNightMode() ==
AppCompatDelegate.MODE_NIGHT_YES
|| resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
val defaultCssId = if (isNightMode) {
R.string.pref_custom_css_default_dark
} else {
R.string.pref_custom_css_default
}
val css: String? = getString(defaultCssId)
launch {
try {
val html = view.context.assets?.readAssetToString(fileName)
?.toHtml()
?: throw RuntimeException("Unable to open stream to $fileName")
infoWebview.loadDataWithBaseURL(null,
String.format(FORMAT_CSS, css) + html,
"text/html",
"UTF-8", null
)
} catch (e: Exception) {
(requireActivity().application as MarkdownApplication).errorHandler.reportException(e)
Toast.makeText(view.context, R.string.file_load_error, Toast.LENGTH_SHORT).show()
findNavController().navigateUp()
}
}
}
override fun onDestroy() {
coroutineContext[Job]?.cancel()
super.onDestroy()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
findNavController().navigateUp()
return true
}
return super.onOptionsItemSelected(item)
}
companion object {
const val FORMAT_CSS = "<style>" +
"%s" +
"</style>"
const val EXTRA_TITLE = "title"
const val EXTRA_FILE = "file"
}
}

View file

@ -1,101 +0,0 @@
package com.wbrawner.simplemarkdown.view.fragment
import android.content.Context
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.preference.PreferenceManager
import com.wbrawner.simplemarkdown.BuildConfig
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.utility.toHtml
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
class PreviewFragment : Fragment(), CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main
private val viewModel: MarkdownViewModel by activityViewModels()
private var markdownPreview: WebView? = null
private var style: String = ""
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_preview, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
markdownPreview = view.findViewById(R.id.markdown_view)
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
launch {
val isNightMode = AppCompatDelegate.getDefaultNightMode() ==
AppCompatDelegate.MODE_NIGHT_YES
|| requireContext().resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
val defaultCssId = if (isNightMode) {
R.string.pref_custom_css_default_dark
} else {
R.string.pref_custom_css_default
}
val css = withContext(Dispatchers.IO) {
val context = context ?: return@withContext null
@Suppress("ConstantConditionIf")
if (!BuildConfig.ENABLE_CUSTOM_CSS) {
context.getString(defaultCssId)
} else {
PreferenceManager.getDefaultSharedPreferences(context)
.getString(
getString(R.string.pref_custom_css),
getString(defaultCssId)
)
}
}
style = String.format(FORMAT_CSS, css ?: "")
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
updateWebContent(viewModel.markdownUpdates.value ?: "")
viewModel.markdownUpdates.observe(this, Observer {
updateWebContent(it)
})
}
private fun updateWebContent(markdown: String) {
markdownPreview?.post {
launch {
markdownPreview?.loadDataWithBaseURL(null,
style + markdown.toHtml(),
"text/html",
"UTF-8", null
)
}
}
}
override fun onDestroyView() {
coroutineContext[Job]?.let {
cancel()
}
markdownPreview?.let {
(it.parent as ViewGroup).removeView(it)
it.destroy()
markdownPreview = null
}
super.onDestroyView()
}
companion object {
var FORMAT_CSS = "<style>" +
"%s" +
"</style>"
}
}

View file

@ -1,28 +0,0 @@
package com.wbrawner.simplemarkdown.view.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import com.wbrawner.simplemarkdown.R
import kotlinx.android.synthetic.main.fragment_settings.*
class SettingsContainerFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_settings, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
toolbar.setupWithNavController(findNavController())
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = findNavController().navigateUp()
}

View file

@ -1,75 +0,0 @@
package com.wbrawner.simplemarkdown.view.fragment
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.ListPreference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.wbrawner.simplemarkdown.BuildConfig
import com.wbrawner.simplemarkdown.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
class SettingsFragment
: PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener,
CoroutineScope {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_general)
}
override val coroutineContext: CoroutineContext = Dispatchers.Main
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
launch(context = Dispatchers.IO) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
sharedPreferences.registerOnSharedPreferenceChangeListener(this@SettingsFragment)
(findPreference(getString(R.string.pref_key_dark_mode)) as? ListPreference)?.let {
setListPreferenceSummary(sharedPreferences, it)
}
@Suppress("ConstantConditionIf")
if (!BuildConfig.ENABLE_CUSTOM_CSS) {
preferenceScreen.removePreference(findPreference(getString(R.string.pref_custom_css)))
}
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (!isAdded) return
val preference = findPreference(key) as? ListPreference ?: return
setListPreferenceSummary(sharedPreferences, preference)
if (preference.key != getString(R.string.pref_key_dark_mode)) {
return
}
var darkMode: Int = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
} else {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
val darkModeValue = sharedPreferences.getString(preference.key, null)
if (darkModeValue != null && darkModeValue.isNotEmpty()) {
if (darkModeValue.equals(getString(R.string.pref_value_light), ignoreCase = true)) {
darkMode = AppCompatDelegate.MODE_NIGHT_NO
} else if (darkModeValue.equals(getString(R.string.pref_value_dark), ignoreCase = true)) {
darkMode = AppCompatDelegate.MODE_NIGHT_YES
}
}
AppCompatDelegate.setDefaultNightMode(darkMode)
activity?.recreate()
}
private fun setListPreferenceSummary(sharedPreferences: SharedPreferences, preference: ListPreference) {
val storedValue = sharedPreferences.getString(
preference.key,
null
) ?: return
val index = preference.findIndexOfValue(storedValue)
if (index < 0) return
preference.summary = preference.entries[index].toString()
}
}

View file

@ -1,61 +0,0 @@
package com.wbrawner.simplemarkdown.view.fragment
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.utility.SupportLinkProvider
import kotlinx.android.synthetic.main.fragment_support.*
class SupportFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_support, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
toolbar.setupWithNavController(findNavController())
githubButton.setOnClickListener {
CustomTabsIntent.Builder()
.addDefaultShareMenuItem()
.build()
.launchUrl(view.context, Uri.parse("https://github" +
".com/wbrawner/SimpleMarkdown"))
}
rateButton.setOnClickListener {
val playStoreIntent = Intent(Intent.ACTION_VIEW)
.apply {
data = Uri.parse("market://details?id=${view.context.packageName}")
addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or
Intent.FLAG_ACTIVITY_NEW_DOCUMENT or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
try {
startActivity(playStoreIntent)
} catch (ignored: ActivityNotFoundException) {
playStoreIntent.data = Uri.parse("https://play.google.com/store/apps/details?id=${view.context.packageName}")
startActivity(playStoreIntent)
}
}
SupportLinkProvider(requireActivity()).supportLinks.observe(viewLifecycleOwner, Observer { links ->
links.forEach {
supportButtons.addView(it)
}
})
}
// override fun onOptionsItemSelected(item: MenuItem): Boolean {
// if (item.itemId == android.R.id.home) {
// findNavController().navigateUp()
// return true
// }
// return super.onOptionsItemSelected(item)
// }
}

View file

@ -1,68 +0,0 @@
package com.wbrawner.simplemarkdown.viewmodel
import android.content.Context
import android.net.Uri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.wbrawner.simplemarkdown.utility.getName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.io.Reader
class MarkdownViewModel : ViewModel() {
val fileName = MutableLiveData<String>("Untitled.md")
val markdownUpdates = MutableLiveData<String>()
val originalMarkdown = MutableLiveData<String>()
val uri = MutableLiveData<Uri>()
fun updateMarkdown(markdown: String?) {
this.markdownUpdates.postValue(markdown ?: "")
}
suspend fun load(context: Context, uri: Uri?): Boolean {
if (uri == null) return false
return withContext(Dispatchers.IO) {
try {
context.contentResolver.openFileDescriptor(uri, "r")?.use {
val fileInput = FileInputStream(it.fileDescriptor)
val fileName = uri.getName(context)
val content = fileInput.reader().use(Reader::readText)
originalMarkdown.postValue(content)
markdownUpdates.postValue(content)
this@MarkdownViewModel.fileName.postValue(fileName)
this@MarkdownViewModel.uri.postValue(uri)
true
} ?: false
} catch (ignored: Exception) {
false
}
}
}
suspend fun save(context: Context, givenUri: Uri? = this.uri.value): Boolean {
val uri = givenUri ?: this.uri.value ?: return false
return withContext(Dispatchers.IO) {
try {
val fileName = uri.getName(context)
context.contentResolver.openOutputStream(uri, "rwt")
?.writer()
?.use {
it.write(markdownUpdates.value ?: "")
}
?: return@withContext false
this@MarkdownViewModel.fileName.postValue(fileName)
this@MarkdownViewModel.uri.postValue(uri)
true
} catch (ignored: Exception) {
false
}
}
}
fun reset(untitledFileName: String) {
fileName.postValue(untitledFileName)
originalMarkdown.postValue("")
markdownUpdates.postValue("")
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
</vector>

View file

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="0.39862207"
android:scaleY="0.39862207"
android:translateX="27"
android:translateY="27">
<path
android:fillColor="#FFFFFF"
android:pathData="M55.033,2.91h25.4v129.646h-25.4z"
android:strokeWidth="0.53079969" />
<path
android:fillColor="#FFFFFF"
android:pathData="M2.725,44.865l12.7,-21.997l117.318,67.733l-12.7,21.997z"
android:strokeWidth="0.54258478" />
<path
android:fillColor="#FFFFFF"
android:pathData="M120.042,22.868l12.7,21.997l-117.318,67.733l-12.7,-21.997z"
android:strokeWidth="0.54258478" />
</group>
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/colorOnBackground"
android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/colorOnBackground"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
</vector>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:top="0dp"
android:bottom="0dp"
android:left="0dp"
android:right="0dp"
android:drawable="@color/colorPrimary"/>
<item
android:drawable="@drawable/splash_fg"
android:gravity="center"/>
</layer-list>

View file

@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/drawerLayout">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.wbrawner.simplemarkdown.view.DisableableViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorBackground"
app:layout_scrollFlags="scroll|enterAlways" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorBackground"
android:visibility="gone">
<com.google.android.material.tabs.TabItem
android:id="@+id/editTab"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/action_edit" />
<com.google.android.material.tabs.TabItem
android:id="@+id/previewTab"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/action_preview" />
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigationView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:menu="@menu/menu_main" />
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/content"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />

View file

@ -1,23 +0,0 @@
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/markdown_edit_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.wbrawner.simplemarkdown.view.fragment.EditFragment">
<EditText
android:id="@+id/markdown_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:gravity="top"
android:fontFamily="monospace"
android:hint="@string/markdown_here"
android:imeOptions="flagNoExtractUi"
android:inputType="textMultiLine|textCapSentences"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="16dp"
android:scrollHorizontally="false"
android:importantForAutofill="no" />
</androidx.core.widget.NestedScrollView>

View file

@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBackground"
android:id="@+id/drawerLayout">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.wbrawner.simplemarkdown.view.DisableableViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorBackground"
app:layout_scrollFlags="scroll|enterAlways|snap" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@color/colorBackground">
<com.google.android.material.tabs.TabItem
android:id="@+id/editTab"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/action_edit" />
<com.google.android.material.tabs.TabItem
android:id="@+id/previewTab"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/action_preview" />
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigationView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:menu="@menu/menu_main" />
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.wbrawner.simplemarkdown.view.fragment.MarkdownInfoFragment">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="128dp"
android:background="@color/colorBackground">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="0dp"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<WebView
android:id="@+id/infoWebview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:nestedScrollingEnabled="false" />
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,13 +0,0 @@
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.wbrawner.simplemarkdown.view.fragment.PreviewFragment">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:nestedScrollingEnabled="false"
android:id="@+id/markdown_view" />
</androidx.core.widget.NestedScrollView>

View file

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorBackground" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_settings"
android:name="com.wbrawner.simplemarkdown.view.fragment.SettingsFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

View file

@ -1,78 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorBackground"
app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipChildren="false"
android:clipToPadding="false"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/heartIcon"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_favorite_black_24dp"
android:tint="@color/colorAccent"
android:contentDescription="@string/description_heart"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/supportInfoText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@string/support_info"
android:textAlignment="center"
android:textColor="@color/colorOnBackground"
app:layout_constraintTop_toBottomOf="@+id/heartIcon" />
<com.google.android.material.button.MaterialButton
android:id="@+id/githubButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/colorBackgroundGitHub"
android:textColor="@color/colorWhite"
android:text="@string/action_view_github"
app:layout_constraintTop_toBottomOf="@+id/supportInfoText" />
<com.google.android.material.button.MaterialButton
android:id="@+id/rateButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/colorBackgroundPlayStore"
android:textColor="@color/colorWhite"
android:text="@string/action_rate"
app:layout_constraintTop_toBottomOf="@+id/githubButton" />
<LinearLayout
android:id="@+id/supportButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/rateButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_share"
android:icon="@drawable/ic_share"
android:title="@string/action_share"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_new"
android:title="@string/action_new"
app:showAsAction="never" />
<item
android:id="@+id/action_load"
android:title="@string/action_open"
app:showAsAction="never" />
<item
android:id="@+id/action_save"
android:title="@string/action_save" />
<item
android:id="@+id/action_save_as"
android:title="@string/action_save_as" />
<item
android:id="@+id/action_lock_swipe"
android:checkable="true"
android:title="@string/action_lock_swipe"
app:showAsAction="never" />
</menu>

View file

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<group android:id="@+id/mainGroup">
<item
android:id="@+id/action_mainFragment_to_settingsContainerFragment"
android:title="@string/action_settings"
android:icon="@drawable/ic_settings_black_24dp"
app:showAsAction="never" />
<item
android:id="@+id/action_mainFragment_to_supportFragment"
android:title="@string/support_title"
android:icon="@drawable/ic_favorite_black_24dp"
app:showAsAction="never" />
</group>
<group android:id="@+id/addtionalInfoGroup">
<item
android:id="@+id/action_mainFragment_to_helpFragment"
android:title="@string/action_help"
android:icon="@drawable/ic_help_black_24dp"
app:showAsAction="never" />
<item
android:id="@+id/action_mainFragment_to_librariesFragment"
android:title="@string/action_libraries"
android:icon="@drawable/ic_info_black_24dp"
app:showAsAction="never" />
<item
android:id="@+id/action_mainFragment_to_privacyFragment"
android:title="@string/action_privacy"
android:icon="@drawable/ic_eye_black_24dp"
app:showAsAction="never" />
</group>
</menu>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,87 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@+id/mainFragment">
<fragment
android:id="@+id/settingsContainerFragment"
android:name="com.wbrawner.simplemarkdown.view.fragment.SettingsContainerFragment"
android:label="@string/title_activity_settings" />
<fragment
android:id="@+id/mainFragment"
android:name="com.wbrawner.simplemarkdown.view.fragment.MainFragment"
android:label="">
<action
android:id="@+id/action_mainFragment_to_helpFragment"
app:destination="@id/helpFragment"
app:enterAnim="@android:anim/slide_in_left"
app:exitAnim="@android:anim/slide_out_right"
app:popExitAnim="@android:anim/slide_out_right"
app:popUpTo="@id/mainFragment" />
<action
android:id="@+id/action_mainFragment_to_privacyFragment"
app:destination="@id/privacyFragment"
app:enterAnim="@android:anim/slide_in_left"
app:exitAnim="@android:anim/slide_out_right"
app:popExitAnim="@android:anim/slide_out_right"
app:popUpTo="@id/mainFragment" />
<action
android:id="@+id/action_mainFragment_to_librariesFragment"
app:destination="@id/librariesFragment"
app:enterAnim="@android:anim/slide_in_left"
app:exitAnim="@android:anim/slide_out_right"
app:popExitAnim="@android:anim/slide_out_right"
app:popUpTo="@id/mainFragment" />
<action
android:id="@+id/action_mainFragment_to_settingsContainerFragment"
app:destination="@id/settingsContainerFragment"
app:enterAnim="@android:anim/slide_in_left"
app:exitAnim="@android:anim/slide_out_right"
app:popExitAnim="@android:anim/slide_out_right"
app:popUpTo="@id/mainFragment" />
<action
android:id="@+id/action_mainFragment_to_supportFragment"
app:destination="@id/supportFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/mainFragment" />
</fragment>
<fragment
android:id="@+id/supportFragment"
android:name="com.wbrawner.simplemarkdown.view.fragment.SupportFragment"
android:label="@string/support_title" />
<fragment
android:id="@+id/helpFragment"
android:name="com.wbrawner.simplemarkdown.view.fragment.MarkdownInfoFragment"
android:label="@string/action_help"
tools:layout="@layout/fragment_markdown_info">
<argument
android:name="file"
app:argType="string"
android:defaultValue="Cheatsheet.md" />
</fragment>
<fragment
android:id="@+id/privacyFragment"
android:name="com.wbrawner.simplemarkdown.view.fragment.MarkdownInfoFragment"
android:label="@string/action_privacy"
tools:layout="@layout/fragment_markdown_info">
<argument
android:name="file"
android:defaultValue="Privacy Policy.md"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/librariesFragment"
android:name="com.wbrawner.simplemarkdown.view.fragment.MarkdownInfoFragment"
android:label="@string/action_libraries"
tools:layout="@layout/fragment_markdown_info">
<argument
android:name="file"
android:defaultValue="Libraries.md"
app:argType="string" />
</fragment>
</navigation>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorBackground">#000000</color>
<color name="colorOnBackground">#FFFFFF</color>
</resources>

Some files were not shown because too many files have changed in this diff Show more