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 {
id("com.android.application")
id("kotlin-android-extensions")
id("kotlin-android")
id("kotlin-kapt")
id("com.osacky.fladle")
@ -36,7 +35,7 @@ android {
)
}
}
compileSdk = 33
compileSdk = 34
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
@ -47,7 +46,7 @@ android {
defaultConfig {
applicationId = "com.wbrawner.simplemarkdown"
minSdk = 23
targetSdk = 33
targetSdk = 34
versionCode = 41
versionName = "0.8.16"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@ -89,6 +88,12 @@ android {
execution = "ANDROIDX_TEST_ORCHESTRATOR"
}
namespace = "com.wbrawner.simplemarkdown"
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
playConfigs {
register("play") {
enabled.set(true)
@ -105,9 +110,10 @@ play {
}
dependencies {
val navigationVersion = "2.5.3"
val navigationVersion = "2.7.2"
implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion")
implementation("androidx.navigation:navigation-ui-ktx:$navigationVersion")
implementation("androidx.navigation:navigation-compose:$navigationVersion")
testImplementation("junit:junit:4.13.2")
testImplementation("org.robolectric:robolectric:4.2.1")
val espressoVersion = "3.5.1"
@ -119,18 +125,32 @@ dependencies {
androidTestUtil("androidx.test:orchestrator:1.4.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
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("androidx.appcompat:appcompat:1.6.0")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.fragment:fragment-ktx:1.5.5")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.fragment:fragment-ktx:1.6.1")
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("com.commonsware.cwac:anddown:0.4.0")
implementation("com.jakewharton.timber:timber:5.0.1")
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.browser:browser:1.4.0")
val coroutinesVersion = "1.6.4"
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.browser:browser:1.6.0")
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")
val lifecycleVersion = "2.2.0"
implementation("androidx.lifecycle:lifecycle-extensions:$lifecycleVersion")

View file

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

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

View file

@ -1,34 +1,45 @@
package com.wbrawner.simplemarkdown.view.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import com.wbrawner.plausible.android.Plausible
import com.wbrawner.simplemarkdown.R
import kotlinx.android.synthetic.main.fragment_settings.*
class SettingsContainerFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_settings, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
toolbar.setupWithNavController(findNavController())
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = findNavController().navigateUp()
override fun onStart() {
super.onStart()
Plausible.pageView("Settings")
}
}
//package com.wbrawner.simplemarkdown.view.fragment
//
//import android.os.Bundle
//import android.view.LayoutInflater
//import android.view.MenuItem
//import android.view.View
//import android.view.ViewGroup
//import androidx.fragment.app.Fragment
//import androidx.navigation.fragment.findNavController
//import androidx.navigation.ui.setupWithNavController
//import com.wbrawner.plausible.android.Plausible
//import com.wbrawner.simplemarkdown.R
//import com.wbrawner.simplemarkdown.databinding.FragmentSettingsBinding
//
//class SettingsContainerFragment : Fragment() {
// private var _binding: FragmentSettingsBinding? = null
// private val binding: FragmentSettingsBinding
// get() = _binding!!
//
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// setHasOptionsMenu(true)
// }
//
// override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
// _binding = FragmentSettingsBinding.inflate(inflater, container, false)
// return binding.root
// }
//
// override fun onDestroyView() {
// super.onDestroyView()
// _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
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import com.wbrawner.plausible.android.Plausible
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.utility.SupportLinkProvider
import kotlinx.android.synthetic.main.fragment_support.*
class SupportFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_support, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
toolbar.setupWithNavController(findNavController())
githubButton.setOnClickListener {
CustomTabsIntent.Builder()
.addDefaultShareMenuItem()
.build()
.launchUrl(view.context, Uri.parse("https://github" +
".com/wbrawner/SimpleMarkdown"))
}
rateButton.setOnClickListener {
val playStoreIntent = Intent(Intent.ACTION_VIEW)
.apply {
data = Uri.parse("market://details?id=${view.context.packageName}")
addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or
Intent.FLAG_ACTIVITY_NEW_DOCUMENT or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
try {
startActivity(playStoreIntent)
} catch (ignored: ActivityNotFoundException) {
playStoreIntent.data = Uri.parse("https://play.google.com/store/apps/details?id=${view.context.packageName}")
startActivity(playStoreIntent)
}
}
SupportLinkProvider(requireActivity()).supportLinks.observe(viewLifecycleOwner, Observer { links ->
links.forEach {
supportButtons.addView(it)
}
})
}
override fun 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)
//package com.wbrawner.simplemarkdown.view.fragment
//
//import android.content.ActivityNotFoundException
//import android.content.Intent
//import android.net.Uri
//import android.os.Bundle
//import android.view.LayoutInflater
//import android.view.View
//import android.view.ViewGroup
//import androidx.browser.customtabs.CustomTabsIntent
//import androidx.fragment.app.Fragment
//import androidx.lifecycle.Observer
//import androidx.navigation.fragment.findNavController
//import androidx.navigation.ui.setupWithNavController
//import com.wbrawner.plausible.android.Plausible
//import com.wbrawner.simplemarkdown.databinding.FragmentSupportBinding
//import com.wbrawner.simplemarkdown.utility.SupportLinkProvider
//
//class SupportFragment : Fragment() {
// private var _binding: FragmentSupportBinding? = null
// private val binding: FragmentSupportBinding
// get() = _binding!!
//
// override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
// _binding = FragmentSupportBinding.inflate(inflater, container, false)
// return binding.root
// }
}
//
// 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.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
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.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
@ -22,15 +25,19 @@ import java.util.concurrent.atomic.AtomicBoolean
const val PREF_KEY_AUTOSAVE_URI = "autosave.uri"
class MarkdownViewModel(val timber: Timber.Tree = Timber.asTree()) : ViewModel() {
val fileName = MutableLiveData<String?>("Untitled.md")
val markdownUpdates = MutableLiveData<String>()
val fileName = MutableStateFlow("Untitled.md")
val markdownUpdates = MutableStateFlow("")
val editorActions = MutableLiveData<EditorAction>()
val uri = MutableLiveData<Uri?>()
private val isDirty = AtomicBoolean(false)
private val saveMutex = Mutex()
fun updateMarkdown(markdown: String?) {
this.markdownUpdates.postValue(markdown ?: "")
init {
markdownUpdates
}
fun updateMarkdown(markdown: String?) = viewModelScope.launch {
markdownUpdates.emit(markdown ?: "")
isDirty.set(true)
}
@ -59,8 +66,8 @@ class MarkdownViewModel(val timber: Timber.Tree = Timber.asTree()) : ViewModel()
return@withContext false
}
editorActions.postValue(EditorAction.Load(content))
markdownUpdates.postValue(content)
this@MarkdownViewModel.fileName.postValue(fileName)
markdownUpdates.emit(content)
this@MarkdownViewModel.fileName.emit(fileName)
this@MarkdownViewModel.uri.postValue(uri)
timber.i("Loaded file $fileName from $fileInput")
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")
return@withContext false
}
this@MarkdownViewModel.fileName.postValue(fileName)
this@MarkdownViewModel.fileName.emit(fileName)
this@MarkdownViewModel.uri.postValue(uri)
isDirty.set(false)
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")
return
}
val isAutoSaveEnabled = sharedPrefs.getBoolean(MainFragment.KEY_AUTOSAVE, true)
val isAutoSaveEnabled = sharedPrefs.getBoolean(KEY_AUTOSAVE, true)
timber.d("Autosave called. isEnabled? $isAutoSaveEnabled")
if (!isDirty.get() || !isAutoSaveEnabled) {
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")
fileName.postValue(untitledFileName)
fileName.tryEmit(untitledFileName)
uri.postValue(null)
markdownUpdates.postValue("")
markdownUpdates.emit("")
editorActions.postValue(EditorAction.Load(""))
isDirty.set(false)
timber.i("Removing autosave uri from shared prefs")

View file

@ -1,11 +1,16 @@
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<style name="Theme.App" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:statusBarColor">@color/colorBackground</item>
<item name="android:navigationBarColor">@color/colorBackground</item>
<item name="android:statusBarColor">@android:color/transparent</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>
</resources>

View file

@ -42,7 +42,7 @@
<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 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_custom_css">pref.custom_css</string>
<string name="pref_title_custom_css">Custom CSS</string>

View file

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