Migrate to Jetpack Compose + Material3

This commit is contained in:
William Brawner 2023-09-10 22:00:58 -06:00
parent e352add928
commit 643345493e
21 changed files with 1503 additions and 628 deletions

View file

@ -4,7 +4,6 @@ import java.util.*
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android-extensions")
id("kotlin-android") id("kotlin-android")
id("kotlin-kapt") id("kotlin-kapt")
id("com.osacky.fladle") id("com.osacky.fladle")
@ -36,7 +35,7 @@ android {
) )
} }
} }
compileSdk = 33 compileSdk = 34
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
@ -47,7 +46,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "com.wbrawner.simplemarkdown" applicationId = "com.wbrawner.simplemarkdown"
minSdk = 23 minSdk = 23
targetSdk = 33 targetSdk = 34
versionCode = 41 versionCode = 41
versionName = "0.8.16" versionName = "0.8.16"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@ -89,6 +88,12 @@ android {
execution = "ANDROIDX_TEST_ORCHESTRATOR" execution = "ANDROIDX_TEST_ORCHESTRATOR"
} }
namespace = "com.wbrawner.simplemarkdown" namespace = "com.wbrawner.simplemarkdown"
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
playConfigs { playConfigs {
register("play") { register("play") {
enabled.set(true) enabled.set(true)
@ -105,9 +110,10 @@ play {
} }
dependencies { dependencies {
val navigationVersion = "2.5.3" val navigationVersion = "2.7.2"
implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion") implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion")
implementation("androidx.navigation:navigation-ui-ktx:$navigationVersion") implementation("androidx.navigation:navigation-ui-ktx:$navigationVersion")
implementation("androidx.navigation:navigation-compose:$navigationVersion")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testImplementation("org.robolectric:robolectric:4.2.1") testImplementation("org.robolectric:robolectric:4.2.1")
val espressoVersion = "3.5.1" val espressoVersion = "3.5.1"
@ -119,18 +125,32 @@ dependencies {
androidTestUtil("androidx.test:orchestrator:1.4.2") androidTestUtil("androidx.test:orchestrator:1.4.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0") androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("com.wbrawner.plausible:plausible-android:0.1.0-SNAPSHOT") implementation("com.wbrawner.plausible:plausible-android:0.1.0-SNAPSHOT")
implementation("androidx.appcompat:appcompat:1.6.0") implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.preference:preference-ktx:1.2.0") implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.fragment:fragment-ktx:1.5.5") implementation("androidx.fragment:fragment-ktx:1.6.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("com.google.android.material:material:1.8.0") implementation("com.google.android.material:material:1.9.0")
implementation("androidx.legacy:legacy-support-v13:1.0.0") implementation("androidx.legacy:legacy-support-v13:1.0.0")
implementation("com.commonsware.cwac:anddown:0.4.0") implementation("com.commonsware.cwac:anddown:0.4.0")
implementation("com.jakewharton.timber:timber:5.0.1") implementation("com.jakewharton.timber:timber:5.0.1")
implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.browser:browser:1.4.0") implementation("androidx.browser:browser:1.6.0")
val coroutinesVersion = "1.6.4" val composeBom = platform("androidx.compose:compose-bom:2023.08.00")
implementation(composeBom)
androidTestImplementation(composeBom)
implementation("androidx.compose.runtime:runtime")
implementation("androidx.compose.ui:ui")
implementation("androidx.activity:activity-compose")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.foundation:foundation-layout")
implementation("androidx.compose.material:material")
implementation("androidx.compose.runtime:runtime-livedata")
implementation("androidx.compose.ui:ui-tooling")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
val coroutinesVersion = "1.7.1"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
val lifecycleVersion = "2.2.0" val lifecycleVersion = "2.2.0"
implementation("androidx.lifecycle:lifecycle-extensions:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-extensions:$lifecycleVersion")

View file

@ -12,12 +12,10 @@
android:resizeableActivity="true" android:resizeableActivity="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/Theme.App.Starting"
tools:ignore="AllowBackup" tools:ignore="AllowBackup"
tools:targetApi="n"> tools:targetApi="n">
<activity <activity android:name=".view.activity.MainActivity"
android:name=".view.activity.SplashActivity"
android:theme="@style/AppTheme.Splash"
android:exported="true" android:exported="true"
android:label="@string/app_name_short"> android:label="@string/app_name_short">
<intent-filter> <intent-filter>
@ -40,7 +38,6 @@
<data android:host="*" /> <data android:host="*" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".view.activity.MainActivity" android:exported="false" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"

View file

@ -1,5 +1,3 @@
## Privacy Policy
The internet access permission is requested primarily for retrieving images from the internet in The internet access permission is requested primarily for retrieving images from the internet in
case you embed them in your markdown, but it also allows me to send automated error and crash case you embed them in your markdown, but it also allows me to send automated error and crash
reports to myself whenever the app runs into an issue. These error reports are opt-out, and are reports to myself whenever the app runs into an issue. These error reports are opt-out, and are

View file

@ -0,0 +1,210 @@
package com.wbrawner.simplemarkdown.ui
import android.content.Intent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DismissibleDrawerSheet
import androidx.compose.material3.DismissibleNavigationDrawer
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity
import androidx.navigation.NavController
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.view.activity.Route
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
var lockSwiping by remember { mutableStateOf(false) }
MarkdownNavigationDrawer(navigate = { navController.navigate(it.path) }) { drawerState ->
Scaffold(topBar = {
val fileName by viewModel.fileName.collectAsState()
val context = LocalContext.current
MarkdownTopAppBar(title = fileName,
backAsUp = false,
navController = navController,
drawerState = drawerState,
actions = {
IconButton(onClick = {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_TEXT, viewModel.markdownUpdates.value)
shareIntent.type = "text/plain"
startActivity(
context, Intent.createChooser(
shareIntent, context.getString(R.string.share_file)
), null
)
}) {
Icon(imageVector = Icons.Default.Share, contentDescription = "Share")
}
Box {
var menuExpanded by remember { mutableStateOf(false) }
IconButton(onClick = { menuExpanded = true }) {
Icon(imageVector = Icons.Default.MoreVert, "Editor Actions")
}
DropdownMenu(expanded = menuExpanded,
onDismissRequest = { menuExpanded = false }) {
DropdownMenuItem(text = { Text("New") }, onClick = {
menuExpanded = false
})
DropdownMenuItem(text = { Text("Open") }, onClick = {
menuExpanded = false
})
DropdownMenuItem(text = { Text("Save") }, onClick = {
menuExpanded = false
})
DropdownMenuItem(text = { Text("Save as…") },
onClick = {
menuExpanded = false
})
DropdownMenuItem(text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Lock Swiping")
Checkbox(checked = lockSwiping, onCheckedChange = { lockSwiping = !lockSwiping })
}
}, onClick = {
lockSwiping = !lockSwiping
menuExpanded = false
})
}
}
})
}) { paddingValues ->
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState { 2 }
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
TabRow(selectedTabIndex = pagerState.currentPage) {
Tab(text = { Text("Edit") },
selected = pagerState.currentPage == 0,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } })
Tab(text = { Text("Preview") },
selected = pagerState.currentPage == 1,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } })
}
HorizontalPager(
modifier = Modifier.weight(1f), state = pagerState,
userScrollEnabled = !lockSwiping
) { page ->
val markdown by viewModel.markdownUpdates.collectAsState()
if (page == 0) {
BasicTextField(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
value = markdown,
onValueChange = { viewModel.updateMarkdown(it) },
textStyle = TextStyle.Default.copy(fontFamily = FontFamily.Monospace)
)
} else {
MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown)
}
}
}
}
}
}
@Composable
fun MarkdownNavigationDrawer(
navigate: (Route) -> Unit, content: @Composable (drawerState: DrawerState) -> Unit
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
DismissibleNavigationDrawer(drawerState = drawerState, drawerContent = {
DismissibleDrawerSheet {
Route.entries.forEach { route ->
if (route == Route.EDITOR) {
return@forEach
}
NavigationDrawerItem(icon = {
Icon(imageVector = route.icon, contentDescription = null)
},
label = { Text(route.title) },
selected = false,
onClick = { navigate(route) })
}
}
}) {
content(drawerState)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MarkdownTopAppBar(
title: String,
navController: NavController,
backAsUp: Boolean = true,
drawerState: DrawerState? = null,
actions: (@Composable RowScope.() -> Unit)? = null
) {
val coroutineScope = rememberCoroutineScope()
TopAppBar(title = {
Text(title)
}, navigationIcon = {
val (icon, contentDescription, onClick) = remember {
if (backAsUp) {
Triple(Icons.Default.ArrowBack, "Go Back") { navController.popBackStack() }
} else {
Triple(
Icons.Default.Menu, "Main Menu"
) {
coroutineScope.launch {
if (drawerState?.isOpen == true) {
drawerState.close()
} else {
drawerState?.open()
}
}
}
}
}
IconButton(onClick = { onClick() }) {
Icon(imageVector = icon, contentDescription = contentDescription)
}
}, actions = actions ?: {})
}

View file

@ -0,0 +1,45 @@
package com.wbrawner.simplemarkdown.ui
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController
import com.wbrawner.simplemarkdown.utility.readAssetToString
import com.wbrawner.simplemarkdown.utility.toHtml
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
@Composable
fun MarkdownInfoScreen(
title: String,
file: String,
navController: NavController,
) {
Scaffold(
topBar = {
MarkdownTopAppBar(
title = title,
navController = navController,
)
}
) { paddingValues ->
val context = LocalContext.current
var markdown by remember { mutableStateOf("") }
LaunchedEffect(file) {
markdown = context.assets.readAssetToString(file)?.toHtml()?: "Failed to load $file"
}
MarkdownPreview(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
markdown = markdown
)
}
}

View file

@ -0,0 +1,69 @@
package com.wbrawner.simplemarkdown.ui
import android.content.res.Configuration
import android.webkit.WebView
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.preference.PreferenceManager
import com.wbrawner.simplemarkdown.BuildConfig
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.utility.toHtml
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun MarkdownPreview(modifier: Modifier = Modifier, markdown: String) {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
var style by remember { mutableStateOf("") }
LaunchedEffect(context) {
val isNightMode = AppCompatDelegate.getDefaultNightMode() ==
AppCompatDelegate.MODE_NIGHT_YES
|| context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
val defaultCssId = if (isNightMode) {
R.string.pref_custom_css_default_dark
} else {
R.string.pref_custom_css_default
}
val css = withContext(Dispatchers.IO) {
@Suppress("ConstantConditionIf")
if (!BuildConfig.ENABLE_CUSTOM_CSS) {
context.getString(defaultCssId)
} else {
PreferenceManager.getDefaultSharedPreferences(context)
.getString(
context.getString(R.string.pref_custom_css),
context.getString(defaultCssId)
)
}
}
style = "<style>$css</style>"
}
AndroidView(
modifier = modifier,
factory = { context ->
WebView(context)
},
update = { preview ->
coroutineScope.launch {
preview.loadDataWithBaseURL(null,
style + markdown.toHtml(),
"text/html",
"UTF-8", null
)
preview.setBackgroundColor(0x01000000)
}
}
)
}

View file

@ -0,0 +1,263 @@
package com.wbrawner.simplemarkdown.ui
import android.content.SharedPreferences
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import androidx.navigation.NavController
import androidx.preference.PreferenceManager
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
const val PREF_KEY_AUTOSAVE = "autosave"
const val PREF_KEY_DARK_MODE = "darkMode"
const val PREF_KEY_ERROR_REPORTS = "crashlytics.enable"
const val PREF_KEY_ANALYTICS = "analytics.enable"
const val PREF_KEY_READABILITY = "readability.enable"
@Composable
fun SettingsScreen(navController: NavController) {
Scaffold(topBar = {
MarkdownTopAppBar(title = "Settings", navController = navController)
}) { paddingValues ->
val context = LocalContext.current
val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
BooleanPreference(
title = "Autosave",
enabledDescription = "Files will be saved automatically",
disabledDescription = "Files will not be saved automatically",
preferenceKey = PREF_KEY_AUTOSAVE,
sharedPreferences = sharedPreferences
)
ListPreference(
title = "Dark mode",
options = listOf("Auto", "Dark", "Light"),
defaultValue = "Auto",
preferenceKey = PREF_KEY_DARK_MODE,
sharedPreferences = sharedPreferences
)
BooleanPreference(
title = "Send crash reports",
enabledDescription = "Error reports will be sent",
disabledDescription = "Error reports will not be sent",
preferenceKey = PREF_KEY_ERROR_REPORTS,
sharedPreferences = sharedPreferences
)
BooleanPreference(
title = "Send analytics",
enabledDescription = "Analytics events will be sent",
disabledDescription = "Analytics events will not be sent",
preferenceKey = PREF_KEY_ANALYTICS,
sharedPreferences = sharedPreferences
)
BooleanPreference(
title = "Readability highlighting",
enabledDescription = "Readability highlighting is on",
disabledDescription = "Readability highlighting is off",
preferenceKey = PREF_KEY_READABILITY,
sharedPreferences = sharedPreferences,
defaultValue = false
)
}
}
}
@Composable
fun BooleanPreference(
title: String,
enabledDescription: String,
disabledDescription: String,
preferenceKey: String,
sharedPreferences: SharedPreferences,
defaultValue: Boolean = true
) {
var enabled by remember {
mutableStateOf(
sharedPreferences.getBoolean(
preferenceKey, defaultValue
)
)
}
BooleanPreference(title = title,
enabledDescription = enabledDescription,
disabledDescription = disabledDescription,
enabled = enabled,
setEnabled = {
enabled = it
sharedPreferences.edit {
putBoolean(preferenceKey, it)
}
})
}
@Composable
fun BooleanPreference(
title: String,
enabledDescription: String,
disabledDescription: String,
enabled: Boolean,
setEnabled: (Boolean) -> Unit
) {
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
setEnabled(!enabled)
}
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Column(verticalArrangement = Arrangement.Center) {
Text(text = title, style = MaterialTheme.typography.bodyLarge)
Text(
text = if (enabled) enabledDescription else disabledDescription,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(checked = enabled, onCheckedChange = setEnabled)
}
}
@Composable
fun ListPreference(
title: String,
options: List<String>,
defaultValue: String,
preferenceKey: String,
sharedPreferences: SharedPreferences
) {
var selected by remember {
mutableStateOf(
sharedPreferences.getString(
preferenceKey, defaultValue
) ?: defaultValue
)
}
ListPreference(title = title, options = options, selected = selected, setSelected = {
selected = it
sharedPreferences.edit {
putString(preferenceKey, it)
}
})
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ListPreference(
title: String, options: List<String>, selected: String, setSelected: (String) -> Unit
) {
var dialogShowing by remember { mutableStateOf(false) }
Column(modifier = Modifier
.fillMaxWidth()
.clickable {
dialogShowing = true
}
.padding(16.dp), verticalArrangement = Arrangement.Center) {
Text(text = title, style = MaterialTheme.typography.bodyLarge)
Text(
text = selected,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (dialogShowing) {
AlertDialog(
title = {
Text(title)
},
onDismissRequest = { dialogShowing = false },
confirmButton = {
TextButton(onClick = { dialogShowing = false }) {
Text("Cancel")
}
},
text = {
Column {
options.forEach { option ->
val onClick = {
setSelected(option)
dialogShowing = false
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(selected = option == selected, onClick = onClick)
Text(option)
}
}
}
}
)
}
}
@Preview
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
fun BooleanPreference_Preview() {
val (enabled, setEnabled) = remember { mutableStateOf(true) }
SimpleMarkdownTheme {
Surface {
BooleanPreference(
"Autosave",
"Files will be saved automatically",
"Files will not be saved automatically",
enabled,
setEnabled
)
}
}
}
@Preview
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
fun ListPreference_Preview() {
val (selected, setSelected) = remember { mutableStateOf("Auto") }
SimpleMarkdownTheme {
Surface {
ListPreference(
"Dark mode", listOf("Light", "Dark", "Auto"), selected, setSelected
)
}
}
}

View file

@ -0,0 +1,2 @@
package com.wbrawner.simplemarkdown.ui

View file

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

View file

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

View file

@ -1,19 +1,87 @@
package com.wbrawner.simplemarkdown.view.activity package com.wbrawner.simplemarkdown.view.activity
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.core.EaseIn
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Help
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.PrivacyTip
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Text
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.wbrawner.plausible.android.Plausible import com.wbrawner.plausible.android.Plausible
import com.wbrawner.simplemarkdown.R import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.ui.MainScreen
import com.wbrawner.simplemarkdown.ui.MarkdownInfoScreen
import com.wbrawner.simplemarkdown.ui.SettingsScreen
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
const val KEY_AUTOSAVE = "autosave"
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback { class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback {
private val viewModel: MarkdownViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) lifecycleScope.launch {
val darkMode = withContext(Dispatchers.IO) {
val darkModeValue = getStringPref(
R.string.pref_key_dark_mode,
getString(R.string.pref_value_auto)
)
return@withContext when {
darkModeValue.equals(
getString(R.string.pref_value_light),
ignoreCase = true
) -> AppCompatDelegate.MODE_NIGHT_NO
darkModeValue.equals(
getString(R.string.pref_value_dark),
ignoreCase = true
) -> AppCompatDelegate.MODE_NIGHT_YES
else -> {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
} else {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
}
}
}
AppCompatDelegate.setDefaultNightMode(darkMode)
}
WindowCompat.setDecorFitsSystemWindows(window, false)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val preferences = mutableMapOf<String, String>() val preferences = mutableMapOf<String, String>()
preferences["Autosave"] = sharedPreferences.getBoolean("autosave", true).toString() preferences["Autosave"] = sharedPreferences.getBoolean("autosave", true).toString()
@ -25,6 +93,53 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
getBooleanPref(R.string.pref_key_error_reports_enabled, true).toString() getBooleanPref(R.string.pref_key_error_reports_enabled, true).toString()
preferences["Readability"] = getBooleanPref(R.string.readability_enabled, false).toString() preferences["Readability"] = getBooleanPref(R.string.readability_enabled, false).toString()
Plausible.event("settings", props = preferences, url = "/") Plausible.event("settings", props = preferences, url = "/")
setContent {
SimpleMarkdownTheme {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Route.EDITOR.path,
enterTransition = { fadeIn(
animationSpec = tween(
300, easing = LinearEasing
)
) + slideIntoContainer(
animationSpec = tween(300, easing = EaseIn),
towards = AnimatedContentTransitionScope.SlideDirection.Start
) },
popEnterTransition = { EnterTransition.None },
popExitTransition = {
fadeOut(
animationSpec = tween(
300, easing = LinearEasing
)
) + slideOutOfContainer(
animationSpec = tween(300, easing = EaseIn),
towards = AnimatedContentTransitionScope.SlideDirection.End
)
}
) {
composable(Route.EDITOR.path) {
MainScreen(navController = navController, viewModel = viewModel)
}
composable(Route.SETTINGS.path) {
SettingsScreen(navController = navController)
}
composable(Route.SUPPORT.path) {
Text("To do")
}
composable(Route.HELP.path) {
MarkdownInfoScreen(title = Route.HELP.title, file = "Cheatsheet.md", navController = navController)
}
composable(Route.ABOUT.path) {
MarkdownInfoScreen(title = Route.ABOUT.title, file = "Libraries.md", navController = navController)
}
composable(Route.PRIVACY.path) {
MarkdownInfoScreen(title = Route.PRIVACY.title, file = "Privacy Policy.md", navController = navController)
}
}
}
}
} }
override fun onBackPressed() { override fun onBackPressed() {
@ -34,12 +149,27 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
} }
} }
fun Context.getBooleanPref(@StringRes key: Int, defaultValue: Boolean) = PreferenceManager.getDefaultSharedPreferences(this).getBoolean( enum class Route(
getString(key), val path: String,
defaultValue val title: String,
) 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.Default.Help),
ABOUT("/about", "About", Icons.Default.Info),
PRIVACY("/privacy", "Privacy", Icons.Default.PrivacyTip),
}
fun Context.getStringPref(@StringRes key: Int, defaultValue: String?) = PreferenceManager.getDefaultSharedPreferences(this).getString( fun Context.getBooleanPref(@StringRes key: Int, defaultValue: Boolean) =
getString(key), PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
defaultValue getString(key),
) defaultValue
)
fun Context.getStringPref(@StringRes key: Int, defaultValue: String?) =
PreferenceManager.getDefaultSharedPreferences(this).getString(
getString(key),
defaultValue
)

View file

@ -1,57 +0,0 @@
package com.wbrawner.simplemarkdown.view.activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.wbrawner.simplemarkdown.R
import kotlinx.coroutines.*
import timber.log.Timber
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val darkMode = withContext(Dispatchers.IO) {
val darkModeValue = PreferenceManager.getDefaultSharedPreferences(this@SplashActivity)
.getString(
getString(R.string.pref_key_dark_mode),
getString(R.string.pref_value_auto)
)
return@withContext when {
darkModeValue.equals(getString(R.string.pref_value_light), ignoreCase = true) -> AppCompatDelegate.MODE_NIGHT_NO
darkModeValue.equals(getString(R.string.pref_value_dark), ignoreCase = true) -> AppCompatDelegate.MODE_NIGHT_YES
else -> {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
} else {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
}
}
}
AppCompatDelegate.setDefaultNightMode(darkMode)
val uri = intent?.data?.let {
Timber.d("Using uri from intent: $it")
it
} ?: run {
Timber.d("No intent provided to load data from")
null
}
val startIntent = Intent(this@SplashActivity, MainActivity::class.java)
.apply {
data = uri
}
startActivity(startIntent)
finish()
}
}
}

View file

@ -1,328 +1,335 @@
package com.wbrawner.simplemarkdown.view.fragment //package com.wbrawner.simplemarkdown.view.fragment
//
import android.Manifest //import android.app.Activity
import android.app.Activity //import android.content.Context
import android.content.Context //import android.content.Intent
import android.content.Intent //import android.content.pm.PackageManager
import android.content.pm.PackageManager //import android.content.res.Configuration
import android.content.res.Configuration //import android.os.Build
import android.os.Build //import android.os.Bundle
import android.os.Bundle //import android.view.*
import android.view.* //import android.webkit.MimeTypeMap
import android.webkit.MimeTypeMap //import android.widget.Toast
import android.widget.Toast //import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AlertDialog //import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatActivity //import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityCompat //import androidx.fragment.app.Fragment
import androidx.core.content.ContextCompat //import androidx.fragment.app.viewModels
import androidx.fragment.app.Fragment //import androidx.lifecycle.lifecycleScope
import androidx.fragment.app.viewModels //import androidx.navigation.fragment.findNavController
import androidx.lifecycle.lifecycleScope //import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.fragment.findNavController //import androidx.navigation.ui.setupWithNavController
import androidx.navigation.ui.AppBarConfiguration //import androidx.preference.PreferenceManager
import androidx.navigation.ui.setupWithNavController //import com.wbrawner.plausible.android.Plausible
import androidx.preference.PreferenceManager //import com.wbrawner.simplemarkdown.R
import com.wbrawner.plausible.android.Plausible //import com.wbrawner.simplemarkdown.databinding.FragmentMainBinding
import com.wbrawner.simplemarkdown.R //import com.wbrawner.simplemarkdown.utility.ErrorHandler
import com.wbrawner.simplemarkdown.utility.ErrorHandler //import com.wbrawner.simplemarkdown.utility.errorHandlerImpl
import com.wbrawner.simplemarkdown.utility.errorHandlerImpl //import com.wbrawner.simplemarkdown.view.adapter.EditPagerAdapter
import com.wbrawner.simplemarkdown.view.adapter.EditPagerAdapter //import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel //import kotlinx.coroutines.Dispatchers
import kotlinx.android.synthetic.main.fragment_main.* //import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers //import kotlinx.coroutines.withContext
import kotlinx.coroutines.launch //import timber.log.Timber
import kotlinx.coroutines.withContext //
import timber.log.Timber //class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallback {
//
class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallback { // private val viewModel: MarkdownViewModel by viewModels()
// private var appBarConfiguration: AppBarConfiguration? = null
private val viewModel: MarkdownViewModel by viewModels() // private val errorHandler: ErrorHandler by errorHandlerImpl()
private var appBarConfiguration: AppBarConfiguration? = null // private var _binding: FragmentMainBinding? = null
private val errorHandler: ErrorHandler by errorHandlerImpl() // private val binding: FragmentMainBinding
// get() = _binding!!
override fun onAttach(context: Context) { //
super.onAttach(context) // override fun onAttach(context: Context) {
if (context !is Activity) return // super.onAttach(context)
lifecycleScope.launch { // if (context !is Activity) return
viewModel.load(context, context.intent?.data) // lifecycleScope.launch {
context.intent?.data = null // viewModel.load(context, context.intent?.data)
} // context.intent?.data = null
} // }
// }
override fun onCreate(savedInstanceState: Bundle?) { //
super.onCreate(savedInstanceState) // override fun onCreate(savedInstanceState: Bundle?) {
setHasOptionsMenu(true) // super.onCreate(savedInstanceState)
} // setHasOptionsMenu(true)
// }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { //
inflater.inflate(R.menu.menu_edit, menu) // override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // inflater.inflate(R.menu.menu_edit, menu)
menu.findItem(R.id.action_save_as) // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
?.setAlphabeticShortcut('S', KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON) // menu.findItem(R.id.action_save_as)
} // ?.setAlphabeticShortcut('S', KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON)
} // }
// }
override fun onCreateView( //
inflater: LayoutInflater, // override fun onCreateView(
container: ViewGroup?, // inflater: LayoutInflater,
savedInstanceState: Bundle? // container: ViewGroup?,
): View? = // savedInstanceState: Bundle?
inflater.inflate(R.layout.fragment_main, container, false) // ): View {
// _binding = FragmentMainBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // return binding.root
with(findNavController()) { // }
appBarConfiguration = AppBarConfiguration(graph, drawerLayout) //
toolbar.setupWithNavController(this, appBarConfiguration!!) // override fun onDestroyView() {
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar) // super.onDestroyView()
navigationView.setupWithNavController(this) // _binding = null
} // }
val adapter = EditPagerAdapter(childFragmentManager, view.context) //
pager.adapter = adapter // override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
pager.addOnPageChangeListener(adapter) // with(findNavController()) {
pager.pageMargin = 1 // appBarConfiguration = AppBarConfiguration(graph, binding.drawerLayout)
pager.setPageMarginDrawable(R.color.colorAccent) // binding.toolbar.setupWithNavController(this, appBarConfiguration!!)
tabLayout.setupWithViewPager(pager) // (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { // binding.navigationView.setupWithNavController(this)
tabLayout!!.visibility = View.GONE // }
} // val adapter = EditPagerAdapter(childFragmentManager, view.context)
@Suppress("CAST_NEVER_SUCCEEDS") // binding.pager.adapter = adapter
viewModel.fileName.observe(viewLifecycleOwner) { // binding.pager.addOnPageChangeListener(adapter)
toolbar?.title = it // binding.pager.pageMargin = 1
} // binding.pager.setPageMarginDrawable(R.color.colorAccent)
} // binding.tabLayout.setupWithViewPager(binding.pager)
// if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
override fun onOptionsItemSelected(item: MenuItem): Boolean { // binding.tabLayout.visibility = View.GONE
return when (item.itemId) { // }
android.R.id.home -> { // @Suppress("CAST_NEVER_SUCCEEDS")
drawerLayout.open() // viewModel.fileName.observe(viewLifecycleOwner) {
true // binding.toolbar.title = it
} // }
// }
R.id.action_save -> { //
Timber.d("Save clicked") // override fun onOptionsItemSelected(item: MenuItem): Boolean {
lifecycleScope.launch { // return when (item.itemId) {
if (!viewModel.save(requireContext())) { // android.R.id.home -> {
requestFileOp(REQUEST_SAVE_FILE) // binding.drawerLayout.open()
} else { // true
Toast.makeText( // }
requireContext(), //
getString(R.string.file_saved, viewModel.fileName.value), // R.id.action_save -> {
Toast.LENGTH_SHORT // Timber.d("Save clicked")
).show() // lifecycleScope.launch {
} // if (!viewModel.save(requireContext())) {
} // requestFileOp(REQUEST_SAVE_FILE)
true // } else {
} // Toast.makeText(
// requireContext(),
R.id.action_save_as -> { // getString(R.string.file_saved, viewModel.fileName.value),
Timber.d("Save as clicked") // Toast.LENGTH_SHORT
requestFileOp(REQUEST_SAVE_FILE) // ).show()
true // }
} // }
// true
R.id.action_share -> { // }
Timber.d("Share clicked") //
val shareIntent = Intent(Intent.ACTION_SEND) // R.id.action_save_as -> {
shareIntent.putExtra(Intent.EXTRA_TEXT, viewModel.markdownUpdates.value) // Timber.d("Save as clicked")
shareIntent.type = "text/plain" // requestFileOp(REQUEST_SAVE_FILE)
startActivity( // true
Intent.createChooser( // }
shareIntent, //
getString(R.string.share_file) // R.id.action_share -> {
) // Timber.d("Share clicked")
) // val shareIntent = Intent(Intent.ACTION_SEND)
true // shareIntent.putExtra(Intent.EXTRA_TEXT, viewModel.markdownUpdates.value)
} // shareIntent.type = "text/plain"
// startActivity(
R.id.action_load -> { // Intent.createChooser(
Timber.d("Load clicked") // shareIntent,
requestFileOp(REQUEST_OPEN_FILE) // getString(R.string.share_file)
true // )
} // )
// true
R.id.action_new -> { // }
Timber.d("New clicked") //
promptSaveOrDiscardChanges() // R.id.action_load -> {
true // Timber.d("Load clicked")
} // requestFileOp(REQUEST_OPEN_FILE)
// true
R.id.action_lock_swipe -> { // }
Timber.d("Lock swiping clicked") //
item.isChecked = !item.isChecked // R.id.action_new -> {
pager!!.setSwipeLocked(item.isChecked) // Timber.d("New clicked")
true // promptSaveOrDiscardChanges()
} // true
// }
else -> super.onOptionsItemSelected(item) //
} // R.id.action_lock_swipe -> {
} // Timber.d("Lock swiping clicked")
// item.isChecked = !item.isChecked
override fun onStart() { // binding.pager.setSwipeLocked(item.isChecked)
super.onStart() // true
Plausible.pageView("") // }
lifecycleScope.launch { //
withContext(Dispatchers.IO) { // else -> super.onOptionsItemSelected(item)
val enableErrorReports = // }
PreferenceManager.getDefaultSharedPreferences(requireContext()) // }
.getBoolean(getString(R.string.pref_key_error_reports_enabled), true) //
Timber.d("MainFragment started. Error reports enabled? $enableErrorReports") // override fun onStart() {
errorHandler.enable(enableErrorReports) // super.onStart()
} // Plausible.pageView("")
} // lifecycleScope.launch {
} // withContext(Dispatchers.IO) {
// val enableErrorReports =
override fun onStop() { // PreferenceManager.getDefaultSharedPreferences(requireContext())
super.onStop() // .getBoolean(getString(R.string.pref_key_error_reports_enabled), true)
val context = context?.applicationContext ?: return // Timber.d("MainFragment started. Error reports enabled? $enableErrorReports")
lifecycleScope.launch { // errorHandler.enable(enableErrorReports)
viewModel.autosave(context, PreferenceManager.getDefaultSharedPreferences(context)) // }
} // }
} // }
//
override fun onConfigurationChanged(newConfig: Configuration) { // override fun onStop() {
super.onConfigurationChanged(newConfig) // super.onStop()
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { // val context = context?.applicationContext ?: return
Timber.d("Orientation changed to landscape, hiding tabs") // lifecycleScope.launch {
tabLayout?.visibility = View.GONE // viewModel.autosave(context, PreferenceManager.getDefaultSharedPreferences(context))
} else { // }
Timber.d("Orientation changed to portrait, showing tabs") // }
tabLayout?.visibility = View.VISIBLE //
} // override fun onConfigurationChanged(newConfig: Configuration) {
} // super.onConfigurationChanged(newConfig)
// if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
override fun onRequestPermissionsResult( // Timber.d("Orientation changed to landscape, hiding tabs")
requestCode: Int, // binding.tabLayout.visibility = View.GONE
permissions: Array<String>, // } else {
grantResults: IntArray // Timber.d("Orientation changed to portrait, showing tabs")
) { // binding.tabLayout.visibility = View.VISIBLE
when (requestCode) { // }
REQUEST_SAVE_FILE, REQUEST_OPEN_FILE -> { // }
// If request is cancelled, the result arrays are empty. //
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // override fun onRequestPermissionsResult(
// Permission granted, open file save dialog // requestCode: Int,
Timber.d("Storage permissions granted") // permissions: Array<String>,
requestFileOp(requestCode) // grantResults: IntArray
} else { // ) {
// Permission denied, do nothing // when (requestCode) {
Timber.d("Storage permissions denied, unable to save or load files") // REQUEST_SAVE_FILE, REQUEST_OPEN_FILE -> {
context?.let { // // If request is cancelled, the result arrays are empty.
Toast.makeText(it, R.string.no_permissions, Toast.LENGTH_SHORT) // if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
.show() // // Permission granted, open file save dialog
} // Timber.d("Storage permissions granted")
} // requestFileOp(requestCode)
} // } else {
} // // Permission denied, do nothing
} // Timber.d("Storage permissions denied, unable to save or load files")
// context?.let {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { // Toast.makeText(it, R.string.no_permissions, Toast.LENGTH_SHORT)
when (requestCode) { // .show()
REQUEST_OPEN_FILE -> { // }
if (resultCode != Activity.RESULT_OK || data?.data == null) { // }
Timber.w( // }
"Unable to open file. Result ok? %b Intent uri: %s", // }
resultCode == Activity.RESULT_OK, // }
data?.data?.toString() //
) // override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
return // when (requestCode) {
} // REQUEST_OPEN_FILE -> {
// if (resultCode != Activity.RESULT_OK || data?.data == null) {
lifecycleScope.launch { // Timber.w(
context?.let { // "Unable to open file. Result ok? %b Intent uri: %s",
if (!viewModel.load(it, data.data)) { // resultCode == Activity.RESULT_OK,
Toast.makeText(it, R.string.file_load_error, Toast.LENGTH_SHORT).show() // data?.data?.toString()
} // )
} // return
} // }
} //
// lifecycleScope.launch {
REQUEST_SAVE_FILE -> { // context?.let {
if (resultCode != Activity.RESULT_OK || data?.data == null) { // if (!viewModel.load(it, data.data)) {
Timber.w( // Toast.makeText(it, R.string.file_load_error, Toast.LENGTH_SHORT).show()
"Unable to save file. Result ok? %b Intent uri: %s", // }
resultCode == Activity.RESULT_OK, // }
data?.data?.toString() // }
) // }
return //
} // REQUEST_SAVE_FILE -> {
// if (resultCode != Activity.RESULT_OK || data?.data == null) {
lifecycleScope.launch { // Timber.w(
context?.let { // "Unable to save file. Result ok? %b Intent uri: %s",
viewModel.save(it, data.data) // resultCode == Activity.RESULT_OK,
} // data?.data?.toString()
} // )
} // return
} // }
super.onActivityResult(requestCode, resultCode, data) //
} // lifecycleScope.launch {
// context?.let {
private fun promptSaveOrDiscardChanges() { // viewModel.save(it, data.data)
if (!viewModel.shouldPromptSave()) { // }
viewModel.reset( // }
"Untitled.md", // }
PreferenceManager.getDefaultSharedPreferences(requireContext()) // }
) // super.onActivityResult(requestCode, resultCode, data)
return // }
} //
val context = context ?: run { // private fun promptSaveOrDiscardChanges() {
Timber.w("Context is null, unable to show prompt for save or discard") // if (!viewModel.shouldPromptSave()) {
return // viewModel.reset(
} // "Untitled.md",
AlertDialog.Builder(context) // PreferenceManager.getDefaultSharedPreferences(requireContext())
.setTitle(R.string.save_changes) // )
.setMessage(R.string.prompt_save_changes) // return
.setNegativeButton(R.string.action_discard) { _, _ -> // }
Timber.d("Discarding changes") // val context = context ?: run {
viewModel.reset( // Timber.w("Context is null, unable to show prompt for save or discard")
"Untitled.md", // return
PreferenceManager.getDefaultSharedPreferences(requireContext()) // }
) // AlertDialog.Builder(context)
} // .setTitle(R.string.save_changes)
.setPositiveButton(R.string.action_save) { _, _ -> // .setMessage(R.string.prompt_save_changes)
Timber.d("Saving changes") // .setNegativeButton(R.string.action_discard) { _, _ ->
requestFileOp(REQUEST_SAVE_FILE) // Timber.d("Discarding changes")
} // viewModel.reset(
.create() // "Untitled.md",
.show() // PreferenceManager.getDefaultSharedPreferences(requireContext())
} // )
// }
private fun requestFileOp(requestType: Int) { // .setPositiveButton(R.string.action_save) { _, _ ->
val intent = when (requestType) { // Timber.d("Saving changes")
REQUEST_SAVE_FILE -> { // requestFileOp(REQUEST_SAVE_FILE)
Timber.d("Requesting save op") // }
Intent(Intent.ACTION_CREATE_DOCUMENT).apply { // .create()
type = "text/markdown" // .show()
putExtra(Intent.EXTRA_TITLE, viewModel.fileName.value) // }
} //
} // private fun requestFileOp(requestType: Int) {
// val intent = when (requestType) {
REQUEST_OPEN_FILE -> { // REQUEST_SAVE_FILE -> {
Timber.d("Requesting open op") // Timber.d("Requesting save op")
Intent(Intent.ACTION_OPEN_DOCUMENT).apply { // Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = "*/*" // type = "text/markdown"
if (MimeTypeMap.getSingleton().hasMimeType("md")) { // putExtra(Intent.EXTRA_TITLE, viewModel.fileName.value)
// If the device doesn't recognize markdown files then we're not going to be // }
// able to open them at all, so there's no sense in filtering them out. // }
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/plain", "text/markdown")) //
} // REQUEST_OPEN_FILE -> {
} // Timber.d("Requesting open op")
} // Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
// type = "*/*"
else -> { // if (MimeTypeMap.getSingleton().hasMimeType("md")) {
Timber.w("Ignoring unknown file op request: $requestType") // // If the device doesn't recognize markdown files then we're not going to be
null // // able to open them at all, so there's no sense in filtering them out.
} // putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/plain", "text/markdown"))
} ?: return // }
intent.addCategory(Intent.CATEGORY_OPENABLE) // }
startActivityForResult( // }
intent, //
requestType // else -> {
) // Timber.w("Ignoring unknown file op request: $requestType")
} // null
// }
companion object { // } ?: return
// Request codes // intent.addCategory(Intent.CATEGORY_OPENABLE)
const val REQUEST_OPEN_FILE = 1 // startActivityForResult(
const val REQUEST_SAVE_FILE = 2 // intent,
const val KEY_AUTOSAVE = "autosave" // requestType
} // )
} // }
//
// companion object {
// // Request codes
// const val REQUEST_OPEN_FILE = 1
// const val REQUEST_SAVE_FILE = 2
// }
//}

View file

@ -1,89 +1,99 @@
package com.wbrawner.simplemarkdown.view.fragment //package com.wbrawner.simplemarkdown.view.fragment
//
import android.content.res.Configuration //import android.content.res.Configuration
import android.os.Bundle //import android.os.Bundle
import android.view.LayoutInflater //import android.view.LayoutInflater
import android.view.MenuItem //import android.view.MenuItem
import android.view.View //import android.view.View
import android.view.ViewGroup //import android.view.ViewGroup
import android.widget.Toast //import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate //import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.Fragment //import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope //import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController //import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController //import androidx.navigation.ui.setupWithNavController
import com.wbrawner.plausible.android.Plausible //import com.wbrawner.plausible.android.Plausible
import com.wbrawner.simplemarkdown.R //import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.utility.* //import com.wbrawner.simplemarkdown.databinding.FragmentMarkdownInfoBinding
import kotlinx.android.synthetic.main.fragment_markdown_info.* //import com.wbrawner.simplemarkdown.utility.*
import kotlinx.coroutines.launch //import kotlinx.coroutines.launch
//
class MarkdownInfoFragment : Fragment() { //class MarkdownInfoFragment : Fragment() {
private val errorHandler: ErrorHandler by errorHandlerImpl() // private val errorHandler: ErrorHandler by errorHandlerImpl()
// private var _binding: FragmentMarkdownInfoBinding? = null
override fun onCreate(savedInstanceState: Bundle?) { // private val binding: FragmentMarkdownInfoBinding
super.onCreate(savedInstanceState) // get() = _binding!!
setHasOptionsMenu(true) //
} // override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = // setHasOptionsMenu(true)
inflater.inflate(R.layout.fragment_markdown_info, container, false) // }
//
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val fileName = arguments?.getString(EXTRA_FILE) // _binding = FragmentMarkdownInfoBinding.inflate(inflater, container, false)
if (fileName.isNullOrBlank()) { // return binding.root
findNavController().navigateUp() // }
return //
} // override fun onDestroyView() {
toolbar.setupWithNavController(findNavController()) // super.onDestroyView()
// _binding = null
val isNightMode = AppCompatDelegate.getDefaultNightMode() == // }
AppCompatDelegate.MODE_NIGHT_YES //
|| resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES // override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val defaultCssId = if (isNightMode) { // val fileName = arguments?.getString(EXTRA_FILE)
R.string.pref_custom_css_default_dark // if (fileName.isNullOrBlank()) {
} else { // findNavController().navigateUp()
R.string.pref_custom_css_default // return
} // }
val css: String? = getString(defaultCssId) // binding.toolbar.setupWithNavController(findNavController())
lifecycleScope.launch { //
try { // val isNightMode = AppCompatDelegate.getDefaultNightMode() ==
val html = view.context.assets?.readAssetToString(fileName) // AppCompatDelegate.MODE_NIGHT_YES
?.toHtml() // || resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
?: throw RuntimeException("Unable to open stream to $fileName") // val defaultCssId = if (isNightMode) {
infoWebview.loadDataWithBaseURL(null, // R.string.pref_custom_css_default_dark
String.format(FORMAT_CSS, css) + html, // } else {
"text/html", // R.string.pref_custom_css_default
"UTF-8", null // }
) // val css: String? = getString(defaultCssId)
} catch (e: Exception) { // lifecycleScope.launch {
errorHandler.reportException(e) // try {
Toast.makeText(view.context, R.string.file_load_error, Toast.LENGTH_SHORT).show() // val html = view.context.assets?.readAssetToString(fileName)
findNavController().navigateUp() // ?.toHtml()
} // ?: throw RuntimeException("Unable to open stream to $fileName")
} // binding.infoWebview.loadDataWithBaseURL(null,
} // String.format(FORMAT_CSS, css) + html,
// "text/html",
override fun onOptionsItemSelected(item: MenuItem): Boolean { // "UTF-8", null
if (item.itemId == android.R.id.home) { // )
findNavController().navigateUp() // } catch (e: Exception) {
return true // errorHandler.reportException(e)
} // Toast.makeText(view.context, R.string.file_load_error, Toast.LENGTH_SHORT).show()
return super.onOptionsItemSelected(item) // findNavController().navigateUp()
} // }
// }
override fun onStart() { // }
super.onStart() //
arguments?.getString(EXTRA_FILE)?.let { // override fun onOptionsItemSelected(item: MenuItem): Boolean {
Plausible.pageView(it) // if (item.itemId == android.R.id.home) {
} // findNavController().navigateUp()
} // return true
// }
companion object { // return super.onOptionsItemSelected(item)
const val FORMAT_CSS = "<style>" + // }
"%s" + //
"</style>" // override fun onStart() {
const val EXTRA_TITLE = "title" // super.onStart()
const val EXTRA_FILE = "file" // arguments?.getString(EXTRA_FILE)?.let {
} // Plausible.pageView(it)
} // }
// }
//
// companion object {
// const val FORMAT_CSS = "<style>" +
// "%s" +
// "</style>"
// const val EXTRA_TITLE = "title"
// const val EXTRA_FILE = "file"
// }
//}

View file

@ -62,9 +62,9 @@ class PreviewFragment : Fragment() {
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
updateWebContent(viewModel.markdownUpdates.value ?: "") updateWebContent(viewModel.markdownUpdates.value ?: "")
viewModel.markdownUpdates.observe(this, { // viewModel.markdownUpdates.observe(this, {
updateWebContent(it) // updateWebContent(it)
}) // })
} }
private fun updateWebContent(markdown: String) { private fun updateWebContent(markdown: String) {

View file

@ -1,34 +1,45 @@
package com.wbrawner.simplemarkdown.view.fragment //package com.wbrawner.simplemarkdown.view.fragment
//
import android.os.Bundle //import android.os.Bundle
import android.view.LayoutInflater //import android.view.LayoutInflater
import android.view.MenuItem //import android.view.MenuItem
import android.view.View //import android.view.View
import android.view.ViewGroup //import android.view.ViewGroup
import androidx.fragment.app.Fragment //import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController //import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController //import androidx.navigation.ui.setupWithNavController
import com.wbrawner.plausible.android.Plausible //import com.wbrawner.plausible.android.Plausible
import com.wbrawner.simplemarkdown.R //import com.wbrawner.simplemarkdown.R
import kotlinx.android.synthetic.main.fragment_settings.* //import com.wbrawner.simplemarkdown.databinding.FragmentSettingsBinding
//
class SettingsContainerFragment : Fragment() { //class SettingsContainerFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) { // private var _binding: FragmentSettingsBinding? = null
super.onCreate(savedInstanceState) // private val binding: FragmentSettingsBinding
setHasOptionsMenu(true) // get() = _binding!!
} //
// override fun onCreate(savedInstanceState: Bundle?) {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = // super.onCreate(savedInstanceState)
inflater.inflate(R.layout.fragment_settings, container, false) // setHasOptionsMenu(true)
// }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { //
toolbar.setupWithNavController(findNavController()) // override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
} // _binding = FragmentSettingsBinding.inflate(inflater, container, false)
// return binding.root
override fun onOptionsItemSelected(item: MenuItem): Boolean = findNavController().navigateUp() // }
//
override fun onStart() { // override fun onDestroyView() {
super.onStart() // super.onDestroyView()
Plausible.pageView("Settings") // _binding = null
} // }
} //
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// binding.toolbar.setupWithNavController(findNavController())
// }
//
// override fun onOptionsItemSelected(item: MenuItem): Boolean = findNavController().navigateUp()
//
// override fun onStart() {
// super.onStart()
// Plausible.pageView("Settings")
// }
//}

View file

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

View file

@ -6,10 +6,13 @@ import android.net.Uri
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.wbrawner.simplemarkdown.utility.getName import com.wbrawner.simplemarkdown.utility.getName
import com.wbrawner.simplemarkdown.view.fragment.MainFragment import com.wbrawner.simplemarkdown.view.activity.KEY_AUTOSAVE
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
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
@ -22,15 +25,19 @@ import java.util.concurrent.atomic.AtomicBoolean
const val PREF_KEY_AUTOSAVE_URI = "autosave.uri" const val PREF_KEY_AUTOSAVE_URI = "autosave.uri"
class MarkdownViewModel(val timber: Timber.Tree = Timber.asTree()) : ViewModel() { class MarkdownViewModel(val timber: Timber.Tree = Timber.asTree()) : ViewModel() {
val fileName = MutableLiveData<String?>("Untitled.md") val fileName = MutableStateFlow("Untitled.md")
val markdownUpdates = MutableLiveData<String>() val markdownUpdates = MutableStateFlow("")
val editorActions = MutableLiveData<EditorAction>() val editorActions = MutableLiveData<EditorAction>()
val uri = MutableLiveData<Uri?>() val uri = MutableLiveData<Uri?>()
private val isDirty = AtomicBoolean(false) private val isDirty = AtomicBoolean(false)
private val saveMutex = Mutex() private val saveMutex = Mutex()
fun updateMarkdown(markdown: String?) { init {
this.markdownUpdates.postValue(markdown ?: "") markdownUpdates
}
fun updateMarkdown(markdown: String?) = viewModelScope.launch {
markdownUpdates.emit(markdown ?: "")
isDirty.set(true) isDirty.set(true)
} }
@ -59,8 +66,8 @@ class MarkdownViewModel(val timber: Timber.Tree = Timber.asTree()) : ViewModel()
return@withContext false return@withContext false
} }
editorActions.postValue(EditorAction.Load(content)) editorActions.postValue(EditorAction.Load(content))
markdownUpdates.postValue(content) markdownUpdates.emit(content)
this@MarkdownViewModel.fileName.postValue(fileName) this@MarkdownViewModel.fileName.emit(fileName)
this@MarkdownViewModel.uri.postValue(uri) this@MarkdownViewModel.uri.postValue(uri)
timber.i("Loaded file $fileName from $fileInput") timber.i("Loaded file $fileName from $fileInput")
timber.v("File contents:\n$content") timber.v("File contents:\n$content")
@ -108,7 +115,7 @@ class MarkdownViewModel(val timber: Timber.Tree = Timber.asTree()) : ViewModel()
timber.w("Open output stream returned null for uri: $uri") timber.w("Open output stream returned null for uri: $uri")
return@withContext false return@withContext false
} }
this@MarkdownViewModel.fileName.postValue(fileName) this@MarkdownViewModel.fileName.emit(fileName)
this@MarkdownViewModel.uri.postValue(uri) this@MarkdownViewModel.uri.postValue(uri)
isDirty.set(false) isDirty.set(false)
timber.i("Saved file $fileName to uri $uri") timber.i("Saved file $fileName to uri $uri")
@ -129,7 +136,7 @@ class MarkdownViewModel(val timber: Timber.Tree = Timber.asTree()) : ViewModel()
timber.i("Ignoring autosave since manual save is already in progress") timber.i("Ignoring autosave since manual save is already in progress")
return return
} }
val isAutoSaveEnabled = sharedPrefs.getBoolean(MainFragment.KEY_AUTOSAVE, true) val isAutoSaveEnabled = sharedPrefs.getBoolean(KEY_AUTOSAVE, true)
timber.d("Autosave called. isEnabled? $isAutoSaveEnabled") timber.d("Autosave called. isEnabled? $isAutoSaveEnabled")
if (!isDirty.get() || !isAutoSaveEnabled) { if (!isDirty.get() || !isAutoSaveEnabled) {
timber.i("Ignoring call to autosave. Contents haven't changed or autosave not enabled") timber.i("Ignoring call to autosave. Contents haven't changed or autosave not enabled")
@ -148,11 +155,11 @@ class MarkdownViewModel(val timber: Timber.Tree = Timber.asTree()) : ViewModel()
} }
} }
fun reset(untitledFileName: String, sharedPrefs: SharedPreferences) { fun reset(untitledFileName: String, sharedPrefs: SharedPreferences) = viewModelScope.launch{
timber.i("Resetting view model to default state") timber.i("Resetting view model to default state")
fileName.postValue(untitledFileName) fileName.tryEmit(untitledFileName)
uri.postValue(null) uri.postValue(null)
markdownUpdates.postValue("") markdownUpdates.emit("")
editorActions.postValue(EditorAction.Load("")) editorActions.postValue(EditorAction.Load(""))
isDirty.set(false) isDirty.set(false)
timber.i("Removing autosave uri from shared prefs") timber.i("Removing autosave uri from shared prefs")

View file

@ -1,11 +1,16 @@
<resources> <resources>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <style name="Theme.App" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
<item name="android:statusBarColor">@color/colorBackground</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@color/colorBackground</item> <item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/colorBackground</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_fg</item>
<item name="postSplashScreenTheme">@style/Theme.App</item>
</style> </style>
</resources> </resources>

View file

@ -42,7 +42,7 @@
<string name="pref_title_readability">Enable readability highlighting (experimental)</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_off">Readability highlighting is off</string>
<string name="pref_readability_on">Readability highlighting is on</string> <string name="pref_readability_on">Readability highlighting is on</string>
<string name="pref_autosave_on">Files will automatically save</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_autosave_off">Files will not be automatically saved</string>
<string name="pref_custom_css">pref.custom_css</string> <string name="pref_custom_css">pref.custom_css</string>
<string name="pref_title_custom_css">Custom CSS</string> <string name="pref_title_custom_css">Custom CSS</string>

View file

@ -1,20 +1,19 @@
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <style name="Theme.App" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
<item name="android:statusBarColor">@color/colorBackground</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item> <item name="android:windowLightStatusBar">true</item>
<item name="android:navigationBarColor">#FF000000</item> <item name="android:navigationBarColor">@android:color/transparent</item>
</style> </style>
<style name="AppTheme.Splash" parent="AppTheme"> <style name="Theme.App.Starting" parent="Theme.SplashScreen">
<item name="android:windowLightStatusBar">false</item> <item name="windowSplashScreenBackground">@color/colorPrimary</item>
<item name="android:statusBarColor">@color/colorPrimary</item> <item name="windowSplashScreenAnimatedIcon">@drawable/splash_fg</item>
<item name="android:navigationBarColor">@color/colorPrimary</item> <item name="postSplashScreenTheme">@style/Theme.App</item>
<item name="android:windowBackground">@drawable/splash_bg</item>
</style> </style>
</resources> </resources>