diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6723798..48611fa 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4f36fe5..9245ef4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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"> - @@ -40,7 +38,6 @@ - + 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 ?: {}) +} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/ui/MarkdownInfoScreen.kt b/app/src/main/java/com/wbrawner/simplemarkdown/ui/MarkdownInfoScreen.kt new file mode 100644 index 0000000..bf63a78 --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/ui/MarkdownInfoScreen.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/ui/MarkdownPreview.kt b/app/src/main/java/com/wbrawner/simplemarkdown/ui/MarkdownPreview.kt new file mode 100644 index 0000000..23aa4ba --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/ui/MarkdownPreview.kt @@ -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 = "" + } + 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) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/ui/SettingsScreen.kt b/app/src/main/java/com/wbrawner/simplemarkdown/ui/SettingsScreen.kt new file mode 100644 index 0000000..3aef1ed --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/ui/SettingsScreen.kt @@ -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, + 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, 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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/ui/SupportScreen.kt b/app/src/main/java/com/wbrawner/simplemarkdown/ui/SupportScreen.kt new file mode 100644 index 0000000..c6a85c4 --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/ui/SupportScreen.kt @@ -0,0 +1,2 @@ +package com.wbrawner.simplemarkdown.ui + diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/ui/theme/Color.kt b/app/src/main/java/com/wbrawner/simplemarkdown/ui/theme/Color.kt new file mode 100644 index 0000000..75217c2 --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/ui/theme/Color.kt @@ -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) diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/ui/theme/Theme.kt b/app/src/main/java/com/wbrawner/simplemarkdown/ui/theme/Theme.kt new file mode 100644 index 0000000..36cd939 --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/ui/theme/Theme.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/MainActivity.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/MainActivity.kt index 44f5cfd..6c1bf17 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/MainActivity.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/MainActivity.kt @@ -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() 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 -) \ No newline at end of file +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 + ) \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SplashActivity.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SplashActivity.kt deleted file mode 100644 index 833b864..0000000 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SplashActivity.kt +++ /dev/null @@ -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() - } - } -} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainFragment.kt index f72ac82..b44b6d9 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainFragment.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainFragment.kt @@ -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, - 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, +// 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 +// } +//} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MarkdownInfoFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MarkdownInfoFragment.kt index 52a8041..29a82ae 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MarkdownInfoFragment.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MarkdownInfoFragment.kt @@ -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 = "" - 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 = "" +// const val EXTRA_TITLE = "title" +// const val EXTRA_FILE = "file" +// } +//} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/PreviewFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/PreviewFragment.kt index a031deb..804c8ee 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/PreviewFragment.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/PreviewFragment.kt @@ -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) { diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SettingsContainerFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SettingsContainerFragment.kt index d9188d7..e61c6b7 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SettingsContainerFragment.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SettingsContainerFragment.kt @@ -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") +// } +//} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SupportFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SupportFragment.kt index 1bef766..6910da7 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SupportFragment.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/SupportFragment.kt @@ -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 // } -} \ No newline at end of file +// +// 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") +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/viewmodel/MarkdownViewModel.kt b/app/src/main/java/com/wbrawner/simplemarkdown/viewmodel/MarkdownViewModel.kt index cd1aeb2..d582ac3 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/viewmodel/MarkdownViewModel.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/viewmodel/MarkdownViewModel.kt @@ -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("Untitled.md") - val markdownUpdates = MutableLiveData() + val fileName = MutableStateFlow("Untitled.md") + val markdownUpdates = MutableStateFlow("") val editorActions = MutableLiveData() val uri = MutableLiveData() 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") diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 2d93108..0987e0f 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -1,11 +1,16 @@ - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a9f3dcf..af86184 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,7 +42,7 @@ Enable readability highlighting (experimental) Readability highlighting is off Readability highlighting is on - Files will automatically save + Files will be automatically saved Files will not be automatically saved pref.custom_css Custom CSS diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 1f0dad3..a73e4e8 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,20 +1,19 @@ - -