Compare commits

...

6 commits

Author SHA1 Message Date
79a5a3809a
Fix failing unit tests
All checks were successful
Build & Test / Validate (pull_request) Successful in 14s
Build & Test / Run Unit Tests (pull_request) Successful in 9m4s
Build & Test / Run UI Tests (pull_request) Successful in 17m58s
Build & Test / Validate (push) Successful in 15s
Build & Test / Run Unit Tests (push) Successful in 9m45s
Build & Test / Run UI Tests (push) Successful in 17m39s
2024-08-02 16:46:03 -06:00
840ebc4fd1
Fix failing UI test
Some checks failed
Build & Test / Validate (pull_request) Successful in 15s
Build & Test / Run Unit Tests (pull_request) Failing after 8m52s
Build & Test / Run UI Tests (pull_request) Successful in 16m38s
2024-08-02 16:11:51 -06:00
b0e8ebbf71
Fix failing UI tests
Some checks failed
Build & Test / Validate (pull_request) Successful in 25s
Build & Test / Run Unit Tests (pull_request) Successful in 9m19s
Build & Test / Run UI Tests (pull_request) Failing after 17m13s
2024-08-01 18:47:12 -06:00
262b63cfa0
Setup Android SDK before attempting to build in UI test job
Some checks failed
Build & Test / Validate (pull_request) Successful in 18s
Build & Test / Run Unit Tests (pull_request) Successful in 9m29s
Build & Test / Run UI Tests (pull_request) Failing after 18m22s
2024-07-31 20:58:33 -06:00
c6e14d7d0b
Setup Android SDK for UI test workflow job
Some checks failed
Build & Test / Validate (pull_request) Successful in 15s
Build & Test / Run UI Tests (pull_request) Failing after 3m57s
Build & Test / Run Unit Tests (pull_request) Has been cancelled
2024-07-31 20:52:38 -06:00
86cd33ff5f
Address lint issues
Some checks failed
Build & Test / Validate (pull_request) Successful in 19s
Build & Test / Run Unit Tests (pull_request) Successful in 10m34s
Build & Test / Run UI Tests (pull_request) Failing after 4m1s
2024-07-31 20:17:14 -06:00
31 changed files with 198 additions and 157 deletions

View file

@ -55,6 +55,8 @@ jobs:
with:
distribution: 'zulu'
java-version: '17'
- name: Setup Android SDK
uses: https://git.wbrawner.com/android-actions/setup-android@v3
- name: Build with Gradle
uses: https://git.wbrawner.com/gradle/gradle-build-action@v2
with:

View file

@ -97,6 +97,9 @@ android {
commit.set(true)
}
}
lint {
warningsAsErrors = true
}
}
play {

View file

@ -6,13 +6,10 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.webkit.WebView
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.NativeKeyEvent
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.test.ExperimentalTestApi
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
@ -27,13 +24,8 @@ 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.performKeyInput
import androidx.compose.ui.test.performKeyPress
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTextInputSelection
import androidx.compose.ui.test.performTextReplacement
import androidx.compose.ui.test.printToLog
import androidx.compose.ui.text.TextRange
import androidx.core.content.FileProvider
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider.getApplicationContext
@ -233,7 +225,7 @@ class MarkdownTests {
private fun ComposeTestRule.checkTitleEquals(title: String) =
onNode(hasAnySibling(hasContentDescription("Main Menu")).and(hasText(title)))
.assertIsDisplayed()
.waitUntilIsDisplayed()
private fun ComposeTestRule.typeMarkdown(markdown: String) =
onNode(hasSetTextAction()).performTextReplacement(markdown)
@ -243,7 +235,9 @@ class MarkdownTests {
val markdownMatcher = SemanticsMatcher("Markdown = [$markdown]") {
it.config.getOrNull(SemanticsProperties.EditableText)?.text == markdown
}
onNode(hasSetTextAction()).assert(markdownMatcher)
onNode(hasSetTextAction()).waitUntil {
assert(markdownMatcher)
}
}
private fun ComposeTestRule.openPreview() = onNodeWithText("Preview").performClick()
@ -258,12 +252,38 @@ class MarkdownTests {
private fun ComposeTestRule.clickSaveMenuItem() = onNodeWithText("Save").performClick()
private fun ComposeTestRule.verifyDialogIsShown(text: String) =
onNode(isDialog().and(hasAnyDescendant(hasText(text)))).assertIsDisplayed()
onNode(isDialog().and(hasAnyDescendant(hasText(text)))).waitUntilIsDisplayed()
private fun ComposeTestRule.verifyDialogIsNotShown() = onNode(isDialog()).assertIsNotDisplayed()
private fun ComposeTestRule.verifyDialogIsNotShown() =
onNode(isDialog()).waitUntilIsNotDisplayed()
private fun ComposeTestRule.discardChanges() = onNodeWithText("No").performClick()
private fun ComposeTestRule.verifyTextIsShown(text: String) =
onNodeWithText(text).assertIsDisplayed()
onNodeWithText(text).waitUntilIsDisplayed()
private val ASSERTION_TIMEOUT = 5_000L
private fun SemanticsNodeInteraction.waitUntil(assertion: SemanticsNodeInteraction.() -> Unit) {
val start = System.currentTimeMillis()
lateinit var assertionError: AssertionError
while (System.currentTimeMillis() - start < ASSERTION_TIMEOUT) {
try {
assertion()
return
} catch (e: AssertionError) {
assertionError = e
Thread.sleep(10)
}
}
throw assertionError
}
private fun SemanticsNodeInteraction.waitUntilIsDisplayed() = waitUntil {
assertIsDisplayed()
}
private fun SemanticsNodeInteraction.waitUntilIsNotDisplayed() = waitUntil {
assertIsNotDisplayed()
}
}

View file

@ -4,6 +4,7 @@ import android.os.Build
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.AnimatedContentTransitionScope
@ -27,6 +28,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.core.app.ActivityCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
@ -135,21 +137,21 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
}
composable(Route.HELP.path) {
MarkdownInfoScreen(
title = Route.HELP.title,
title = stringResource(Route.HELP.title),
file = "Cheatsheet.md",
navController = navController
)
}
composable(Route.ABOUT.path) {
MarkdownInfoScreen(
title = Route.ABOUT.title,
title = stringResource(Route.ABOUT.title),
file = "Libraries.md",
navController = navController
)
}
composable(Route.PRIVACY.path) {
MarkdownInfoScreen(
title = Route.PRIVACY.title,
title = stringResource(Route.PRIVACY.title),
file = "Privacy Policy.md",
navController = navController
)
@ -162,13 +164,14 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
enum class Route(
val path: String,
val title: String,
@StringRes
val title: Int,
val icon: ImageVector
) {
EDITOR("/", "Editor", Icons.Default.Edit),
SETTINGS("/settings", "Settings", Icons.Default.Settings),
SUPPORT("/support", "Support SimpleMarkdown", Icons.Default.Favorite),
HELP("/help", "Help", Icons.AutoMirrored.Filled.Help),
ABOUT("/about", "About", Icons.Default.Info),
PRIVACY("/privacy", "Privacy", Icons.Default.PrivacyTip),
EDITOR("/", R.string.title_editor, Icons.Default.Edit),
SETTINGS("/settings", R.string.title_settings, Icons.Default.Settings),
SUPPORT("/support", R.string.support_title, Icons.Default.Favorite),
HELP("/help", R.string.title_help, Icons.AutoMirrored.Filled.Help),
ABOUT("/about", R.string.title_about, Icons.Default.Info),
PRIVACY("/privacy", R.string.action_privacy, Icons.Default.PrivacyTip),
}

View file

@ -9,13 +9,16 @@ import com.wbrawner.simplemarkdown.utility.FileHelper
import com.wbrawner.simplemarkdown.utility.PersistentTree
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
import com.wbrawner.simplemarkdown.utility.ReviewHelper
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
class MarkdownApplication : Application() {
private val coroutineScope = CoroutineScope(Dispatchers.Default)
override fun onCreate() {
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
@ -27,9 +30,9 @@ class MarkdownApplication : Application() {
.penaltyLog()
.build())
Timber.plant(Timber.DebugTree())
GlobalScope.launch {
coroutineScope.launch {
try {
Timber.plant(PersistentTree.create(File(getExternalFilesDir(null), "logs")))
Timber.plant(PersistentTree.create(coroutineScope, File(getExternalFilesDir(null), "logs")))
} catch (e: Exception) {
Timber.e(e, "Unable to create PersistentTree")
}

View file

@ -1,5 +1,6 @@
package com.wbrawner.simplemarkdown
import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@ -22,7 +23,7 @@ data class EditorState(
val fileName: String = "Untitled.md",
val markdown: String = "",
val path: URI? = null,
val toast: String? = null,
val toast: ParameterizedText? = null,
val alert: AlertDialogModel? = null,
val saveCallback: (() -> Unit)? = null,
/**
@ -95,7 +96,7 @@ class MarkdownViewModel(
markdown = content,
initialMarkdown = content,
reloadToggle = currentState.reloadToggle.inv(),
toast = "Successfully loaded $name"
toast = ParameterizedText(R.string.file_loaded, arrayOf(name))
)
preferenceHelper[Preference.AUTOSAVE_URI] = actualLoadPath
} ?: throw IllegalStateException("Opened file was null")
@ -103,8 +104,8 @@ class MarkdownViewModel(
Timber.e(e, "Failed to open file at path: $actualLoadPath")
_state.value = _state.value.copy(
alert = AlertDialogModel(
text = "Failed to open file at path: $actualLoadPath",
confirmButton = AlertDialogModel.ButtonModel("OK", onClick = ::dismissAlert)
text = ParameterizedText(R.string.file_load_error),
confirmButton = AlertDialogModel.ButtonModel(ParameterizedText(R.string.ok), onClick = ::dismissAlert)
)
)
}
@ -130,20 +131,19 @@ class MarkdownViewModel(
fileName = name,
path = actualSavePath,
initialMarkdown = currentState.markdown,
toast = if (interactive) "Successfully saved $name" else null
toast = if (interactive) ParameterizedText(R.string.file_saved, arrayOf(name)) else null
)
Timber.i("Saved file $name to uri $actualSavePath")
Timber.i("Persisting autosave uri in shared prefs: $actualSavePath")
preferenceHelper[Preference.AUTOSAVE_URI] = actualSavePath
true
} catch (e: Exception) {
val message = "Failed to save file to $actualSavePath"
Timber.e(e, message)
Timber.e(e, "Failed to save file to $actualSavePath")
_state.value = _state.value.copy(
alert = AlertDialogModel(
text = message,
text = ParameterizedText(R.string.file_save_error),
confirmButton = AlertDialogModel.ButtonModel(
text = "OK",
text = ParameterizedText(R.string.ok),
onClick = ::dismissAlert
)
)
@ -189,9 +189,9 @@ class MarkdownViewModel(
Timber.i("Resetting view model to default state")
if (!force && _state.value.dirty) {
_state.value = _state.value.copy(alert = AlertDialogModel(
text = "Would you like to save your changes?",
text = ParameterizedText(R.string.prompt_save_changes),
confirmButton = AlertDialogModel.ButtonModel(
text = "Yes",
text = ParameterizedText(R.string.yes),
onClick = {
_state.value = _state.value.copy(
saveCallback = {
@ -201,7 +201,7 @@ class MarkdownViewModel(
}
),
dismissButton = AlertDialogModel.ButtonModel(
text = "No",
text = ParameterizedText(R.string.no),
onClick = {
reset(untitledFileName, true)
}
@ -209,7 +209,8 @@ class MarkdownViewModel(
))
return
}
_state.value = EditorState(fileName = untitledFileName)
_state.value =
EditorState(fileName = untitledFileName, reloadToggle = _state.value.reloadToggle.inv())
Timber.i("Removing autosave uri from shared prefs")
preferenceHelper[Preference.AUTOSAVE_URI] = null
}
@ -231,9 +232,29 @@ class MarkdownViewModel(
}
data class AlertDialogModel(
val text: String,
val text: ParameterizedText,
val confirmButton: ButtonModel,
val dismissButton: ButtonModel? = null
) {
data class ButtonModel(val text: String, val onClick: () -> Unit)
data class ButtonModel(val text: ParameterizedText, val onClick: () -> Unit)
}
data class ParameterizedText(@StringRes val text: Int, val params: Array<Any> = arrayOf()) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParameterizedText
if (text != other.text) return false
if (!params.contentEquals(other.params)) return false
return true
}
override fun hashCode(): Int {
var result = text
result = 31 * result + params.contentHashCode()
return result
}
}

View file

@ -52,15 +52,18 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity
import androidx.navigation.NavController
import com.wbrawner.simplemarkdown.AlertDialogModel
import com.wbrawner.simplemarkdown.EditorState
import com.wbrawner.simplemarkdown.MarkdownViewModel
import com.wbrawner.simplemarkdown.ParameterizedText
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.Route
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@ -98,7 +101,7 @@ fun MainScreen(
initialMarkdown = initialMarkdown,
markdown = markdown,
setMarkdown = viewModel::updateMarkdown,
message = toast,
message = toast?.stringRes(),
dismissMessage = viewModel::dismissToast,
alert = alert,
dismissAlert = viewModel::dismissAlert,
@ -181,17 +184,17 @@ private fun MainScreen(
onDismissRequest = dismissAlert,
confirmButton = {
TextButton(onClick = it.confirmButton.onClick) {
Text(it.confirmButton.text)
Text(stringResource(it.confirmButton.text.text))
}
},
dismissButton = {
it.dismissButton?.let { dismissButton ->
TextButton(onClick = dismissButton.onClick) {
Text(dismissButton.text)
Text(dismissButton.text.stringRes())
}
}
},
text = { Text(it.text) }
text = { Text(it.text.stringRes()) }
)
}
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
@ -215,28 +218,28 @@ private fun MainScreen(
), null
)
}) {
Icon(imageVector = Icons.Default.Share, contentDescription = "Share")
Icon(imageVector = Icons.Default.Share, contentDescription = stringResource(R.string.action_share))
}
Box {
var menuExpanded by remember { mutableStateOf(false) }
IconButton(onClick = { menuExpanded = true }) {
Icon(imageVector = Icons.Default.MoreVert, "Editor Actions")
Icon(imageVector = Icons.Default.MoreVert, stringResource(R.string.action_editor_actions))
}
DropdownMenu(expanded = menuExpanded,
onDismissRequest = { menuExpanded = false }) {
DropdownMenuItem(text = { Text("New") }, onClick = {
DropdownMenuItem(text = { Text(stringResource(R.string.action_new)) }, onClick = {
menuExpanded = false
reset()
})
DropdownMenuItem(text = { Text("Open") }, onClick = {
DropdownMenuItem(text = { Text(stringResource(R.string.action_open)) }, onClick = {
menuExpanded = false
openFileLauncher.launch(arrayOf("text/*"))
})
DropdownMenuItem(text = { Text("Save") }, onClick = {
DropdownMenuItem(text = { Text(stringResource(R.string.action_save)) }, onClick = {
menuExpanded = false
saveFile(null)
})
DropdownMenuItem(text = { Text("Save as…") },
DropdownMenuItem(text = { Text(stringResource(R.string.action_save_as )) },
onClick = {
menuExpanded = false
saveFileLauncher.launch(fileName)
@ -244,7 +247,7 @@ private fun MainScreen(
if (!enableWideLayout) {
DropdownMenuItem(text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Lock Swiping")
Text(stringResource(R.string.action_lock_swipe))
Checkbox(
checked = lockSwiping,
onCheckedChange = { lockSwiping = !lockSwiping })
@ -332,10 +335,10 @@ private fun TabbedMarkdownEditor(
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState { 2 }
TabRow(selectedTabIndex = pagerState.currentPage) {
Tab(text = { Text("Edit") },
Tab(text = { Text(stringResource(R.string.action_edit)) },
selected = pagerState.currentPage == 0,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } })
Tab(text = { Text("Preview") },
Tab(text = { Text(stringResource(R.string.action_preview)) },
selected = pagerState.currentPage == 1,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } })
}
@ -374,5 +377,7 @@ private fun TabbedMarkdownEditor(
@Composable
fun <P> MarkdownViewModel.collectAsState(prop: KProperty1<EditorState, P>, initial: P): State<P> =
state.map { prop.get(it) }
.collectAsState(initial)
remember(prop) { state.map { prop.get(it) }.distinctUntilChanged() }.collectAsState(initial)
@Composable
fun ParameterizedText.stringRes() = stringResource(text, *params)

View file

@ -31,7 +31,7 @@ fun MarkdownInfoScreen(
val context = LocalContext.current
val (markdown, setMarkdown) = remember { mutableStateOf("") }
LaunchedEffect(file) {
setMarkdown(context.assets.readAssetToString(file) ?: "Failed to load $file")
setMarkdown(context.assets.readAssetToString(file))
}
MarkdownText(
modifier = Modifier

View file

@ -17,6 +17,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.Route
@ -44,7 +45,7 @@ fun MarkdownNavigationDrawer(
tint = MaterialTheme.colorScheme.onSurface
)
Text(
text = "Simple Markdown",
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.titleLarge
)
}
@ -56,7 +57,7 @@ fun MarkdownNavigationDrawer(
icon = {
Icon(imageVector = route.icon, contentDescription = null)
},
label = { Text(route.title) },
label = { Text(stringResource(route.title)) },
selected = false,
onClick = {
navigate(route)

View file

@ -1,5 +1,6 @@
package com.wbrawner.simplemarkdown.ui
import android.annotation.SuppressLint
import android.view.ViewGroup
import android.webkit.WebView
import androidx.compose.foundation.isSystemInDarkTheme
@ -69,6 +70,7 @@ fun MarkdownText(modifier: Modifier = Modifier, markdown: String) {
}
}
@SuppressLint("SetJavaScriptEnabled")
@OptIn(ExperimentalStdlibApi::class)
@Composable
fun HtmlText(html: String, modifier: Modifier = Modifier) {

View file

@ -24,6 +24,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
@ -33,6 +34,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.model.Readability
@OptIn(ExperimentalMaterial3Api::class)
@ -96,7 +98,7 @@ fun MarkdownTextField(
visualTransformation = VisualTransformation.None,
innerTextField = innerTextField,
placeholder = {
Text("Markdown here…")
Text(stringResource(R.string.markdown_here))
},
singleLine = false,
enabled = true,

View file

@ -11,14 +11,14 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import com.wbrawner.simplemarkdown.R
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@ -32,6 +32,7 @@ fun MarkdownTopAppBar(
scrollBehavior: TopAppBarScrollBehavior? = null,
) {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
TopAppBar(
title = {
Text(text = title, maxLines = 1, overflow = TextOverflow.Ellipsis)
@ -40,10 +41,10 @@ fun MarkdownTopAppBar(
navigationIcon = {
val (icon, contentDescription, onClick) = remember {
if (backAsUp) {
Triple(Icons.AutoMirrored.Filled.ArrowBack, "Go Back", goBack)
Triple(Icons.AutoMirrored.Filled.ArrowBack, context.getString(R.string.action_back), goBack)
} else {
Triple(
Icons.Default.Menu, "Main Menu"
Icons.Default.Menu, context.getString(R.string.action_menu)
) {
coroutineScope.launch {
if (drawerState?.isOpen == true) {

View file

@ -26,10 +26,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.wbrawner.simplemarkdown.BuildConfig
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
import com.wbrawner.simplemarkdown.utility.Preference
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
@ -47,36 +50,36 @@ fun SettingsScreen(navController: NavController, preferenceHelper: PreferenceHel
.verticalScroll(rememberScrollState())
) {
BooleanPreference(
title = "Autosave",
enabledDescription = "Files will be saved automatically",
disabledDescription = "Files will not be saved automatically",
title = stringResource(R.string.pref_title_autosave),
enabledDescription = stringResource(R.string.pref_autosave_on),
disabledDescription = stringResource(R.string.pref_autosave_off),
preference = Preference.AUTOSAVE_ENABLED,
preferenceHelper = preferenceHelper
)
ListPreference(
title = "Dark mode",
options = listOf("auto", "dark", "light"),
title = stringResource(R.string.title_dark_mode),
options = stringArrayResource(R.array.pref_values_dark_mode),
preference = Preference.DARK_MODE,
preferenceHelper = preferenceHelper
)
BooleanPreference(
title = "Send crash reports",
enabledDescription = "Error reports will be sent",
disabledDescription = "Error reports will not be sent",
title = stringResource(R.string.pref_title_error_reports),
enabledDescription = stringResource(R.string.pref_error_reports_on),
disabledDescription = stringResource(R.string.pref_error_reports_off),
preference = Preference.ERROR_REPORTS_ENABLED,
preferenceHelper = preferenceHelper
)
BooleanPreference(
title = "Send analytics",
enabledDescription = "Analytics events will be sent",
disabledDescription = "Analytics events will not be sent",
title = stringResource(R.string.pref_title_analytics),
enabledDescription = stringResource(R.string.pref_analytics_on),
disabledDescription = stringResource(R.string.pref_analytics_off),
preference = Preference.ANALYTICS_ENABLED,
preferenceHelper = preferenceHelper
)
BooleanPreference(
title = "Readability highlighting",
enabledDescription = "Readability highlighting is on",
disabledDescription = "Readability highlighting is off",
title = stringResource(R.string.pref_title_readability),
enabledDescription = stringResource(R.string.pref_readability_on),
disabledDescription = stringResource(R.string.pref_readability_off),
preference = Preference.READABILITY_ENABLED,
preferenceHelper = preferenceHelper
)
@ -90,9 +93,9 @@ fun SettingsScreen(navController: NavController, preferenceHelper: PreferenceHel
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Column(verticalArrangement = Arrangement.Center) {
Text(text = "Force a crash", style = MaterialTheme.typography.bodyLarge)
Text(text = stringResource(R.string.action_force_crash), style = MaterialTheme.typography.bodyLarge)
Text(
text = "Purposefully crash the app for testing purposes",
text = stringResource(R.string.description_force_crash),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@ -155,7 +158,7 @@ fun BooleanPreference(
@Composable
fun ListPreference(
title: String,
options: List<String>,
options: Array<String>,
preference: Preference,
preferenceHelper: PreferenceHelper
) {
@ -171,7 +174,7 @@ fun ListPreference(
@Composable
fun ListPreference(
title: String, options: List<String>, selected: String, setSelected: (String) -> Unit
title: String, options: Array<String>, selected: String, setSelected: (String) -> Unit
) {
var dialogShowing by remember { mutableStateOf(false) }
Column(modifier = Modifier
@ -248,7 +251,7 @@ fun ListPreference_Preview() {
SimpleMarkdownTheme {
Surface {
ListPreference(
"Dark mode", listOf("Light", "Dark", "Auto"), selected, setSelected
"Dark mode", arrayOf("Light", "Dark", "Auto"), selected, setSelected
)
}
}

View file

@ -28,6 +28,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity
@ -39,7 +40,7 @@ import com.wbrawner.simplemarkdown.utility.SupportLinks
@Composable
fun SupportScreen(navController: NavController) {
Scaffold(topBar = {
MarkdownTopAppBar(title = "Support SimpleMarkdown", goBack = { navController.popBackStack() })
MarkdownTopAppBar(title = stringResource(R.string.support_title), goBack = { navController.popBackStack() })
}) { paddingValues ->
val context = LocalContext.current
Column(
@ -56,7 +57,7 @@ fun SupportScreen(navController: NavController) {
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Text(context.getString(R.string.support_info), textAlign = TextAlign.Center)
Text(stringResource(R.string.support_info), textAlign = TextAlign.Center)
Spacer(modifier = Modifier.height(8.dp))
Button(
modifier = Modifier.fillMaxWidth(),
@ -71,7 +72,7 @@ fun SupportScreen(navController: NavController) {
contentColor = Color.White
)
) {
Text(context.getString(R.string.action_view_github))
Text(stringResource(R.string.action_view_github))
}
Button(
modifier = Modifier.fillMaxWidth(),
@ -98,7 +99,7 @@ fun SupportScreen(navController: NavController) {
contentColor = Color.White
)
) {
Text(context.getString(R.string.action_rate))
Text(stringResource(R.string.action_rate))
}
SupportLinks()
}

View file

@ -10,17 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.Reader
fun View.showKeyboard() {
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
requestFocus()
}
fun View.hideKeyboard() =
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.hideSoftInputFromWindow(windowToken, 0)
suspend fun AssetManager.readAssetToString(asset: String): String? {
suspend fun AssetManager.readAssetToString(asset: String): String {
return withContext(Dispatchers.IO) {
open(asset).reader().use(Reader::readText)
}

View file

@ -2,6 +2,7 @@ package com.wbrawner.simplemarkdown.utility
import android.util.Log
import com.wbrawner.simplemarkdown.utility.PersistentTree.Companion.create
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -18,7 +19,10 @@ import java.util.*
* A [Timber.Tree] implementation that persists all logs to disk for retrieval later. Create
* instances via [create] instead of calling the constructor directly.
*/
class PersistentTree private constructor(private val logFile: File) : Timber.Tree() {
class PersistentTree private constructor(
private val coroutineScope: CoroutineScope,
private val logFile: File
) : Timber.Tree() {
private val dateFormat = object : ThreadLocal<SimpleDateFormat>() {
override fun initialValue(): SimpleDateFormat =
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
@ -26,7 +30,7 @@ class PersistentTree private constructor(private val logFile: File) : Timber.Tre
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
val timestamp = dateFormat.get()!!.format(System.currentTimeMillis())
GlobalScope.launch(Dispatchers.IO) {
coroutineScope.launch(Dispatchers.IO) {
val priorityLetter = when (priority) {
Log.ASSERT -> "A"
Log.DEBUG -> "D"
@ -63,14 +67,14 @@ class PersistentTree private constructor(private val logFile: File) : Timber.Tre
* created/written to
*/
@Throws(IllegalArgumentException::class, IOException::class)
suspend fun create(logDir: File): PersistentTree = withContext(Dispatchers.IO) {
suspend fun create(coroutineScope: CoroutineScope, logDir: File): PersistentTree = withContext(Dispatchers.IO) {
if (!logDir.mkdirs() && !logDir.isDirectory)
throw IllegalArgumentException("Unable to create log directory at ${logDir.absolutePath}")
val timestamp = SimpleDateFormat("yyyyMMddHHmmss", Locale.US).format(Date())
val logFile = File(logDir, "persistent-log-$timestamp.log")
if (!logFile.createNewFile())
throw IOException("Unable to create logFile at ${logFile.absolutePath}")
PersistentTree(logFile)
PersistentTree(coroutineScope, logFile)
}
}
}

View file

@ -23,7 +23,7 @@ interface PreferenceHelper {
fun <T> observe(preference: Preference): StateFlow<T>
}
class AndroidPreferenceHelper(context: Context, 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 states = mapOf(
Preference.ANALYTICS_ENABLED to MutableStateFlow(get(Preference.ANALYTICS_ENABLED)),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View file

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

View file

@ -4,8 +4,6 @@
<color name="colorPrimaryDark">#b71c1c</color>
<color name="colorAccent">#d32f2f</color>
<color name="colorBackground">#FFFFFF</color>
<color name="colorOnBackground">#000000</color>
<color name="colorBackgroundGitHub">#24292e</color>
<color name="colorWhite">#FFFFFFFF</color>
<color name="colorBackgroundPlayStore">#689f38</color>
</resources>

View file

@ -1,3 +0,0 @@
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>

View file

@ -2,65 +2,52 @@
<string name="app_name">Simple Markdown</string>
<string name="app_name_short">Markdown</string>
<string name="action_settings">Settings</string>
<string name="action_help">Help</string>
<string name="title_editor">Editor</string>
<string name="title_settings">Settings</string>
<string name="title_help">Help</string>
<string name="title_about">About</string>
<string name="action_edit">Edit</string>
<string name="action_preview">Preview</string>
<string name="ok">OK</string>
<string name="markdown_here">Markdown here…</string>
<string name="action_save">Save</string>
<string name="action_editor_actions">Editor Actions</string>
<string name="action_share">Share</string>
<string name="action_export">Export</string>
<string name="no_shareable_apps">Unable to share file - no capable apps installed</string>
<string name="share_file">Share file to…</string>
<string name="no_permissions">Unable to save file without permissions</string>
<string name="file_saved">Successfully saved %1$s</string>
<string name="action_load">Load</string>
<string name="open_file">Select a file to open</string>
<string name="no_filebrowser">No file browser apps found</string>
<string name="file_save_error">An error occurred while saving the file</string>
<string name="file_loaded">File successfully loaded</string>
<string name="error_write">An error occurred while writing the file</string>
<string name="file_loaded">Successfully loaded %1$s</string>
<string name="file_load_error">An error occurred while opening the file</string>
<string name="action_libraries">Libraries</string>
<string name="action_back">Back</string>
<string name="action_menu">Main Menu</string>
<string name="action_new">New</string>
<string name="action_done">Done</string>
<string name="action_open">Open</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="title_activity_settings">Settings</string>
<string name="pref_title_autosave">Enable autosave</string>
<string name="pref_description_autosave">Automatically save files when closing the app</string>
<string name="action_lock_swipe">Lock Swiping</string>
<string name="action_select">Select</string>
<string name="action_privacy">Privacy</string>
<string name="pref_key_error_reports_enabled">crashlytics.enable</string>
<string name="pref_title_error_reports">Enable automated error reports</string>
<string name="pref_error_reports_off">Error reports will not be sent</string>
<string name="pref_error_reports_on">Error reports will be sent</string>
<string name="readability_enabled">readability.enable</string>
<string name="pref_title_analytics">Send analytics</string>
<string name="pref_analytics_off">Analytics events will not be sent</string>
<string name="pref_analytics_on">Analytics events will be sent</string>
<string name="action_force_crash">Force a crash</string>
<string name="description_force_crash">Purposefully crash the app for testing purposes</string>
<string name="pref_title_readability">Enable readability highlighting (experimental)</string>
<string name="pref_readability_off">Readability highlighting is off</string>
<string name="pref_readability_on">Readability highlighting is on</string>
<string name="pref_autosave_on">Files will be automatically saved</string>
<string name="pref_autosave_off">Files will not be automatically saved</string>
<string name="pref_custom_css">pref.custom_css</string>
<string name="pref_title_custom_css">Custom CSS</string>
<string name="pref_description_custom_css">Paste or write your own CSS to be used for the preview pane</string>
<string name="pref_custom_css_default" translatable="false">pre {overflow:scroll; padding:15px; background: #F1F1F1;}</string>
<string name="pref_custom_css_default_dark" translatable="false">body{background:
#000000;color: #F1F1F1;}a{color:#7b91ff;}pre{background:#111111;}</string>
<string name="pref_key_dark_mode">darkMode</string>
<string name="title_dark_mode">Dark Mode</string>
<string name="pref_value_light">Light</string>
<string name="pref_value_dark">Dark</string>
<string name="pref_value_auto">Auto</string>
<!-- <string name="pref_value_auto">Auto</string>-->
<string name="pref_key_dark_mode_light" translatable="false">light</string>
<string name="pref_key_dark_mode_dark" translatable="false">dark</string>
<string name="pref_key_dark_mode_auto" translatable="false">auto</string>
<string name="save_changes">Save Changes</string>
<string name="prompt_save_changes">Would you like to save your changes?</string>
<string name="action_discard">Discard</string>
<string name="action_save_as">Save as…</string>
<string name="support_info">SimpleMarkdown is a personal project of mine that I develop and
maintain in my free time. I very much appreciate any and all forms of support, whether
@ -73,12 +60,11 @@
<string name="action_view_github">View SimpleMarkdown on GitHub</string>
<string name="support_title">Support SimpleMarkdown</string>
<string name="action_rate">Rate SimpleMarkdown</string>
<string name="description_heart">Heart</string>
<string-array name="pref_entries_dark_mode">
<item>@string/pref_value_light</item>
<item>@string/pref_value_dark</item>
<item>@string/pref_value_auto</item>
</string-array>
<!-- <string-array name="pref_entries_dark_mode">-->
<!-- <item>@string/pref_value_light</item>-->
<!-- <item>@string/pref_value_dark</item>-->
<!-- <item>@string/pref_value_auto</item>-->
<!-- </string-array>-->
<string-array name="pref_values_dark_mode">
<item>@string/pref_key_dark_mode_light</item>
<item>@string/pref_key_dark_mode_dark</item>

View file

@ -5,14 +5,11 @@ import androidx.lifecycle.viewmodel.CreationExtras
import com.wbrawner.simplemarkdown.utility.Preference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestResult
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.Assert.assertEquals
@ -25,8 +22,6 @@ import org.junit.Test
import timber.log.Timber
import java.io.File
import java.net.URI
import java.util.Deque
import java.util.concurrent.ConcurrentLinkedDeque
class MarkdownViewModelTest {
private lateinit var fileHelper: FakeFileHelper
@ -185,7 +180,7 @@ class MarkdownViewModelTest {
requireNotNull(onClick)
onClick.invoke()
}
assertEquals(viewModel.state.value, EditorState())
assertEquals(viewModel.state.value, EditorState(reloadToggle = 0.inv()))
}
@Test
@ -204,7 +199,7 @@ class MarkdownViewModelTest {
viewModel.save(uri)
assertNotNull(viewModel.state.value.saveCallback)
requireNotNull(viewModel.state.value.saveCallback).invoke()
assertEquals(viewModel.state.value, EditorState())
assertEquals(viewModel.state.value, EditorState(reloadToggle = 0.inv()))
}
@Test

View file

@ -47,6 +47,9 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
lint {
warningsAsErrors = true
}
}
dependencies {

View file

@ -33,6 +33,9 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
lint {
warningsAsErrors = true
}
}
dependencies {

View file

@ -1,6 +1,6 @@
[versions]
acra = "5.11.3"
activityKtx = "1.9.0"
activityKtx = "1.9.1"
animationCore = "1.6.8"
appcompat = "1.7.0"
billing = "7.0.0"
@ -20,7 +20,7 @@ androidGradlePlugin = "8.5.1"
hamcrestCore = "1.3"
junit = "4.13.2"
kotlin = "2.0.0"
lifecycleViewmodelKtx = "2.8.3"
lifecycleViewmodelKtx = "2.8.4"
material = "1.12.0"
material3WindowSizeClassAndroid = "1.2.1"
materialIconsCore = "1.6.8"

View file

@ -33,6 +33,9 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
lint {
warningsAsErrors = true
}
}
dependencies {