Compare commits
104 commits
persist-lo
...
main
Author | SHA1 | Date | |
---|---|---|---|
10c1f42ca3 | |||
6fe4d99dfd | |||
dd76557232 | |||
092fb5bf3a | |||
1ab9c3db55 | |||
49de5ab1f3 | |||
d8b5f84e78 | |||
8b87ca0dd7 | |||
90a2550b0d | |||
6248f464d5 | |||
3b7a6fd57c | |||
2f5ebb28f6 | |||
6569ac64b2 | |||
13962a11d7 | |||
380280686a | |||
53df7da0aa | |||
9e8c65396e | |||
730bf3f7d8 | |||
80b18b242e | |||
1a4e5b8e9e | |||
29469df9c6 | |||
0d30c2b40d | |||
055437c052 | |||
9bc75b475e | |||
1de349e161 | |||
20d688d110 | |||
554937ae03 | |||
8e6e305ecf | |||
c1ba9dcf91 | |||
9bc220d5bb | |||
bb158f0120 | |||
8809d295c4 | |||
7de4996816 | |||
a4ef52f2cc | |||
ce14205c4f | |||
41607fba52 | |||
ca83a92158 | |||
c59faea4d4 | |||
d028c8cb2f | |||
f2f92b2318 | |||
e94b5a67c1 | |||
e386fcd82f | |||
cbbef5cf6f | |||
e8eb71e18b | |||
5abfe147f3 | |||
24ed864645 | |||
4f36b2f54c | |||
b6ee357407 | |||
154ccad9f1 | |||
d9032fb686 | |||
9898e09ac6 | |||
4932ac15f8 | |||
8fd4c8f8fd | |||
79b0f9996f | |||
7024ffc6d6 | |||
2f30d2fd6f | |||
0ade9be784 | |||
bf01006004 | |||
e5e072e4fe | |||
6d34bb6f94 | |||
9d26752d2b | |||
3f5c6b7ebf | |||
79d609f138 | |||
4271ded6aa | |||
7f945ba5fe | |||
12d5fcb834 | |||
3ac064c02c | |||
b0105dc5a4 | |||
ee5db64532 | |||
9b9ab9b971 | |||
428b6ec76b | |||
caade0c0d1 | |||
362a54c653 | |||
d1a9b114b6 | |||
cb973ba0ec | |||
8cb0f7c94d | |||
5035c287d2 | |||
5c2807ff9a | |||
a1b3672472 | |||
3033c65a8f | |||
7055229805 | |||
0dbcdb51b9 | |||
39451e67c2 | |||
7ee7687117 | |||
ef0ca65ee6 | |||
756251a64f | |||
c7a888a413 | |||
97181f8190 | |||
0195e4ee58 | |||
8ac6eb24a0 | |||
a1a8bb794e | |||
caf485ec61 | |||
50f0c299f5 | |||
09d073c5f3 | |||
51e5652475 | |||
01978548c6 | |||
b42b949bdb | |||
040777b99a | |||
307e7642b9 | |||
617035c424 | |||
9ce5e129a6 | |||
b1e698c9c9 | |||
c53ac549dc | |||
b6203cd6d1 |
35 changed files with 770 additions and 515 deletions
|
@ -2,8 +2,6 @@ name: Build & Test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
|
@ -17,7 +15,7 @@ jobs:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: https://git.wbrawner.com/gradle/actions/wrapper-validation@v3
|
uses: https://git.wbrawner.com/gradle/actions/wrapper-validation@v4
|
||||||
unit_tests:
|
unit_tests:
|
||||||
name: Run Unit Tests
|
name: Run Unit Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -32,10 +30,10 @@ jobs:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: https://git.wbrawner.com/android-actions/setup-android@v3
|
uses: https://git.wbrawner.com/android-actions/setup-android@v3
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: https://git.wbrawner.com/gradle/actions/setup-gradle@v4
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
uses: https://git.wbrawner.com/gradle/actions/setup-gradle@v3
|
run: ./gradlew check
|
||||||
with:
|
|
||||||
arguments: check
|
|
||||||
- name: Publish JUnit Results
|
- name: Publish JUnit Results
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
if: always()
|
if: always()
|
||||||
|
@ -47,7 +45,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Run UI Tests
|
name: Run UI Tests
|
||||||
needs:
|
needs:
|
||||||
- validate
|
- unit_tests
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: set up JDK
|
- name: set up JDK
|
||||||
|
@ -57,10 +55,10 @@ jobs:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: https://git.wbrawner.com/android-actions/setup-android@v3
|
uses: https://git.wbrawner.com/android-actions/setup-android@v3
|
||||||
- name: Build with Gradle
|
- name: Setup Gradle
|
||||||
uses: https://git.wbrawner.com/gradle/gradle-build-action@v2
|
uses: https://git.wbrawner.com/gradle/actions/setup-gradle@v4
|
||||||
with:
|
- name: Build APKs
|
||||||
arguments: assemblePlayDebug assemblePlayDebugAndroidTest
|
run: ./gradlew assemblePlayDebug assemblePlayDebugAndroidTest
|
||||||
- name: Grant execute permission for flank_auth.sh
|
- name: Grant execute permission for flank_auth.sh
|
||||||
run: chmod +x flank_auth.sh
|
run: chmod +x flank_auth.sh
|
||||||
- name: Add auth for flank
|
- name: Add auth for flank
|
||||||
|
@ -69,6 +67,4 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
./flank_auth.sh
|
./flank_auth.sh
|
||||||
- name: Run UI tests
|
- name: Run UI tests
|
||||||
uses: https://git.wbrawner.com/gradle/gradle-build-action@v2
|
run: ./gradlew runFlank
|
||||||
with:
|
|
||||||
arguments: runFlank
|
|
||||||
|
|
6
.github/workflows/android.yml
vendored
6
.github/workflows/android.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/gradle-build-action@v3
|
||||||
with:
|
with:
|
||||||
arguments: testPlayDebugUnitTest
|
arguments: testPlayDebugUnitTest
|
||||||
- name: Publish JUnit Results
|
- name: Publish JUnit Results
|
||||||
|
@ -61,7 +61,7 @@ jobs:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/gradle-build-action@v3
|
||||||
with:
|
with:
|
||||||
arguments: assemblePlayDebug assemblePlayDebugAndroidTest
|
arguments: assemblePlayDebug assemblePlayDebugAndroidTest
|
||||||
- name: Grant execute permission for flank_auth.sh
|
- name: Grant execute permission for flank_auth.sh
|
||||||
|
@ -72,6 +72,6 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
./flank_auth.sh
|
./flank_auth.sh
|
||||||
- name: Run UI tests
|
- name: Run UI tests
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/gradle-build-action@v3
|
||||||
with:
|
with:
|
||||||
arguments: runFlank
|
arguments: runFlank
|
||||||
|
|
|
@ -21,6 +21,7 @@ try {
|
||||||
keystoreProperties["keyPassword"] = ""
|
keystoreProperties["keyPassword"] = ""
|
||||||
keystoreProperties["storeFile"] = File.createTempFile("temp", ".tmp").absolutePath
|
keystoreProperties["storeFile"] = File.createTempFile("temp", ".tmp").absolutePath
|
||||||
keystoreProperties["storePassword"] = ""
|
keystoreProperties["storePassword"] = ""
|
||||||
|
keystoreProperties["publishCredentialsFile"] = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -37,18 +38,18 @@ android {
|
||||||
}
|
}
|
||||||
compileSdk = libs.versions.maxSdk.get().toInt()
|
compileSdk = libs.versions.maxSdk.get().toInt()
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "11"
|
||||||
}
|
}
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.wbrawner.simplemarkdown"
|
applicationId = "com.wbrawner.simplemarkdown"
|
||||||
minSdk = libs.versions.minSdk.get().toInt()
|
minSdk = libs.versions.minSdk.get().toInt()
|
||||||
targetSdk = libs.versions.maxSdk.get().toInt()
|
targetSdk = libs.versions.maxSdk.get().toInt()
|
||||||
versionCode = 41
|
versionCode = 45
|
||||||
versionName = "0.8.16"
|
versionName = "2024.10.0"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
testInstrumentationRunnerArguments["clearPackageData"] = "true"
|
testInstrumentationRunnerArguments["clearPackageData"] = "true"
|
||||||
buildConfigField("boolean", "ENABLE_CUSTOM_CSS", "true")
|
buildConfigField("boolean", "ENABLE_CUSTOM_CSS", "true")
|
||||||
|
@ -98,7 +99,11 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
disable += listOf("AndroidGradlePluginVersion", "GradleDependency")
|
disable += listOf(
|
||||||
|
"AndroidGradlePluginVersion",
|
||||||
|
"GradleDependency",
|
||||||
|
"ObsoleteLintCustomCheck"
|
||||||
|
)
|
||||||
warningsAsErrors = true
|
warningsAsErrors = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,6 +113,9 @@ play {
|
||||||
enabled.set(false)
|
enabled.set(false)
|
||||||
track.set("production")
|
track.set("production")
|
||||||
defaultToAppBundles.set(true)
|
defaultToAppBundles.set(true)
|
||||||
|
(keystoreProperties["publishCredentialsFile"] as? String)?.ifBlank { null }?.let {
|
||||||
|
serviceAccountCredentials.set(file(it))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -154,7 +162,6 @@ dependencies {
|
||||||
androidTestImplementation(libs.androidx.core)
|
androidTestImplementation(libs.androidx.core)
|
||||||
androidTestImplementation(libs.androidx.monitor)
|
androidTestImplementation(libs.androidx.monitor)
|
||||||
androidTestImplementation(libs.junit)
|
androidTestImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.hamcrest.core)
|
|
||||||
implementation(libs.androidx.activity.ktx)
|
implementation(libs.androidx.activity.ktx)
|
||||||
implementation(libs.androidx.activity.ktx)
|
implementation(libs.androidx.activity.ktx)
|
||||||
implementation(libs.androidx.animation.core)
|
implementation(libs.androidx.animation.core)
|
||||||
|
@ -181,7 +188,7 @@ fladle {
|
||||||
environmentVariables.put("clearPackageData", "true")
|
environmentVariables.put("clearPackageData", "true")
|
||||||
testTimeout.set("7m")
|
testTimeout.set("7m")
|
||||||
devices.add(
|
devices.add(
|
||||||
mapOf("model" to "NexusLowRes", "version" to "29")
|
mapOf("model" to "Pixel2.arm", "version" to "33")
|
||||||
)
|
)
|
||||||
projectId.set("simplemarkdown")
|
projectId.set("simplemarkdown")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,43 +5,17 @@ import android.app.Instrumentation
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.compose.ui.semantics.SemanticsProperties
|
|
||||||
import androidx.compose.ui.semantics.getOrNull
|
|
||||||
import androidx.compose.ui.test.SemanticsMatcher
|
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteraction
|
|
||||||
import androidx.compose.ui.test.assert
|
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
|
||||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
|
||||||
import androidx.compose.ui.test.hasAnyDescendant
|
|
||||||
import androidx.compose.ui.test.hasAnySibling
|
|
||||||
import androidx.compose.ui.test.hasContentDescription
|
|
||||||
import androidx.compose.ui.test.hasSetTextAction
|
|
||||||
import androidx.compose.ui.test.hasText
|
|
||||||
import androidx.compose.ui.test.isDialog
|
|
||||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
|
||||||
import androidx.compose.ui.test.junit4.createEmptyComposeRule
|
import androidx.compose.ui.test.junit4.createEmptyComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
|
||||||
import androidx.compose.ui.test.performClick
|
|
||||||
import androidx.compose.ui.test.performTextReplacement
|
|
||||||
import androidx.compose.ui.test.printToLog
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.test.core.app.ActivityScenario
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
||||||
import androidx.test.espresso.action.ViewActions.*
|
|
||||||
import androidx.test.espresso.intent.Intents.intending
|
import androidx.test.espresso.intent.Intents.intending
|
||||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||||
import androidx.test.espresso.intent.rule.IntentsRule
|
import androidx.test.espresso.intent.rule.IntentsRule
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||||
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
|
|
||||||
import androidx.test.espresso.web.sugar.Web.onWebView
|
|
||||||
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
|
|
||||||
import androidx.test.espresso.web.webdriver.DriverAtoms.getText
|
|
||||||
import androidx.test.espresso.web.webdriver.Locator
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||||
|
import com.wbrawner.simplemarkdown.robot.onMainScreen
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.hamcrest.CoreMatchers.containsString
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
@ -79,13 +53,13 @@ class MarkdownTests {
|
||||||
@Test
|
@Test
|
||||||
fun editAndPreviewMarkdownTest() {
|
fun editAndPreviewMarkdownTest() {
|
||||||
ActivityScenario.launch(MainActivity::class.java)
|
ActivityScenario.launch(MainActivity::class.java)
|
||||||
composeRule.typeMarkdown("# Header test")
|
onMainScreen(composeRule) {
|
||||||
composeRule.checkMarkdownEquals("# Header test")
|
typeMarkdown("# Header test")
|
||||||
composeRule.openPreview()
|
checkMarkdownEquals("# Header test")
|
||||||
onWebView(isAssignableFrom(WebView::class.java))
|
openPreview()
|
||||||
.forceJavascriptEnabled()
|
} onPreview {
|
||||||
.withElement(findElement(Locator.TAG_NAME, "h1"))
|
verifyH1("Header test")
|
||||||
.check(webMatches(getText(), containsString("Header test")))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -98,104 +72,138 @@ class MarkdownTests {
|
||||||
})
|
})
|
||||||
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
|
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
|
||||||
ActivityScenario.launch(MainActivity::class.java)
|
ActivityScenario.launch(MainActivity::class.java)
|
||||||
composeRule.openMenu()
|
onMainScreen(composeRule) {
|
||||||
composeRule.clickOpenMenuItem()
|
openMenu()
|
||||||
composeRule.checkMarkdownEquals(markdownText)
|
clickOpenMenuItem()
|
||||||
composeRule.openMenu()
|
checkMarkdownEquals(markdownText)
|
||||||
composeRule.clickNewMenuItem()
|
openMenu()
|
||||||
composeRule.verifyDialogIsNotShown()
|
clickNewMenuItem()
|
||||||
composeRule.checkMarkdownEquals("")
|
verifyDialogIsNotShown()
|
||||||
|
checkMarkdownEquals("")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun editThenNewMarkdownTest() {
|
fun editThenNewMarkdownTest() {
|
||||||
ActivityScenario.launch(MainActivity::class.java)
|
ActivityScenario.launch(MainActivity::class.java)
|
||||||
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
||||||
composeRule.typeMarkdown(markdownText)
|
onMainScreen(composeRule) {
|
||||||
composeRule.openMenu()
|
typeMarkdown(markdownText)
|
||||||
composeRule.clickNewMenuItem()
|
openMenu()
|
||||||
composeRule.onNode(isDialog()).printToLog("TestDebugging")
|
clickNewMenuItem()
|
||||||
composeRule.verifyDialogIsShown("Would you like to save your changes?")
|
verifyDialogIsShown("Would you like to save your changes?")
|
||||||
composeRule.discardChanges()
|
discardChanges()
|
||||||
composeRule.checkMarkdownEquals("")
|
checkMarkdownEquals("")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun saveMarkdownWithFileUriTest() = runTest {
|
fun saveMarkdownWithFileUriTest() = runTest {
|
||||||
ActivityScenario.launch(MainActivity::class.java)
|
ActivityScenario.launch(MainActivity::class.java)
|
||||||
composeRule.checkTitleEquals("Untitled.md")
|
onMainScreen(composeRule) {
|
||||||
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
checkTitleEquals("Untitled.md")
|
||||||
composeRule.typeMarkdown(markdownText)
|
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
||||||
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
|
typeMarkdown(markdownText)
|
||||||
data = Uri.fromFile(file)
|
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
|
||||||
})
|
data = Uri.fromFile(file)
|
||||||
intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult)
|
})
|
||||||
composeRule.openMenu()
|
intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult)
|
||||||
composeRule.clickSaveMenuItem()
|
openMenu()
|
||||||
composeRule.awaitIdle()
|
clickSaveMenuItem()
|
||||||
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
|
awaitIdle()
|
||||||
composeRule.checkTitleEquals("temp.md")
|
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
|
||||||
|
checkTitleEquals("temp.md")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun saveMarkdownWithContentUriTest() = runTest {
|
fun saveMarkdownWithContentUriTest() = runTest {
|
||||||
ActivityScenario.launch(MainActivity::class.java)
|
ActivityScenario.launch(MainActivity::class.java)
|
||||||
composeRule.checkTitleEquals("Untitled.md")
|
onMainScreen(composeRule) {
|
||||||
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
checkTitleEquals("Untitled.md")
|
||||||
composeRule.typeMarkdown(markdownText)
|
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
||||||
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
|
typeMarkdown(markdownText)
|
||||||
data = FileProvider.getUriForFile(
|
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
|
||||||
getApplicationContext(),
|
data = FileProvider.getUriForFile(
|
||||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
getApplicationContext(),
|
||||||
file
|
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||||
)
|
file
|
||||||
})
|
)
|
||||||
intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult)
|
})
|
||||||
composeRule.openMenu()
|
intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult)
|
||||||
composeRule.clickSaveMenuItem()
|
openMenu()
|
||||||
composeRule.awaitIdle()
|
clickSaveMenuItem()
|
||||||
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
|
awaitIdle()
|
||||||
composeRule.checkTitleEquals("temp.md")
|
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
|
||||||
|
checkTitleEquals("temp.md")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun loadMarkdownWithFileUriTest() = runTest {
|
fun loadMarkdownWithFileUriTest() = runTest {
|
||||||
ActivityScenario.launch(MainActivity::class.java)
|
ActivityScenario.launch(MainActivity::class.java)
|
||||||
composeRule.checkTitleEquals("Untitled.md")
|
onMainScreen(composeRule) {
|
||||||
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
checkTitleEquals("Untitled.md")
|
||||||
file.outputStream().writer().use { it.write(markdownText) }
|
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
||||||
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
|
file.outputStream().writer().use { it.write(markdownText) }
|
||||||
data = Uri.fromFile(file)
|
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
|
||||||
})
|
data = Uri.fromFile(file)
|
||||||
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
|
})
|
||||||
composeRule.openMenu()
|
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
|
||||||
composeRule.clickOpenMenuItem()
|
openMenu()
|
||||||
composeRule.awaitIdle()
|
clickOpenMenuItem()
|
||||||
composeRule.checkMarkdownEquals(markdownText)
|
awaitIdle()
|
||||||
composeRule.checkTitleEquals("temp.md")
|
checkMarkdownEquals(markdownText)
|
||||||
|
checkTitleEquals("temp.md")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun loadMarkdownWithContentUriTest() = runTest {
|
fun loadMarkdownWithContentUriTest() = runTest {
|
||||||
ActivityScenario.launch(MainActivity::class.java)
|
ActivityScenario.launch(MainActivity::class.java)
|
||||||
composeRule.checkTitleEquals("Untitled.md")
|
onMainScreen(composeRule) {
|
||||||
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
checkTitleEquals("Untitled.md")
|
||||||
file.outputStream().writer().use { it.write(markdownText) }
|
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
||||||
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
|
file.outputStream().writer().use { it.write(markdownText) }
|
||||||
data = FileProvider.getUriForFile(
|
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
|
||||||
getApplicationContext(),
|
data = FileProvider.getUriForFile(
|
||||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
getApplicationContext(),
|
||||||
file
|
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||||
)
|
file
|
||||||
})
|
)
|
||||||
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
|
})
|
||||||
composeRule.openMenu()
|
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
|
||||||
composeRule.clickOpenMenuItem()
|
openMenu()
|
||||||
composeRule.awaitIdle()
|
clickOpenMenuItem()
|
||||||
composeRule.checkMarkdownEquals(markdownText)
|
awaitIdle()
|
||||||
composeRule.checkTitleEquals("temp.md")
|
checkMarkdownEquals(markdownText)
|
||||||
|
checkTitleEquals("temp.md")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun launchWithContentUriTest() = runTest {
|
||||||
|
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
||||||
|
file.outputStream().writer().use { it.write(markdownText) }
|
||||||
|
val fileUri = FileProvider.getUriForFile(
|
||||||
|
getApplicationContext(),
|
||||||
|
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
ActivityScenario.launch<MainActivity>(
|
||||||
|
Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
fileUri,
|
||||||
|
getInstrumentation().targetContext,
|
||||||
|
MainActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onMainScreen(composeRule) {
|
||||||
|
awaitIdle()
|
||||||
|
checkMarkdownEquals(markdownText)
|
||||||
|
checkTitleEquals("temp.md")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun openEditAndSaveMarkdownTest() = runTest {
|
fun openEditAndSaveMarkdownTest() = runTest {
|
||||||
|
@ -206,84 +214,41 @@ class MarkdownTests {
|
||||||
})
|
})
|
||||||
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
|
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
|
||||||
ActivityScenario.launch(MainActivity::class.java)
|
ActivityScenario.launch(MainActivity::class.java)
|
||||||
composeRule.checkTitleEquals("Untitled.md")
|
onMainScreen(composeRule) {
|
||||||
composeRule.openMenu()
|
checkTitleEquals("Untitled.md")
|
||||||
composeRule.clickOpenMenuItem()
|
openMenu()
|
||||||
composeRule.awaitIdle()
|
clickOpenMenuItem()
|
||||||
composeRule.verifyTextIsShown("Successfully loaded temp.md")
|
awaitIdle()
|
||||||
composeRule.checkMarkdownEquals(markdownText)
|
verifyTextIsShown("Successfully loaded temp.md")
|
||||||
composeRule.checkTitleEquals("temp.md")
|
checkMarkdownEquals(markdownText)
|
||||||
val additionalText = "# More info\n\nThis is some additional text"
|
checkTitleEquals("temp.md")
|
||||||
composeRule.typeMarkdown(additionalText)
|
val additionalText = "# More info\n\nThis is some additional text"
|
||||||
composeRule.openMenu()
|
typeMarkdown(additionalText)
|
||||||
composeRule.clickSaveMenuItem()
|
openMenu()
|
||||||
composeRule.awaitIdle()
|
clickSaveMenuItem()
|
||||||
composeRule.verifyTextIsShown("Successfully saved temp.md")
|
awaitIdle()
|
||||||
assertEquals(additionalText, file.inputStream().reader().use(Reader::readText))
|
verifyTextIsShown("Successfully saved temp.md")
|
||||||
composeRule.checkTitleEquals("temp.md")
|
assertEquals(additionalText, file.inputStream().reader().use(Reader::readText))
|
||||||
}
|
checkTitleEquals("temp.md")
|
||||||
|
|
||||||
private fun ComposeTestRule.checkTitleEquals(title: String) =
|
|
||||||
onNode(hasAnySibling(hasContentDescription("Main Menu")).and(hasText(title)))
|
|
||||||
.waitUntilIsDisplayed()
|
|
||||||
|
|
||||||
private fun ComposeTestRule.typeMarkdown(markdown: String) =
|
|
||||||
onNode(hasSetTextAction()).performTextReplacement(markdown)
|
|
||||||
|
|
||||||
|
|
||||||
private fun ComposeTestRule.checkMarkdownEquals(markdown: String) {
|
|
||||||
val markdownMatcher = SemanticsMatcher("Markdown = [$markdown]") {
|
|
||||||
it.config.getOrNull(SemanticsProperties.EditableText)?.text == markdown
|
|
||||||
}
|
|
||||||
onNode(hasSetTextAction()).waitUntil {
|
|
||||||
assert(markdownMatcher)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ComposeTestRule.openPreview() = onNodeWithText("Preview").performClick()
|
@Test
|
||||||
|
fun editAndViewHelpMarkdownTest() = runTest {
|
||||||
private fun ComposeTestRule.openMenu() =
|
ActivityScenario.launch(MainActivity::class.java)
|
||||||
onNodeWithContentDescription("Editor Actions").performClick()
|
onMainScreen(composeRule) {
|
||||||
|
checkTitleEquals("Untitled.md")
|
||||||
private fun ComposeTestRule.clickOpenMenuItem() = onNodeWithText("Open").performClick()
|
typeMarkdown("# Header test")
|
||||||
|
checkMarkdownEquals("# Header test")
|
||||||
private fun ComposeTestRule.clickNewMenuItem() = onNodeWithText("New").performClick()
|
openDrawer()
|
||||||
|
} onNavigationDrawer {
|
||||||
private fun ComposeTestRule.clickSaveMenuItem() = onNodeWithText("Save").performClick()
|
openHelpPage()
|
||||||
|
} onHelpScreen {
|
||||||
private fun ComposeTestRule.verifyDialogIsShown(text: String) =
|
checkTitleEquals("Help")
|
||||||
onNode(isDialog().and(hasAnyDescendant(hasText(text)))).waitUntilIsDisplayed()
|
verifyH1("Headings/Titles")
|
||||||
|
pressBack()
|
||||||
private fun ComposeTestRule.verifyDialogIsNotShown() =
|
} onMainScreen {
|
||||||
onNode(isDialog()).waitUntilIsNotDisplayed()
|
checkMarkdownEquals("# Header test")
|
||||||
|
|
||||||
private fun ComposeTestRule.discardChanges() = onNodeWithText("No").performClick()
|
|
||||||
|
|
||||||
private fun ComposeTestRule.verifyTextIsShown(text: String) =
|
|
||||||
onNodeWithText(text).waitUntilIsDisplayed()
|
|
||||||
|
|
||||||
private val ASSERTION_TIMEOUT = 5_000L
|
|
||||||
|
|
||||||
private fun SemanticsNodeInteraction.waitUntil(assertion: SemanticsNodeInteraction.() -> Unit) {
|
|
||||||
val start = System.currentTimeMillis()
|
|
||||||
lateinit var assertionError: AssertionError
|
|
||||||
while (System.currentTimeMillis() - start < ASSERTION_TIMEOUT) {
|
|
||||||
try {
|
|
||||||
assertion()
|
|
||||||
return
|
|
||||||
} catch (e: AssertionError) {
|
|
||||||
assertionError = e
|
|
||||||
Thread.sleep(10)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw assertionError
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun SemanticsNodeInteraction.waitUntilIsDisplayed() = waitUntil {
|
|
||||||
assertIsDisplayed()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun SemanticsNodeInteraction.waitUntilIsNotDisplayed() = waitUntil {
|
|
||||||
assertIsNotDisplayed()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@
|
||||||
<application
|
<application
|
||||||
android:name=".MarkdownApplication"
|
android:name=".MarkdownApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
|
@ -14,7 +15,7 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.App.Starting"
|
android:theme="@style/Theme.App.Starting"
|
||||||
tools:ignore="AllowBackup"
|
tools:ignore="AllowBackup"
|
||||||
tools:targetApi="n">
|
tools:targetApi="tiramisu">
|
||||||
<activity android:name=".MainActivity"
|
<activity android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name_short">
|
android:label="@string/app_name_short">
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package com.wbrawner.simplemarkdown
|
package com.wbrawner.simplemarkdown
|
||||||
|
|
||||||
|
import android.app.ComponentCaller
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
@ -8,12 +10,11 @@ import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
import androidx.compose.animation.EnterTransition
|
|
||||||
import androidx.compose.animation.core.EaseIn
|
import androidx.compose.animation.core.EaseIn
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Help
|
import androidx.compose.material.icons.automirrored.filled.Help
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
@ -27,11 +28,13 @@ import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
@ -43,6 +46,7 @@ import com.wbrawner.simplemarkdown.ui.SettingsScreen
|
||||||
import com.wbrawner.simplemarkdown.ui.SupportScreen
|
import com.wbrawner.simplemarkdown.ui.SupportScreen
|
||||||
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
|
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
|
||||||
import com.wbrawner.simplemarkdown.utility.Preference
|
import com.wbrawner.simplemarkdown.utility.Preference
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.acra.ACRA
|
import org.acra.ACRA
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback {
|
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback {
|
||||||
|
@ -61,8 +65,6 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
setContent {
|
setContent {
|
||||||
val autosaveEnabled by preferenceHelper.observe<Boolean>(Preference.AUTOSAVE_ENABLED)
|
val autosaveEnabled by preferenceHelper.observe<Boolean>(Preference.AUTOSAVE_ENABLED)
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
val readabilityEnabled by preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED)
|
|
||||||
.collectAsState()
|
|
||||||
val darkModePreference by preferenceHelper.observe<String>(Preference.DARK_MODE)
|
val darkModePreference by preferenceHelper.observe<String>(Preference.DARK_MODE)
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
LaunchedEffect(darkModePreference) {
|
LaunchedEffect(darkModePreference) {
|
||||||
|
@ -92,6 +94,10 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
LaunchedEffect(errorReporterPreference) {
|
LaunchedEffect(errorReporterPreference) {
|
||||||
ACRA.errorReporter.setEnabled(errorReporterPreference)
|
ACRA.errorReporter.setEnabled(errorReporterPreference)
|
||||||
}
|
}
|
||||||
|
val intentData = remember(intent) { intent?.data }
|
||||||
|
LaunchedEffect(intentData) {
|
||||||
|
viewModel.load(intentData?.toString())
|
||||||
|
}
|
||||||
val windowSizeClass = calculateWindowSizeClass(this)
|
val windowSizeClass = calculateWindowSizeClass(this)
|
||||||
SimpleMarkdownTheme {
|
SimpleMarkdownTheme {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
@ -108,13 +114,9 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
towards = AnimatedContentTransitionScope.SlideDirection.Start
|
towards = AnimatedContentTransitionScope.SlideDirection.Start
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
popEnterTransition = { EnterTransition.None },
|
popEnterTransition = { fadeIn() },
|
||||||
popExitTransition = {
|
popExitTransition = {
|
||||||
fadeOut(
|
scaleOut(targetScale = 0.9f) + slideOutOfContainer(
|
||||||
animationSpec = tween(
|
|
||||||
300, easing = LinearEasing
|
|
||||||
)
|
|
||||||
) + slideOutOfContainer(
|
|
||||||
animationSpec = tween(300, easing = EaseIn),
|
animationSpec = tween(300, easing = EaseIn),
|
||||||
towards = AnimatedContentTransitionScope.SlideDirection.End
|
towards = AnimatedContentTransitionScope.SlideDirection.End
|
||||||
)
|
)
|
||||||
|
@ -126,7 +128,6 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
enableWideLayout = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded,
|
enableWideLayout = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded,
|
||||||
enableAutosave = autosaveEnabled,
|
enableAutosave = autosaveEnabled,
|
||||||
enableReadability = readabilityEnabled
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Route.SETTINGS.path) {
|
composable(Route.SETTINGS.path) {
|
||||||
|
@ -160,6 +161,15 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||||
|
super.onNewIntent(intent, caller)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
intent.data?.let {
|
||||||
|
viewModel.load(it.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Route(
|
enum class Route(
|
||||||
|
|
|
@ -38,7 +38,9 @@ class MarkdownApplication : Application() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Timber.plant(ErrorReporterTree.create(this))
|
coroutineScope.launch {
|
||||||
|
Timber.plant(ErrorReporterTree.create(this@MarkdownApplication))
|
||||||
|
}
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
ReviewHelper.init(this)
|
ReviewHelper.init(this)
|
||||||
fileHelper = AndroidFileHelper(this)
|
fileHelper = AndroidFileHelper(this)
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
package com.wbrawner.simplemarkdown
|
package com.wbrawner.simplemarkdown
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
|
import com.wbrawner.simplemarkdown.core.LocalOnlyException
|
||||||
|
import com.wbrawner.simplemarkdown.model.Readability
|
||||||
import com.wbrawner.simplemarkdown.utility.FileHelper
|
import com.wbrawner.simplemarkdown.utility.FileHelper
|
||||||
import com.wbrawner.simplemarkdown.utility.Preference
|
import com.wbrawner.simplemarkdown.utility.Preference
|
||||||
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
|
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
|
||||||
|
@ -13,7 +19,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -23,21 +28,17 @@ import java.net.URI
|
||||||
|
|
||||||
data class EditorState(
|
data class EditorState(
|
||||||
val fileName: String = "Untitled.md",
|
val fileName: String = "Untitled.md",
|
||||||
val markdown: String = "",
|
val markdown: TextFieldValue = TextFieldValue(""),
|
||||||
val path: URI? = null,
|
val path: URI? = null,
|
||||||
val toast: ParameterizedText? = null,
|
val toast: ParameterizedText? = null,
|
||||||
val alert: AlertDialogModel? = null,
|
val alert: AlertDialogModel? = null,
|
||||||
val saveCallback: (() -> Unit)? = null,
|
val saveCallback: (() -> Unit)? = null,
|
||||||
/**
|
|
||||||
* Used to signal to the view that it should reload due to an external change, like loading
|
|
||||||
* a new file
|
|
||||||
*/
|
|
||||||
val reloadToggle: Int = 0,
|
|
||||||
val lockSwiping: Boolean = false,
|
val lockSwiping: Boolean = false,
|
||||||
private val initialMarkdown: String = "",
|
val enableReadability: Boolean = false,
|
||||||
|
val initialMarkdown: String = "",
|
||||||
) {
|
) {
|
||||||
val dirty: Boolean
|
val dirty: Boolean
|
||||||
get() = markdown != initialMarkdown
|
get() = markdown.text != initialMarkdown
|
||||||
}
|
}
|
||||||
|
|
||||||
class MarkdownViewModel(
|
class MarkdownViewModel(
|
||||||
|
@ -49,32 +50,43 @@ class MarkdownViewModel(
|
||||||
private val saveMutex = Mutex()
|
private val saveMutex = Mutex()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
|
||||||
load(null)
|
|
||||||
}
|
|
||||||
preferenceHelper.observe<Boolean>(Preference.LOCK_SWIPING)
|
preferenceHelper.observe<Boolean>(Preference.LOCK_SWIPING)
|
||||||
.onEach {
|
.onEach {
|
||||||
_state.value = _state.value.copy(lockSwiping = it)
|
updateState { copy(lockSwiping = it) }
|
||||||
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED)
|
||||||
|
.onEach {
|
||||||
|
updateState {
|
||||||
|
copy(
|
||||||
|
enableReadability = it,
|
||||||
|
markdown = markdown.copy(annotatedString = markdown.text.annotate(it)),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateMarkdown(markdown: String?) {
|
fun updateMarkdown(markdown: String?) = updateMarkdown(TextFieldValue(markdown.orEmpty()))
|
||||||
_state.value = _state.value.copy(
|
|
||||||
markdown = markdown ?: "",
|
fun updateMarkdown(markdown: TextFieldValue) {
|
||||||
)
|
updateState {
|
||||||
|
copy(
|
||||||
|
markdown = markdown.copy(annotatedString = markdown.text.annotate(enableReadability)),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissToast() {
|
fun dismissToast() {
|
||||||
_state.value = _state.value.copy(toast = null)
|
updateState { copy(toast = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissAlert() {
|
fun dismissAlert() {
|
||||||
_state.value = _state.value.copy(alert = null)
|
updateState { copy(alert = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unsetSaveCallback() {
|
private fun unsetSaveCallback() {
|
||||||
_state.value = _state.value.copy(saveCallback = null)
|
updateState { copy(saveCallback = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun load(loadPath: String?) {
|
suspend fun load(loadPath: String?) {
|
||||||
|
@ -97,25 +109,30 @@ class MarkdownViewModel(
|
||||||
val uri = URI.create(actualLoadPath)
|
val uri = URI.create(actualLoadPath)
|
||||||
fileHelper.open(uri)
|
fileHelper.open(uri)
|
||||||
?.let { (name, content) ->
|
?.let { (name, content) ->
|
||||||
val currentState = _state.value
|
updateState {
|
||||||
_state.value = currentState.copy(
|
copy(
|
||||||
path = uri,
|
path = uri,
|
||||||
fileName = name,
|
fileName = name,
|
||||||
markdown = content,
|
markdown = TextFieldValue(content),
|
||||||
initialMarkdown = content,
|
initialMarkdown = content,
|
||||||
reloadToggle = currentState.reloadToggle.inv(),
|
toast = ParameterizedText(R.string.file_loaded, arrayOf(name))
|
||||||
toast = ParameterizedText(R.string.file_loaded, arrayOf(name))
|
)
|
||||||
)
|
}
|
||||||
preferenceHelper[Preference.AUTOSAVE_URI] = actualLoadPath
|
preferenceHelper[Preference.AUTOSAVE_URI] = actualLoadPath
|
||||||
} ?: throw IllegalStateException("Opened file was null")
|
} ?: throw IllegalStateException("Opened file was null")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Failed to open file at path: $actualLoadPath")
|
Timber.e(LocalOnlyException(e), "Failed to open file at path: $actualLoadPath")
|
||||||
_state.value = _state.value.copy(
|
updateState {
|
||||||
alert = AlertDialogModel(
|
copy(
|
||||||
text = ParameterizedText(R.string.file_load_error),
|
alert = AlertDialogModel(
|
||||||
confirmButton = AlertDialogModel.ButtonModel(ParameterizedText(R.string.ok), onClick = ::dismissAlert)
|
text = ParameterizedText(R.string.file_load_error),
|
||||||
|
confirmButton = AlertDialogModel.ButtonModel(
|
||||||
|
ParameterizedText(R.string.ok),
|
||||||
|
onClick = ::dismissAlert
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,35 +144,44 @@ class MarkdownViewModel(
|
||||||
?: run {
|
?: run {
|
||||||
Timber.w("Attempted to save file with empty path")
|
Timber.w("Attempted to save file with empty path")
|
||||||
if (interactive) {
|
if (interactive) {
|
||||||
_state.value = _state.value.copy(saveCallback = ::unsetSaveCallback)
|
updateState {
|
||||||
|
copy(saveCallback = ::unsetSaveCallback)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return@withLock false
|
return@withLock false
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Timber.i("Saving file to $actualSavePath...")
|
Timber.i("Saving file to $actualSavePath...")
|
||||||
val currentState = _state.value
|
val currentState = _state.value
|
||||||
val name = fileHelper.save(actualSavePath, currentState.markdown)
|
val name = fileHelper.save(actualSavePath, currentState.markdown.text)
|
||||||
_state.value = currentState.copy(
|
updateState {
|
||||||
fileName = name,
|
currentState.copy(
|
||||||
path = actualSavePath,
|
fileName = name,
|
||||||
initialMarkdown = currentState.markdown,
|
path = actualSavePath,
|
||||||
toast = if (interactive) ParameterizedText(R.string.file_saved, arrayOf(name)) else null
|
initialMarkdown = currentState.markdown.text,
|
||||||
)
|
toast = if (interactive) ParameterizedText(
|
||||||
|
R.string.file_saved,
|
||||||
|
arrayOf(name)
|
||||||
|
) else null
|
||||||
|
)
|
||||||
|
}
|
||||||
Timber.i("Saved file $name to uri $actualSavePath")
|
Timber.i("Saved file $name to uri $actualSavePath")
|
||||||
Timber.i("Persisting autosave uri in shared prefs: $actualSavePath")
|
Timber.i("Persisting autosave uri in shared prefs: $actualSavePath")
|
||||||
preferenceHelper[Preference.AUTOSAVE_URI] = actualSavePath
|
preferenceHelper[Preference.AUTOSAVE_URI] = actualSavePath
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Failed to save file to $actualSavePath")
|
Timber.e(e, "Failed to save file to $actualSavePath")
|
||||||
_state.value = _state.value.copy(
|
updateState {
|
||||||
alert = AlertDialogModel(
|
copy(
|
||||||
text = ParameterizedText(R.string.file_save_error),
|
alert = AlertDialogModel(
|
||||||
confirmButton = AlertDialogModel.ButtonModel(
|
text = ParameterizedText(R.string.file_save_error),
|
||||||
text = ParameterizedText(R.string.ok),
|
confirmButton = AlertDialogModel.ButtonModel(
|
||||||
onClick = ::dismissAlert
|
text = ParameterizedText(R.string.ok),
|
||||||
|
onClick = ::dismissAlert
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,7 +213,7 @@ class MarkdownViewModel(
|
||||||
// to an internal storage location, thus marking it as not dirty, but no longer able to
|
// to an internal storage location, thus marking it as not dirty, but no longer able to
|
||||||
// access the file if the accidentally go to create a new file without properly saving
|
// access the file if the accidentally go to create a new file without properly saving
|
||||||
// the current one
|
// the current one
|
||||||
fileHelper.save(file, _state.value.markdown)
|
fileHelper.save(file, _state.value.markdown.text)
|
||||||
preferenceHelper[Preference.AUTOSAVE_URI] = file
|
preferenceHelper[Preference.AUTOSAVE_URI] = file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,33 +222,35 @@ class MarkdownViewModel(
|
||||||
fun reset(untitledFileName: String, force: Boolean = false) {
|
fun reset(untitledFileName: String, force: Boolean = false) {
|
||||||
Timber.i("Resetting view model to default state")
|
Timber.i("Resetting view model to default state")
|
||||||
if (!force && _state.value.dirty) {
|
if (!force && _state.value.dirty) {
|
||||||
_state.value = _state.value.copy(alert = AlertDialogModel(
|
updateState {
|
||||||
text = ParameterizedText(R.string.prompt_save_changes),
|
copy(alert = AlertDialogModel(
|
||||||
confirmButton = AlertDialogModel.ButtonModel(
|
text = ParameterizedText(R.string.prompt_save_changes),
|
||||||
text = ParameterizedText(R.string.yes),
|
confirmButton = AlertDialogModel.ButtonModel(
|
||||||
onClick = {
|
text = ParameterizedText(R.string.yes),
|
||||||
_state.value = _state.value.copy(
|
onClick = {
|
||||||
saveCallback = {
|
_state.value = _state.value.copy(
|
||||||
reset(untitledFileName, false)
|
saveCallback = {
|
||||||
}
|
reset(untitledFileName, false)
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
),
|
}
|
||||||
dismissButton = AlertDialogModel.ButtonModel(
|
),
|
||||||
text = ParameterizedText(R.string.no),
|
dismissButton = AlertDialogModel.ButtonModel(
|
||||||
onClick = {
|
text = ParameterizedText(R.string.no),
|
||||||
reset(untitledFileName, true)
|
onClick = {
|
||||||
}
|
reset(untitledFileName, true)
|
||||||
)
|
}
|
||||||
))
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_state.value =
|
updateState {
|
||||||
EditorState(
|
EditorState(
|
||||||
fileName = untitledFileName,
|
fileName = untitledFileName,
|
||||||
reloadToggle = _state.value.reloadToggle.inv(),
|
|
||||||
lockSwiping = preferenceHelper[Preference.LOCK_SWIPING] as Boolean
|
lockSwiping = preferenceHelper[Preference.LOCK_SWIPING] as Boolean
|
||||||
)
|
)
|
||||||
|
}
|
||||||
Timber.i("Removing autosave uri from shared prefs")
|
Timber.i("Removing autosave uri from shared prefs")
|
||||||
preferenceHelper[Preference.AUTOSAVE_URI] = null
|
preferenceHelper[Preference.AUTOSAVE_URI] = null
|
||||||
}
|
}
|
||||||
|
@ -231,6 +259,10 @@ class MarkdownViewModel(
|
||||||
preferenceHelper[Preference.LOCK_SWIPING] = enabled
|
preferenceHelper[Preference.LOCK_SWIPING] = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateState(block: EditorState.() -> EditorState) {
|
||||||
|
_state.value = _state.value.block()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun factory(
|
fun factory(
|
||||||
fileHelper: FileHelper,
|
fileHelper: FileHelper,
|
||||||
|
@ -273,4 +305,17 @@ data class ParameterizedText(@StringRes val text: Int, val params: Array<Any> =
|
||||||
result = 31 * result + params.contentHashCode()
|
result = 31 * result + params.contentHashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.annotate(enableReadability: Boolean): AnnotatedString {
|
||||||
|
if (!enableReadability) return AnnotatedString(this)
|
||||||
|
val readability = Readability(this)
|
||||||
|
val annotated = AnnotatedString.Builder(this)
|
||||||
|
for (sentence in readability.sentences()) {
|
||||||
|
var color = Color.Transparent
|
||||||
|
if (sentence.syllableCount() > 25) color = Color(229, 232, 42, 100)
|
||||||
|
if (sentence.syllableCount() > 35) color = Color(193, 66, 66, 100)
|
||||||
|
annotated.addStyle(SpanStyle(background = color), sentence.start(), sentence.end())
|
||||||
|
}
|
||||||
|
return annotated.toAnnotatedString()
|
||||||
}
|
}
|
|
@ -4,7 +4,6 @@ import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
@ -53,6 +52,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
@ -76,13 +76,10 @@ fun MainScreen(
|
||||||
viewModel: MarkdownViewModel,
|
viewModel: MarkdownViewModel,
|
||||||
enableWideLayout: Boolean,
|
enableWideLayout: Boolean,
|
||||||
enableAutosave: Boolean,
|
enableAutosave: Boolean,
|
||||||
enableReadability: Boolean
|
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val fileName by viewModel.collectAsState(EditorState::fileName, "")
|
val fileName by viewModel.collectAsState(EditorState::fileName, "")
|
||||||
val initialMarkdown by viewModel.collectAsState(EditorState::markdown, "")
|
val markdown by viewModel.collectAsState(EditorState::markdown, TextFieldValue(""))
|
||||||
val reloadToggle by viewModel.collectAsState(EditorState::reloadToggle, 0)
|
|
||||||
val markdown by viewModel.collectAsState(EditorState::markdown, "")
|
|
||||||
val dirty by viewModel.collectAsState(EditorState::dirty, false)
|
val dirty by viewModel.collectAsState(EditorState::dirty, false)
|
||||||
val alert by viewModel.collectAsState(EditorState::alert, null)
|
val alert by viewModel.collectAsState(EditorState::alert, null)
|
||||||
val saveCallback by viewModel.collectAsState(EditorState::saveCallback, null)
|
val saveCallback by viewModel.collectAsState(EditorState::saveCallback, null)
|
||||||
|
@ -98,8 +95,6 @@ fun MainScreen(
|
||||||
MainScreen(
|
MainScreen(
|
||||||
dirty = dirty,
|
dirty = dirty,
|
||||||
fileName = fileName,
|
fileName = fileName,
|
||||||
reloadToggle = reloadToggle,
|
|
||||||
initialMarkdown = initialMarkdown,
|
|
||||||
markdown = markdown,
|
markdown = markdown,
|
||||||
setMarkdown = viewModel::updateMarkdown,
|
setMarkdown = viewModel::updateMarkdown,
|
||||||
lockSwiping = lockSwiping,
|
lockSwiping = lockSwiping,
|
||||||
|
@ -127,7 +122,6 @@ fun MainScreen(
|
||||||
viewModel.reset("Untitled.md")
|
viewModel.reset("Untitled.md")
|
||||||
},
|
},
|
||||||
enableWideLayout = enableWideLayout,
|
enableWideLayout = enableWideLayout,
|
||||||
enableReadability = enableReadability,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,10 +130,8 @@ fun MainScreen(
|
||||||
private fun MainScreen(
|
private fun MainScreen(
|
||||||
fileName: String = "Untitled.md",
|
fileName: String = "Untitled.md",
|
||||||
dirty: Boolean = false,
|
dirty: Boolean = false,
|
||||||
reloadToggle: Int = 0,
|
markdown: TextFieldValue = TextFieldValue(""),
|
||||||
initialMarkdown: String = "",
|
setMarkdown: (TextFieldValue) -> Unit = {},
|
||||||
markdown: String = "",
|
|
||||||
setMarkdown: (String) -> Unit = {},
|
|
||||||
lockSwiping: Boolean,
|
lockSwiping: Boolean,
|
||||||
toggleLockSwiping: (Boolean) -> Unit,
|
toggleLockSwiping: (Boolean) -> Unit,
|
||||||
message: String? = null,
|
message: String? = null,
|
||||||
|
@ -153,7 +145,6 @@ private fun MainScreen(
|
||||||
saveCallback: (() -> Unit)? = null,
|
saveCallback: (() -> Unit)? = null,
|
||||||
reset: () -> Unit = {},
|
reset: () -> Unit = {},
|
||||||
enableWideLayout: Boolean = false,
|
enableWideLayout: Boolean = false,
|
||||||
enableReadability: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
val openFileLauncher =
|
val openFileLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
|
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||||
|
@ -214,7 +205,7 @@ private fun MainScreen(
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||||
shareIntent.putExtra(Intent.EXTRA_TEXT, markdown)
|
shareIntent.putExtra(Intent.EXTRA_TEXT, markdown.text)
|
||||||
shareIntent.type = "text/plain"
|
shareIntent.type = "text/plain"
|
||||||
startActivity(
|
startActivity(
|
||||||
context, Intent.createChooser(
|
context, Intent.createChooser(
|
||||||
|
@ -287,10 +278,8 @@ private fun MainScreen(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
reload = reloadToggle,
|
|
||||||
markdown = markdown,
|
markdown = markdown,
|
||||||
setMarkdown = setMarkdown,
|
setMarkdown = setMarkdown,
|
||||||
enableReadability = enableReadability,
|
|
||||||
)
|
)
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -302,7 +291,7 @@ private fun MainScreen(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
markdown = markdown
|
markdown = markdown.text
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -312,12 +301,9 @@ private fun MainScreen(
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
TabbedMarkdownEditor(
|
TabbedMarkdownEditor(
|
||||||
initialMarkdown = initialMarkdown,
|
|
||||||
markdown = markdown,
|
markdown = markdown,
|
||||||
setMarkdown = setMarkdown,
|
setMarkdown = setMarkdown,
|
||||||
lockSwiping = lockSwiping,
|
lockSwiping = lockSwiping,
|
||||||
enableReadability = enableReadability,
|
|
||||||
reloadToggle = reloadToggle,
|
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -327,14 +313,11 @@ private fun MainScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
private fun TabbedMarkdownEditor(
|
private fun TabbedMarkdownEditor(
|
||||||
initialMarkdown: String,
|
markdown: TextFieldValue,
|
||||||
markdown: String,
|
setMarkdown: (TextFieldValue) -> Unit,
|
||||||
setMarkdown: (String) -> Unit,
|
|
||||||
lockSwiping: Boolean,
|
lockSwiping: Boolean,
|
||||||
enableReadability: Boolean,
|
|
||||||
reloadToggle: Int,
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior
|
scrollBehavior: TopAppBarScrollBehavior
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
@ -349,7 +332,7 @@ private fun TabbedMarkdownEditor(
|
||||||
}
|
}
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
modifier = Modifier.fillMaxSize(1f), state = pagerState,
|
modifier = Modifier.fillMaxSize(1f), state = pagerState,
|
||||||
beyondBoundsPageCount = 1,
|
beyondViewportPageCount = 1,
|
||||||
userScrollEnabled = !lockSwiping
|
userScrollEnabled = !lockSwiping
|
||||||
) { page ->
|
) { page ->
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
@ -364,17 +347,15 @@ private fun TabbedMarkdownEditor(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
markdown = initialMarkdown,
|
markdown = markdown,
|
||||||
setMarkdown = setMarkdown,
|
setMarkdown = setMarkdown,
|
||||||
enableReadability = enableReadability,
|
|
||||||
reload = reloadToggle,
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MarkdownText(
|
MarkdownText(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
markdown
|
markdown.text
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package com.wbrawner.simplemarkdown.ui
|
package com.wbrawner.simplemarkdown.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.Color.TRANSPARENT
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import android.widget.FrameLayout
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
@ -15,7 +17,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import com.wbrawner.simplemarkdown.BuildConfig
|
import com.wbrawner.simplemarkdown.BuildConfig
|
||||||
|
@ -91,22 +92,34 @@ fun HtmlText(html: String, modifier: Modifier = Modifier) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
WebView(context).apply {
|
FrameLayout(context).apply {
|
||||||
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
)
|
)
|
||||||
setBackgroundColor(Color.Transparent.toArgb())
|
addView(
|
||||||
isNestedScrollingEnabled = false
|
WebView(context).apply {
|
||||||
settings.javaScriptEnabled = true
|
tag = WEBVIEW_TAG
|
||||||
loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null)
|
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
||||||
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
setBackgroundColor(TRANSPARENT)
|
||||||
|
isNestedScrollingEnabled = false
|
||||||
|
settings.javaScriptEnabled = true
|
||||||
|
loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update = { webView ->
|
update = { frameLayout ->
|
||||||
webView.loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null)
|
frameLayout.findViewWithTag<WebView>(WEBVIEW_TAG)
|
||||||
|
.loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val WEBVIEW_TAG = "com.wbrawner.simplemarkdown.MarkdownText#WebView"
|
||||||
|
|
||||||
private fun String.wrapTag(tag: String) = "<$tag>$this</$tag>"
|
private fun String.wrapTag(tag: String) = "<$tag>$this</$tag>"
|
|
@ -1,33 +1,23 @@
|
||||||
package com.wbrawner.simplemarkdown.ui
|
package com.wbrawner.simplemarkdown.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.TextRange
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
@ -35,28 +25,14 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.wbrawner.simplemarkdown.R
|
import com.wbrawner.simplemarkdown.R
|
||||||
import com.wbrawner.simplemarkdown.model.Readability
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MarkdownTextField(
|
fun MarkdownTextField(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
markdown: String,
|
markdown: TextFieldValue,
|
||||||
setMarkdown: (String) -> Unit,
|
setMarkdown: (TextFieldValue) -> Unit,
|
||||||
reload: Int = 0,
|
|
||||||
enableReadability: Boolean = false,
|
|
||||||
) {
|
) {
|
||||||
val (selection, setSelection) = remember { mutableStateOf(TextRange.Zero) }
|
|
||||||
val (composition, setComposition) = remember { mutableStateOf<TextRange?>(null) }
|
|
||||||
val (textFieldValue, setTextFieldValue) = remember(reload) {
|
|
||||||
mutableStateOf(TextFieldValue(markdown.annotate(enableReadability), selection, composition))
|
|
||||||
}
|
|
||||||
val setTextFieldAndViewModelValues: (TextFieldValue) -> Unit = {
|
|
||||||
setSelection(it.selection)
|
|
||||||
setComposition(it.composition)
|
|
||||||
setTextFieldValue(it.copy(annotatedString = it.text.annotate(enableReadability)))
|
|
||||||
setMarkdown(it.text)
|
|
||||||
}
|
|
||||||
val colors = TextFieldDefaults.colors(
|
val colors = TextFieldDefaults.colors(
|
||||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
@ -70,57 +46,37 @@ fun MarkdownTextField(
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
Column(
|
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
||||||
modifier = modifier
|
BasicTextField(
|
||||||
.fillMaxSize()
|
value = markdown,
|
||||||
.imePadding()
|
modifier = modifier.imePadding(),
|
||||||
.verticalScroll(rememberScrollState())
|
onValueChange = setMarkdown,
|
||||||
) {
|
enabled = true,
|
||||||
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
readOnly = false,
|
||||||
BasicTextField(
|
textStyle = textStyle,
|
||||||
value = textFieldValue,
|
cursorBrush = SolidColor(colors.cursorColor),
|
||||||
modifier = Modifier.fillMaxSize(),
|
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
|
||||||
onValueChange = setTextFieldAndViewModelValues,
|
keyboardActions = KeyboardActions.Default,
|
||||||
enabled = true,
|
interactionSource = interactionSource,
|
||||||
readOnly = false,
|
singleLine = false,
|
||||||
textStyle = textStyle,
|
maxLines = Int.MAX_VALUE,
|
||||||
cursorBrush = SolidColor(colors.cursorColor),
|
minLines = 1,
|
||||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
|
decorationBox = @Composable { innerTextField ->
|
||||||
keyboardActions = KeyboardActions.Default,
|
// places leading icon, text field with label and placeholder, trailing icon
|
||||||
interactionSource = interactionSource,
|
TextFieldDefaults.DecorationBox(
|
||||||
singleLine = false,
|
value = markdown.text,
|
||||||
maxLines = Int.MAX_VALUE,
|
visualTransformation = VisualTransformation.None,
|
||||||
minLines = 1,
|
innerTextField = innerTextField,
|
||||||
decorationBox = @Composable { innerTextField ->
|
placeholder = {
|
||||||
// places leading icon, text field with label and placeholder, trailing icon
|
Text(stringResource(R.string.markdown_here))
|
||||||
TextFieldDefaults.DecorationBox(
|
},
|
||||||
value = textFieldValue.text,
|
singleLine = false,
|
||||||
visualTransformation = VisualTransformation.None,
|
enabled = true,
|
||||||
innerTextField = innerTextField,
|
interactionSource = interactionSource,
|
||||||
placeholder = {
|
colors = colors,
|
||||||
Text(stringResource(R.string.markdown_here))
|
contentPadding = PaddingValues(8.dp)
|
||||||
},
|
)
|
||||||
singleLine = false,
|
},
|
||||||
enabled = true,
|
)
|
||||||
interactionSource = interactionSource,
|
|
||||||
colors = colors,
|
|
||||||
contentPadding = PaddingValues(8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.annotate(enableReadability: Boolean): AnnotatedString {
|
|
||||||
if (!enableReadability) return AnnotatedString(this)
|
|
||||||
val readability = Readability(this)
|
|
||||||
val annotated = AnnotatedString.Builder(this)
|
|
||||||
for (sentence in readability.sentences()) {
|
|
||||||
var color = Color.Transparent
|
|
||||||
if (sentence.syllableCount() > 25) color = Color(229, 232, 42, 100)
|
|
||||||
if (sentence.syllableCount() > 35) color = Color(193, 66, 66, 100)
|
|
||||||
annotated.addStyle(SpanStyle(background = color), sentence.start(), sentence.end())
|
|
||||||
}
|
|
||||||
return annotated.toAnnotatedString()
|
|
||||||
}
|
|
|
@ -1,11 +1,11 @@
|
||||||
package com.wbrawner.simplemarkdown.utility
|
package com.wbrawner.simplemarkdown.utility
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.view.View
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.Reader
|
import java.io.Reader
|
||||||
|
@ -22,11 +22,11 @@ suspend fun Uri.getName(context: Context): String {
|
||||||
if ("content" == scheme) {
|
if ("content" == scheme) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
context.contentResolver.query(
|
context.contentResolver.query(
|
||||||
this@getName,
|
this@getName,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
)?.use {
|
)?.use {
|
||||||
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
it.moveToFirst()
|
it.moveToFirst()
|
||||||
|
@ -41,3 +41,11 @@ suspend fun Uri.getName(context: Context): String {
|
||||||
}
|
}
|
||||||
return fileName ?: "Untitled.md"
|
return fileName ?: "Untitled.md"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("RecursivePropertyAccessor")
|
||||||
|
val Context.activity: Activity?
|
||||||
|
get() = when (this) {
|
||||||
|
is Activity -> this
|
||||||
|
is ContextWrapper -> baseContext.activity
|
||||||
|
else -> null
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package com.wbrawner.simplemarkdown.utility
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -34,8 +33,10 @@ interface FileHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AndroidFileHelper(private val context: Context) : FileHelper {
|
class AndroidFileHelper(private val context: Context) : FileHelper {
|
||||||
override val defaultDirectory: File = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
|
override val defaultDirectory: File by lazy {
|
||||||
?: context.filesDir
|
context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
|
||||||
|
?: context.filesDir
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun open(source: URI): Pair<String, String>? = withContext(Dispatchers.IO) {
|
override suspend fun open(source: URI): Pair<String, String>? = withContext(Dispatchers.IO) {
|
||||||
val uri = source.toString().toUri()
|
val uri = source.toString().toUri()
|
||||||
|
|
|
@ -19,10 +19,17 @@ interface PreferenceHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AndroidPreferenceHelper(context: Context, private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)): PreferenceHelper {
|
class AndroidPreferenceHelper(context: Context, private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)): PreferenceHelper {
|
||||||
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
private val sharedPreferences by lazy {
|
||||||
private val states = Preference.entries.associateWith { MutableStateFlow(get(it)) }
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
}
|
||||||
|
private val states by lazy {
|
||||||
|
val allPrefs: Map<String, Any?> = sharedPreferences.all
|
||||||
|
Preference.entries.associateWith { preference ->
|
||||||
|
MutableStateFlow(allPrefs[preference.key] ?: preference.default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun get(preference: Preference): Any? = sharedPreferences.all[preference.key]?: preference.default
|
override fun get(preference: Preference): Any? = states[preference]?.value
|
||||||
|
|
||||||
override fun set(preference: Preference, value: Any?) {
|
override fun set(preference: Preference, value: Any?) {
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
|
@ -40,6 +47,7 @@ class AndroidPreferenceHelper(context: Context, private val coroutineScope: Coro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T> observe(preference: Preference): StateFlow<T> = states[preference]!!.asStateFlow() as StateFlow<T>
|
override fun <T> observe(preference: Preference): StateFlow<T> = states[preference]!!.asStateFlow() as StateFlow<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
app/src/play/play/release-notes/en-US/default.txt
Normal file
2
app/src/play/play/release-notes/en-US/default.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
- Fix opening files from external apps
|
||||||
|
- Update dependencies
|
|
@ -1,2 +0,0 @@
|
||||||
- Add support for themed icon
|
|
||||||
- Fix crashes
|
|
|
@ -23,6 +23,7 @@ class FakePreferenceHelper: PreferenceHelper {
|
||||||
preferences[preference] = value
|
preferences[preference] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T> observe(preference: Preference): StateFlow<T> =
|
override fun <T> observe(preference: Preference): StateFlow<T> =
|
||||||
preferenceFlow(preference) as StateFlow<T>
|
preferenceFlow(preference) as StateFlow<T>
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package com.wbrawner.simplemarkdown
|
package com.wbrawner.simplemarkdown
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
import com.wbrawner.simplemarkdown.utility.Preference
|
import com.wbrawner.simplemarkdown.utility.Preference
|
||||||
|
@ -23,6 +24,7 @@ import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class MarkdownViewModelTest {
|
class MarkdownViewModelTest {
|
||||||
private lateinit var fileHelper: FakeFileHelper
|
private lateinit var fileHelper: FakeFileHelper
|
||||||
private lateinit var preferenceHelper: FakePreferenceHelper
|
private lateinit var preferenceHelper: FakePreferenceHelper
|
||||||
|
@ -51,9 +53,9 @@ class MarkdownViewModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMarkdownUpdate() = runTest {
|
fun testMarkdownUpdate() = runTest {
|
||||||
assertEquals("", viewModel.state.value.markdown)
|
assertEquals("".asTextFieldValue(), viewModel.state.value.markdown)
|
||||||
viewModel.updateMarkdown("Updated content")
|
viewModel.updateMarkdown("Updated content")
|
||||||
assertEquals("Updated content", viewModel.state.value.markdown)
|
assertEquals("Updated content".asTextFieldValue(), viewModel.state.value.markdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -67,11 +69,11 @@ class MarkdownViewModelTest {
|
||||||
val uri = URI.create("file:///home/user/Untitled.md")
|
val uri = URI.create("file:///home/user/Untitled.md")
|
||||||
preferenceHelper[Preference.AUTOSAVE_URI] = uri.toString()
|
preferenceHelper[Preference.AUTOSAVE_URI] = uri.toString()
|
||||||
viewModel = viewModelFactory.create(MarkdownViewModel::class.java, CreationExtras.Empty)
|
viewModel = viewModelFactory.create(MarkdownViewModel::class.java, CreationExtras.Empty)
|
||||||
viewModelScope.advanceUntilIdle()
|
viewModel.load(null)
|
||||||
assertEquals(uri, fileHelper.openedUris.firstOrNull())
|
assertEquals(uri, fileHelper.openedUris.firstOrNull())
|
||||||
val (fileName, contents) = fileHelper.file
|
val (fileName, contents) = fileHelper.file
|
||||||
assertEquals(fileName, viewModel.state.value.fileName)
|
assertEquals(fileName, viewModel.state.value.fileName)
|
||||||
assertEquals(contents, viewModel.state.value.markdown)
|
assertEquals(contents.asTextFieldValue(), viewModel.state.value.markdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -81,7 +83,7 @@ class MarkdownViewModelTest {
|
||||||
assertEquals(uri, fileHelper.openedUris.firstOrNull())
|
assertEquals(uri, fileHelper.openedUris.firstOrNull())
|
||||||
val (fileName, contents) = fileHelper.file
|
val (fileName, contents) = fileHelper.file
|
||||||
assertEquals(fileName, viewModel.state.value.fileName)
|
assertEquals(fileName, viewModel.state.value.fileName)
|
||||||
assertEquals(contents, viewModel.state.value.markdown)
|
assertEquals(contents.asTextFieldValue(), viewModel.state.value.markdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -125,7 +127,7 @@ class MarkdownViewModelTest {
|
||||||
val uri = URI.create("file:///home/user/Saved.md")
|
val uri = URI.create("file:///home/user/Saved.md")
|
||||||
val testMarkdown = "# Test"
|
val testMarkdown = "# Test"
|
||||||
viewModel.updateMarkdown(testMarkdown)
|
viewModel.updateMarkdown(testMarkdown)
|
||||||
assertEquals(testMarkdown, viewModel.state.value.markdown)
|
assertEquals(testMarkdown.asTextFieldValue(), viewModel.state.value.markdown)
|
||||||
assertTrue(viewModel.save(uri))
|
assertTrue(viewModel.save(uri))
|
||||||
assertEquals("Saved.md", viewModel.state.value.fileName)
|
assertEquals("Saved.md", viewModel.state.value.fileName)
|
||||||
assertEquals(uri, fileHelper.savedData.last().uri)
|
assertEquals(uri, fileHelper.savedData.last().uri)
|
||||||
|
@ -138,7 +140,7 @@ class MarkdownViewModelTest {
|
||||||
val uri = URI.create("file:///home/user/Untitled.md")
|
val uri = URI.create("file:///home/user/Untitled.md")
|
||||||
val testMarkdown = "# Test"
|
val testMarkdown = "# Test"
|
||||||
viewModel.updateMarkdown(testMarkdown)
|
viewModel.updateMarkdown(testMarkdown)
|
||||||
assertEquals(testMarkdown, viewModel.state.value.markdown)
|
assertEquals(testMarkdown.asTextFieldValue(), viewModel.state.value.markdown)
|
||||||
fileHelper.errorOnSave = true
|
fileHelper.errorOnSave = true
|
||||||
assertNull(viewModel.state.value.alert)
|
assertNull(viewModel.state.value.alert)
|
||||||
assertFalse(viewModel.save(uri))
|
assertFalse(viewModel.save(uri))
|
||||||
|
@ -158,7 +160,7 @@ class MarkdownViewModelTest {
|
||||||
assertNull(viewModel.state.value.alert)
|
assertNull(viewModel.state.value.alert)
|
||||||
with(viewModel.state.value) {
|
with(viewModel.state.value) {
|
||||||
assertEquals("New.md", fileName)
|
assertEquals("New.md", fileName)
|
||||||
assertEquals("", markdown)
|
assertEquals("".asTextFieldValue(), markdown)
|
||||||
assertNull(path)
|
assertNull(path)
|
||||||
assertNull(saveCallback)
|
assertNull(saveCallback)
|
||||||
assertNull(alert)
|
assertNull(alert)
|
||||||
|
@ -180,7 +182,7 @@ class MarkdownViewModelTest {
|
||||||
requireNotNull(onClick)
|
requireNotNull(onClick)
|
||||||
onClick.invoke()
|
onClick.invoke()
|
||||||
}
|
}
|
||||||
assertEquals(viewModel.state.value, EditorState(reloadToggle = 0.inv()))
|
assertEquals(viewModel.state.value, EditorState())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -199,7 +201,7 @@ class MarkdownViewModelTest {
|
||||||
viewModel.save(uri)
|
viewModel.save(uri)
|
||||||
assertNotNull(viewModel.state.value.saveCallback)
|
assertNotNull(viewModel.state.value.saveCallback)
|
||||||
requireNotNull(viewModel.state.value.saveCallback).invoke()
|
requireNotNull(viewModel.state.value.saveCallback).invoke()
|
||||||
assertEquals(viewModel.state.value, EditorState(reloadToggle = 0.inv()))
|
assertEquals(viewModel.state.value, EditorState())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -216,7 +218,7 @@ class MarkdownViewModelTest {
|
||||||
assertNull(viewModel.state.value.alert)
|
assertNull(viewModel.state.value.alert)
|
||||||
with(viewModel.state.value) {
|
with(viewModel.state.value) {
|
||||||
assertEquals("Unsaved.md", fileName)
|
assertEquals("Unsaved.md", fileName)
|
||||||
assertEquals("", markdown)
|
assertEquals("".asTextFieldValue(), markdown)
|
||||||
assertNull(path)
|
assertNull(path)
|
||||||
assertNull(saveCallback)
|
assertNull(saveCallback)
|
||||||
assertNull(alert)
|
assertNull(alert)
|
||||||
|
@ -302,4 +304,6 @@ class MarkdownViewModelTest {
|
||||||
assertFalse(preferenceHelper.preferences[Preference.LOCK_SWIPING] as Boolean)
|
assertFalse(preferenceHelper.preferences[Preference.LOCK_SWIPING] as Boolean)
|
||||||
assertFalse(viewModel.state.value.lockSwiping)
|
assertFalse(viewModel.state.value.lockSwiping)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.asTextFieldValue() = TextFieldValue(this)
|
||||||
}
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.wbrawner.releasehelper
|
||||||
|
|
||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.file.RegularFileProperty
|
||||||
|
import org.gradle.api.internal.provider.Providers
|
||||||
|
import org.gradle.api.model.ObjectFactory
|
||||||
|
import org.gradle.api.provider.Property
|
||||||
|
import org.gradle.api.provider.ProviderFactory
|
||||||
|
import org.gradle.api.tasks.Input
|
||||||
|
import org.gradle.api.tasks.OutputFile
|
||||||
|
import org.gradle.api.tasks.TaskAction
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val CHANGELOG_PATH = "src/play/play/release-notes/en-US/production.txt"
|
||||||
|
|
||||||
|
abstract class ChangelogTask @Inject constructor(
|
||||||
|
objectFactory: ObjectFactory,
|
||||||
|
providers: ProviderFactory,
|
||||||
|
) : DefaultTask() {
|
||||||
|
@get:OutputFile
|
||||||
|
val changelogFile: RegularFileProperty = objectFactory.fileProperty()
|
||||||
|
|
||||||
|
@get:Input
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
val latestTag: String = providers.exec {
|
||||||
|
commandLine("git" , "describe", "--tags", "--abbrev=0")
|
||||||
|
}.standardOutput.asText.get()
|
||||||
|
|
||||||
|
init {
|
||||||
|
changelogFile.set(project.layout.projectDirectory.file(CHANGELOG_PATH))
|
||||||
|
}
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun execute() {
|
||||||
|
val changelog = "git log --format=\"%B\" ${latestTag.trim()}..".execute()
|
||||||
|
logger.info("Latest tag: $latestTag")
|
||||||
|
logger.info("Changelog: ${changelog.joinToString("\n")}")
|
||||||
|
changelogFile.get().asFile.writer().use { writer ->
|
||||||
|
writer.write(
|
||||||
|
changelog.joinToString("\n") { it.trim('"') }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.execute(): List<String> = ProcessBuilder()
|
||||||
|
.command(this.split(" "))
|
||||||
|
.redirectOutput(ProcessBuilder.Redirect.PIPE)
|
||||||
|
.start()
|
||||||
|
.inputReader()
|
||||||
|
.readLines()
|
||||||
|
}
|
|
@ -2,46 +2,15 @@ package com.wbrawner.releasehelper
|
||||||
|
|
||||||
import org.gradle.api.Plugin
|
import org.gradle.api.Plugin
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
import org.gradle.api.tasks.Exec
|
|
||||||
import org.gradle.kotlin.dsl.extra
|
import org.gradle.kotlin.dsl.extra
|
||||||
import org.gradle.kotlin.dsl.provideDelegate
|
import org.gradle.kotlin.dsl.provideDelegate
|
||||||
import java.io.ByteArrayOutputStream
|
import org.gradle.kotlin.dsl.register
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
class ReleaseHelperPlugin : Plugin<Project> {
|
class ReleaseHelperPlugin : Plugin<Project> {
|
||||||
override fun apply(target: Project) {
|
override fun apply(target: Project) {
|
||||||
target.tasks.register("getLatestTag", Exec::class.java) {
|
target.tasks.register<ChangelogTask>("changelog")
|
||||||
val latestTag = ByteArrayOutputStream()
|
|
||||||
standardOutput = latestTag
|
|
||||||
commandLine("git describe --tags --abbrev=0".split(" "))
|
|
||||||
doLast {
|
|
||||||
target.project.extra["latestTag"] = latestTag.toString().trim()
|
|
||||||
logger.info("Latest tag: ${target.project.extra["latestTag"]}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target.tasks.register("changelog") {
|
|
||||||
val changelogFile = File(target.projectDir, "src/play/play/release-notes/en-US/default.txt")
|
|
||||||
inputs.property("tag", target.provider {
|
|
||||||
target.project.extra["latestTag"]
|
|
||||||
})
|
|
||||||
outputs.file(changelogFile)
|
|
||||||
dependsOn("getLatestTag")
|
|
||||||
doLast {
|
|
||||||
val latestTag: String by target.project.extra
|
|
||||||
val changelog = ByteArrayOutputStream()
|
|
||||||
target.exec {
|
|
||||||
standardOutput = changelog
|
|
||||||
commandLine = "git log --format=\"%B\" ${latestTag.trim()}..".split(" ")
|
|
||||||
}
|
|
||||||
changelogFile.writeText(
|
|
||||||
changelog.toString()
|
|
||||||
.split("\n")
|
|
||||||
.mapNotNull { it.trim('"').ifBlank { null } }
|
|
||||||
.joinToString("\n") { "- $it" }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target.tasks.register("majorRelease") {
|
target.tasks.register("majorRelease") {
|
||||||
dependsOn("changelog", "getLatestTag")
|
dependsOn("changelog", "getLatestTag")
|
||||||
|
|
|
@ -41,19 +41,24 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "11"
|
||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
disable += listOf("AndroidGradlePluginVersion", "GradleDependency")
|
disable += listOf(
|
||||||
|
"AndroidGradlePluginVersion",
|
||||||
|
"GradleDependency",
|
||||||
|
"ObsoleteLintCustomCheck"
|
||||||
|
)
|
||||||
warningsAsErrors = true
|
warningsAsErrors = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.acra.core)
|
implementation(libs.acra.core)
|
||||||
implementation(libs.acra.http)
|
implementation(libs.acra.http)
|
||||||
runtimeOnly(libs.acra.limiter)
|
runtimeOnly(libs.acra.limiter)
|
||||||
|
|
|
@ -2,7 +2,8 @@ package com.wbrawner.simplemarkdown.core
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import org.acra.ACRA
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.acra.config.httpSender
|
import org.acra.config.httpSender
|
||||||
import org.acra.data.StringFormat
|
import org.acra.data.StringFormat
|
||||||
import org.acra.ktx.initAcra
|
import org.acra.ktx.initAcra
|
||||||
|
@ -13,22 +14,32 @@ import timber.log.Timber
|
||||||
class ErrorReporterTree private constructor(): Timber.Tree() {
|
class ErrorReporterTree private constructor(): Timber.Tree() {
|
||||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||||
if (priority != Log.ERROR) return
|
if (priority != Log.ERROR) return
|
||||||
t?.sendSilentlyWithAcra()
|
if (t !is LocalOnlyException) {
|
||||||
|
t?.sendSilentlyWithAcra()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun create(application: Application): ErrorReporterTree {
|
suspend fun create(application: Application): ErrorReporterTree {
|
||||||
application.createErrorReporterTree()
|
application.createErrorReporterTree()
|
||||||
return ErrorReporterTree()
|
return ErrorReporterTree()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Application.createErrorReporterTree() {
|
/**
|
||||||
|
* An exception wrapper that prevents exceptions from being sent to an error reporter. Useful for
|
||||||
|
* logging things like IOExceptions that are useful to see locally but not so helpful if reported
|
||||||
|
*/
|
||||||
|
class LocalOnlyException(override val message: String?, override val cause: Throwable): Exception(message, cause) {
|
||||||
|
constructor(cause: Throwable): this(null, cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun Application.createErrorReporterTree() = withContext(Dispatchers.IO) {
|
||||||
initAcra {
|
initAcra {
|
||||||
reportFormat = StringFormat.JSON
|
reportFormat = StringFormat.JSON
|
||||||
httpSender {
|
httpSender {
|
||||||
uri = "${BuildConfig.ACRA_URL}/report" /*best guess, you may need to adjust this*/
|
uri = "${BuildConfig.ACRA_URL}/report"
|
||||||
basicAuthLogin = BuildConfig.ACRA_USER
|
basicAuthLogin = BuildConfig.ACRA_USER
|
||||||
basicAuthPassword = BuildConfig.ACRA_PASS
|
basicAuthPassword = BuildConfig.ACRA_PASS
|
||||||
httpMethod = HttpSender.Method.POST
|
httpMethod = HttpSender.Method.POST
|
||||||
|
|
|
@ -10,7 +10,7 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = libs.versions.minSdk.get().toInt()
|
minSdk = libs.versions.minSdk.get().toInt()
|
||||||
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,14 +27,18 @@ android {
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "11"
|
||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
disable += listOf("AndroidGradlePluginVersion", "GradleDependency")
|
disable += listOf(
|
||||||
|
"AndroidGradlePluginVersion",
|
||||||
|
"GradleDependency",
|
||||||
|
"ObsoleteLintCustomCheck"
|
||||||
|
)
|
||||||
warningsAsErrors = true
|
warningsAsErrors = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-dontwarn javax.annotation.processing.AbstractProcessor
|
||||||
|
-dontwarn javax.annotation.processing.SupportedOptions
|
|
@ -1,43 +1,40 @@
|
||||||
[versions]
|
[versions]
|
||||||
acra = "5.11.3"
|
acra = "5.12.0"
|
||||||
activityKtx = "1.9.1"
|
activityKtx = "1.9.3"
|
||||||
animationCore = "1.6.8"
|
animationCore = "1.7.5"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
billing = "7.0.0"
|
billing = "7.1.1"
|
||||||
browser = "1.8.0"
|
browser = "1.8.0"
|
||||||
commonMarkVersion = "0.22.0"
|
commonMarkVersion = "0.24.0"
|
||||||
composeBom = "2024.06.00"
|
composeBom = "2024.11.00"
|
||||||
core = "1.6.1"
|
core = "1.6.1"
|
||||||
coreKtx = "1.13.1"
|
coreKtx = "1.15.0"
|
||||||
coreSplashscreen = "1.0.1"
|
coreSplashscreen = "1.0.1"
|
||||||
coroutines = "1.8.1"
|
coroutines = "1.9.0"
|
||||||
dependencyAnalysis = "1.33.0"
|
dependencyAnalysis = "2.5.0"
|
||||||
espressoVersion = "3.6.1"
|
espressoVersion = "3.6.1"
|
||||||
fladle = "0.17.5"
|
fladle = "0.17.5"
|
||||||
googleServices = "4.4.2"
|
googleServices = "4.4.2"
|
||||||
firebaseCrashlyticsGradle = "3.0.2"
|
firebaseCrashlyticsGradle = "3.0.2"
|
||||||
androidGradlePlugin = "8.5.2"
|
androidGradlePlugin = "8.7.2"
|
||||||
hamcrestCore = "1.3"
|
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlin = "2.0.10"
|
kotlin = "2.0.21"
|
||||||
lifecycleViewmodelKtx = "2.8.4"
|
lifecycleViewmodelKtx = "2.8.7"
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
material3WindowSizeClassAndroid = "1.2.1"
|
material3WindowSizeClassAndroid = "1.3.1"
|
||||||
materialIconsCore = "1.6.8"
|
materialIconsCore = "1.7.5"
|
||||||
maxSdk = "35"
|
maxSdk = "35"
|
||||||
minSdk = "23"
|
minSdk = "23"
|
||||||
monitor = "1.7.1"
|
monitor = "1.7.2"
|
||||||
navigationCommon = "2.7.7"
|
navigation = "2.8.4"
|
||||||
navigationRuntimeKtx = "2.7.7"
|
orchestrator = "1.5.1"
|
||||||
navigationVersion = "2.7.7"
|
play = "2.0.2"
|
||||||
orchestrator = "1.5.0"
|
|
||||||
play = "2.0.1"
|
|
||||||
preferenceKtx = "1.2.1"
|
preferenceKtx = "1.2.1"
|
||||||
robolectric = "4.13"
|
robolectric = "4.14.1"
|
||||||
runner = "1.6.1"
|
runner = "1.6.2"
|
||||||
syllableCounter = "4.1.0"
|
syllableCounter = "4.1.0"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
tripletPlay = "3.10.1"
|
tripletPlay = "3.12.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
acra-advanced-scheduler = { module = "ch.acra:acra-advanced-scheduler", version.ref = "acra" }
|
acra-advanced-scheduler = { module = "ch.acra:acra-advanced-scheduler", version.ref = "acra" }
|
||||||
|
@ -65,9 +62,9 @@ androidx-material-icons-extended = { module = "androidx.compose.material:materia
|
||||||
androidx-material3 = { module = "androidx.compose.material3:material3" }
|
androidx-material3 = { module = "androidx.compose.material3:material3" }
|
||||||
androidx-material3-windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class-android", version.ref = "material3WindowSizeClassAndroid" }
|
androidx-material3-windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class-android", version.ref = "material3WindowSizeClassAndroid" }
|
||||||
androidx-monitor = { module = "androidx.test:monitor", version.ref = "monitor" }
|
androidx-monitor = { module = "androidx.test:monitor", version.ref = "monitor" }
|
||||||
androidx-navigation-common = { module = "androidx.navigation:navigation-common", version.ref = "navigationCommon" }
|
androidx-navigation-common = { module = "androidx.navigation:navigation-common", version.ref = "navigation" }
|
||||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationVersion" }
|
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
|
||||||
androidx-navigation-runtime-ktx = { module = "androidx.navigation:navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" }
|
androidx-navigation-runtime-ktx = { module = "androidx.navigation:navigation-runtime-ktx", version.ref = "navigation" }
|
||||||
androidx-orchestrator = { module = "androidx.test:orchestrator", version.ref = "orchestrator" }
|
androidx-orchestrator = { module = "androidx.test:orchestrator", version.ref = "orchestrator" }
|
||||||
androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
|
androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
|
||||||
androidx-runner = { module = "androidx.test:runner", version.ref = "runner" }
|
androidx-runner = { module = "androidx.test:runner", version.ref = "runner" }
|
||||||
|
@ -92,7 +89,6 @@ commonmark-ext-yaml-front-matter = { module = "org.commonmark:commonmark-ext-yam
|
||||||
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
|
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
|
||||||
foundation = { module = "androidx.compose.foundation:foundation" }
|
foundation = { module = "androidx.compose.foundation:foundation" }
|
||||||
foundation-layout = { module = "androidx.compose.foundation:foundation-layout" }
|
foundation-layout = { module = "androidx.compose.foundation:foundation-layout" }
|
||||||
hamcrest-core = { module = "org.hamcrest:hamcrest-core", version.ref = "hamcrestCore" }
|
|
||||||
junit = { module = "junit:junit", version.ref = "junit" }
|
junit = { module = "junit:junit", version.ref = "junit" }
|
||||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
storePassword=
|
storePassword=
|
||||||
keyPassword=
|
keyPassword=
|
||||||
keyAlias=
|
keyAlias=
|
||||||
storeFile=
|
storeFile=
|
||||||
|
publishCredentialsFile=
|
|
@ -24,17 +24,21 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "11"
|
||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
disable += listOf("AndroidGradlePluginVersion", "GradleDependency")
|
disable += listOf(
|
||||||
|
"AndroidGradlePluginVersion",
|
||||||
|
"GradleDependency",
|
||||||
|
"ObsoleteLintCustomCheck"
|
||||||
|
)
|
||||||
warningsAsErrors = true
|
warningsAsErrors = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue