Migrate some more to Jetpack Compose + Material3

This commit is contained in:
William Brawner 2023-09-11 21:23:10 -06:00
parent 643345493e
commit c7e54f21d9
26 changed files with 223 additions and 1005 deletions

View file

@ -166,9 +166,9 @@ android.productFlavors.forEach { flavor ->
apply(plugin = "com.google.firebase.crashlytics")
dependencies {
implementation("com.android.billingclient:billing:5.1.0")
implementation("com.android.billingclient:billing:6.0.1")
implementation("com.google.android.play:core-ktx:1.8.1")
implementation("com.google.firebase:firebase-crashlytics:18.3.5")
implementation("com.google.firebase:firebase-crashlytics:18.4.1")
}
}
}

View file

@ -1,9 +1,6 @@
package com.wbrawner.simplemarkdown.utility
import android.app.Activity
import androidx.lifecycle.MutableLiveData
import com.google.android.material.button.MaterialButton
import androidx.compose.runtime.Composable
class SupportLinkProvider(@Suppress("unused") private val activity: Activity) {
val supportLinks = MutableLiveData<List<MaterialButton>>()
}
@Composable
fun SupportLinks() {}

View file

@ -11,6 +11,7 @@ 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.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Menu
@ -26,6 +27,7 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
@ -42,9 +44,13 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity
import androidx.navigation.NavController
@ -101,7 +107,9 @@ fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
DropdownMenuItem(text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Lock Swiping")
Checkbox(checked = lockSwiping, onCheckedChange = { lockSwiping = !lockSwiping })
Checkbox(
checked = lockSwiping,
onCheckedChange = { lockSwiping = !lockSwiping })
}
}, onClick = {
lockSwiping = !lockSwiping
@ -138,7 +146,9 @@ fun MainScreen(navController: NavController, viewModel: MarkdownViewModel) {
.padding(8.dp),
value = markdown,
onValueChange = { viewModel.updateMarkdown(it) },
textStyle = TextStyle.Default.copy(fontFamily = FontFamily.Monospace)
textStyle = TextStyle.Default.copy(fontFamily = FontFamily.Monospace, color = MaterialTheme.colorScheme.onSurface),
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface)
)
} else {
MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown)
@ -154,18 +164,26 @@ fun MarkdownNavigationDrawer(
navigate: (Route) -> Unit, content: @Composable (drawerState: DrawerState) -> Unit
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope()
DismissibleNavigationDrawer(drawerState = drawerState, drawerContent = {
DismissibleDrawerSheet {
Route.entries.forEach { route ->
if (route == Route.EDITOR) {
return@forEach
}
NavigationDrawerItem(icon = {
NavigationDrawerItem(
icon = {
Icon(imageVector = route.icon, contentDescription = null)
},
label = { Text(route.title) },
selected = false,
onClick = { navigate(route) })
onClick = {
navigate(route)
coroutineScope.launch {
drawerState.close()
}
}
)
}
}
}) {

View file

@ -14,7 +14,6 @@ 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(

View file

@ -1,2 +1,104 @@
package com.wbrawner.simplemarkdown.ui
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_ON
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
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.utility.SupportLinks
@Composable
fun SupportScreen(navController: NavController) {
Scaffold(topBar = {
MarkdownTopAppBar(title = "Support SimpleMarkdown", navController = navController)
}) { paddingValues ->
val context = LocalContext.current
Column(
modifier = Modifier
.padding(paddingValues)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
modifier = Modifier.size(100.dp),
imageVector = Icons.Default.Favorite,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Text(context.getString(R.string.support_info), textAlign = TextAlign.Center)
Spacer(modifier = Modifier.height(8.dp))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
CustomTabsIntent.Builder()
.setShareState(SHARE_STATE_ON)
.build()
.launchUrl(context, Uri.parse("https://github.com/wbrawner/SimpleMarkdown"))
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(context.getColor(R.color.colorBackgroundGitHub)),
contentColor = Color.White
)
) {
Text(context.getString(R.string.action_view_github))
}
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
val playStoreIntent = Intent(Intent.ACTION_VIEW)
.apply {
data = Uri.parse("market://details?id=${context.packageName}")
addFlags(
Intent.FLAG_ACTIVITY_NO_HISTORY or
Intent.FLAG_ACTIVITY_NEW_DOCUMENT or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
)
}
try {
startActivity(context, playStoreIntent, null)
} catch (ignored: ActivityNotFoundException) {
playStoreIntent.data =
Uri.parse("https://play.google.com/store/apps/details?id=${context.packageName}")
startActivity(context, playStoreIntent, null)
}
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(context.getColor(R.color.colorBackgroundPlayStore)),
contentColor = Color.White
)
) {
Text(context.getString(R.string.action_rate))
}
SupportLinks()
}
}
}

View file

@ -1,28 +0,0 @@
package com.wbrawner.simplemarkdown.view
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager
class DisableableViewPager : ViewPager {
private var isSwipeLocked = false
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return !isSwipeLocked && super.onInterceptTouchEvent(ev)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent): Boolean {
return !isSwipeLocked && super.onTouchEvent(ev)
}
fun setSwipeLocked(locked: Boolean) {
this.isSwipeLocked = locked
}
}

View file

@ -1,6 +0,0 @@
package com.wbrawner.simplemarkdown.view
interface ViewPagerPage {
fun onSelected()
fun onDeselected()
}

View file

@ -38,6 +38,7 @@ 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.SupportScreen
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel
import kotlinx.coroutines.Dispatchers
@ -126,7 +127,7 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
SettingsScreen(navController = navController)
}
composable(Route.SUPPORT.path) {
Text("To do")
SupportScreen(navController = navController)
}
composable(Route.HELP.path) {
MarkdownInfoScreen(title = Route.HELP.title, file = "Cheatsheet.md", navController = navController)
@ -141,12 +142,6 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
}
}
}
override fun onBackPressed() {
if (!findNavController(R.id.content).navigateUp()) {
super.onBackPressed()
}
}
}
enum class Route(

View file

@ -1,70 +0,0 @@
package com.wbrawner.simplemarkdown.view.adapter
import android.content.Context
import android.content.res.Configuration
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.view.fragment.EditFragment
import com.wbrawner.simplemarkdown.view.fragment.PreviewFragment
class EditPagerAdapter(fm: FragmentManager, private val context: Context)
: FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT), ViewPager.OnPageChangeListener {
private val editFragment = EditFragment()
private val previewFragment = PreviewFragment()
override fun getItem(position: Int): Fragment {
return when (position) {
FRAGMENT_EDIT -> editFragment
FRAGMENT_PREVIEW -> previewFragment
else -> throw IllegalStateException("Attempting to get fragment for invalid page number")
}
}
override fun getCount(): Int {
return NUM_PAGES
}
override fun getPageTitle(position: Int): CharSequence? {
var stringId = 0
when (position) {
FRAGMENT_EDIT -> stringId = R.string.action_edit
FRAGMENT_PREVIEW -> stringId = R.string.action_preview
}
return context.getString(stringId)
}
override fun getPageWidth(position: Int): Float {
return if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
0.5f
} else {
super.getPageWidth(position)
}
}
override fun onPageScrollStateChanged(state: Int) {
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
}
override fun onPageSelected(position: Int) {
when (position) {
FRAGMENT_EDIT -> {
editFragment.onSelected()
}
FRAGMENT_PREVIEW -> {
editFragment.onDeselected()
}
}
}
companion object {
const val FRAGMENT_EDIT = 0
const val FRAGMENT_PREVIEW = 1
const val NUM_PAGES = 2
}
}

View file

@ -1,99 +0,0 @@
//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

@ -1,45 +0,0 @@
//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,82 +0,0 @@
package com.wbrawner.simplemarkdown.view.fragment
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.lifecycleScope
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.wbrawner.simplemarkdown.BuildConfig
import com.wbrawner.simplemarkdown.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SettingsFragment
: PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_general)
if (BuildConfig.DEBUG) {
preferenceScreen.addPreference(Preference(preferenceScreen.context).apply {
title = "Force crash"
onPreferenceClickListener = Preference.OnPreferenceClickListener {
throw RuntimeException("Forced crash from settings")
}
})
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
lifecycleScope.launch(context = Dispatchers.IO) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
sharedPreferences.registerOnSharedPreferenceChangeListener(this@SettingsFragment)
(findPreference(getString(R.string.pref_key_dark_mode)) as? ListPreference)?.let {
setListPreferenceSummary(sharedPreferences, it)
}
@Suppress("ConstantConditionIf")
if (!BuildConfig.ENABLE_CUSTOM_CSS) {
findPreference<Preference>(getString(R.string.pref_custom_css))?.let {
preferenceScreen.removePreference(it)
}
}
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (!isAdded || sharedPreferences == null || key == null) return
val preference = findPreference(key) as? ListPreference ?: return
setListPreferenceSummary(sharedPreferences, preference)
if (preference.key != getString(R.string.pref_key_dark_mode)) {
return
}
var darkMode: Int = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
} else {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
val darkModeValue = sharedPreferences.getString(preference.key, null)
if (!darkModeValue.isNullOrEmpty()) {
if (darkModeValue.equals(getString(R.string.pref_value_light), ignoreCase = true)) {
darkMode = AppCompatDelegate.MODE_NIGHT_NO
} else if (darkModeValue.equals(getString(R.string.pref_value_dark), ignoreCase = true)) {
darkMode = AppCompatDelegate.MODE_NIGHT_YES
}
}
AppCompatDelegate.setDefaultNightMode(darkMode)
activity?.recreate()
}
private fun setListPreferenceSummary(sharedPreferences: SharedPreferences, preference: ListPreference) {
val storedValue = sharedPreferences.getString(
preference.key,
null
) ?: return
val index = preference.findIndexOfValue(storedValue)
if (index < 0) return
preference.summary = preference.entries[index].toString()
}
}

View file

@ -1,69 +0,0 @@
//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

@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/drawerLayout">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.wbrawner.simplemarkdown.view.DisableableViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorBackground"
app:layout_scrollFlags="scroll|enterAlways" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorBackground"
android:visibility="gone">
<com.google.android.material.tabs.TabItem
android:id="@+id/editTab"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/action_edit" />
<com.google.android.material.tabs.TabItem
android:id="@+id/previewTab"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/action_preview" />
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigationView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:menu="@menu/menu_main" />
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/content"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />

View file

@ -1,23 +0,0 @@
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/markdown_edit_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.wbrawner.simplemarkdown.view.fragment.EditFragment">
<EditText
android:id="@+id/markdown_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:gravity="top"
android:fontFamily="monospace"
android:hint="@string/markdown_here"
android:imeOptions="flagNoExtractUi"
android:inputType="textMultiLine|textCapSentences"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="16dp"
android:scrollHorizontally="false"
android:importantForAutofill="no" />
</androidx.core.widget.NestedScrollView>

View file

@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBackground"
android:id="@+id/drawerLayout">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.wbrawner.simplemarkdown.view.DisableableViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorBackground"
app:layout_scrollFlags="scroll|enterAlways|snap" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@color/colorBackground">
<com.google.android.material.tabs.TabItem
android:id="@+id/editTab"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/action_edit" />
<com.google.android.material.tabs.TabItem
android:id="@+id/previewTab"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/action_preview" />
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigationView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:menu="@menu/menu_main" />
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.wbrawner.simplemarkdown.view.fragment.MarkdownInfoFragment">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="128dp"
android:background="@color/colorBackground">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="0dp"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<WebView
android:id="@+id/infoWebview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:nestedScrollingEnabled="false" />
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,13 +0,0 @@
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.wbrawner.simplemarkdown.view.fragment.PreviewFragment">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:nestedScrollingEnabled="false"
android:id="@+id/markdown_view" />
</androidx.core.widget.NestedScrollView>

View file

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorBackground" />
</com.google.android.material.appbar.AppBarLayout>
<fragment
android:id="@+id/fragment_settings"
android:name="com.wbrawner.simplemarkdown.view.fragment.SettingsFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

View file

@ -1,78 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorBackground"
app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipChildren="false"
android:clipToPadding="false"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/heartIcon"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_favorite_black_24dp"
android:tint="@color/colorAccent"
android:contentDescription="@string/description_heart"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/supportInfoText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@string/support_info"
android:textAlignment="center"
android:textColor="@color/colorOnBackground"
app:layout_constraintTop_toBottomOf="@+id/heartIcon" />
<com.google.android.material.button.MaterialButton
android:id="@+id/githubButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/colorBackgroundGitHub"
android:textColor="@color/colorWhite"
android:text="@string/action_view_github"
app:layout_constraintTop_toBottomOf="@+id/supportInfoText" />
<com.google.android.material.button.MaterialButton
android:id="@+id/rateButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/colorBackgroundPlayStore"
android:textColor="@color/colorWhite"
android:text="@string/action_rate"
app:layout_constraintTop_toBottomOf="@+id/githubButton" />
<LinearLayout
android:id="@+id/supportButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/rateButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_share"
android:icon="@drawable/ic_share"
android:title="@string/action_share"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_new"
android:title="@string/action_new"
android:alphabeticShortcut="N"
app:showAsAction="never" />
<item
android:id="@+id/action_load"
android:title="@string/action_open"
android:alphabeticShortcut="O"
app:showAsAction="never" />
<item
android:id="@+id/action_save"
android:alphabeticShortcut="S"
android:title="@string/action_save" />
<item
android:id="@+id/action_save_as"
android:title="@string/action_save_as"
tools:ignore="UnusedAttribute" />
<item
android:id="@+id/action_lock_swipe"
android:checkable="true"
android:title="@string/action_lock_swipe"
app:showAsAction="never" />
</menu>

View file

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<group android:id="@+id/mainGroup">
<item
android:id="@+id/action_mainFragment_to_settingsContainerFragment"
android:title="@string/action_settings"
android:icon="@drawable/ic_settings_black_24dp"
app:showAsAction="never" />
<item
android:id="@+id/action_mainFragment_to_supportFragment"
android:title="@string/support_title"
android:icon="@drawable/ic_favorite_black_24dp"
app:showAsAction="never" />
</group>
<group android:id="@+id/addtionalInfoGroup">
<item
android:id="@+id/action_mainFragment_to_helpFragment"
android:title="@string/action_help"
android:icon="@drawable/ic_help_black_24dp"
app:showAsAction="never" />
<item
android:id="@+id/action_mainFragment_to_librariesFragment"
android:title="@string/action_libraries"
android:icon="@drawable/ic_info_black_24dp"
app:showAsAction="never" />
<item
android:id="@+id/action_mainFragment_to_privacyFragment"
android:title="@string/action_privacy"
android:icon="@drawable/ic_eye_black_24dp"
app:showAsAction="never" />
</group>
</menu>

View file

@ -1,87 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@+id/mainFragment">
<fragment
android:id="@+id/settingsContainerFragment"
android:name="com.wbrawner.simplemarkdown.view.fragment.SettingsContainerFragment"
android:label="@string/title_activity_settings" />
<fragment
android:id="@+id/mainFragment"
android:name="com.wbrawner.simplemarkdown.view.fragment.MainFragment"
android:label="">
<action
android:id="@+id/action_mainFragment_to_helpFragment"
app:destination="@id/helpFragment"
app:enterAnim="@android:anim/slide_in_left"
app:exitAnim="@android:anim/slide_out_right"
app:popExitAnim="@android:anim/slide_out_right"
app:popUpTo="@id/mainFragment" />
<action
android:id="@+id/action_mainFragment_to_privacyFragment"
app:destination="@id/privacyFragment"
app:enterAnim="@android:anim/slide_in_left"
app:exitAnim="@android:anim/slide_out_right"
app:popExitAnim="@android:anim/slide_out_right"
app:popUpTo="@id/mainFragment" />
<action
android:id="@+id/action_mainFragment_to_librariesFragment"
app:destination="@id/librariesFragment"
app:enterAnim="@android:anim/slide_in_left"
app:exitAnim="@android:anim/slide_out_right"
app:popExitAnim="@android:anim/slide_out_right"
app:popUpTo="@id/mainFragment" />
<action
android:id="@+id/action_mainFragment_to_settingsContainerFragment"
app:destination="@id/settingsContainerFragment"
app:enterAnim="@android:anim/slide_in_left"
app:exitAnim="@android:anim/slide_out_right"
app:popExitAnim="@android:anim/slide_out_right"
app:popUpTo="@id/mainFragment" />
<action
android:id="@+id/action_mainFragment_to_supportFragment"
app:destination="@id/supportFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/mainFragment" />
</fragment>
<fragment
android:id="@+id/supportFragment"
android:name="com.wbrawner.simplemarkdown.view.fragment.SupportFragment"
android:label="@string/support_title" />
<fragment
android:id="@+id/helpFragment"
android:name="com.wbrawner.simplemarkdown.view.fragment.MarkdownInfoFragment"
android:label="@string/action_help"
tools:layout="@layout/fragment_markdown_info">
<argument
android:name="file"
app:argType="string"
android:defaultValue="Cheatsheet.md" />
</fragment>
<fragment
android:id="@+id/privacyFragment"
android:name="com.wbrawner.simplemarkdown.view.fragment.MarkdownInfoFragment"
android:label="@string/action_privacy"
tools:layout="@layout/fragment_markdown_info">
<argument
android:name="file"
android:defaultValue="Privacy Policy.md"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/librariesFragment"
android:name="com.wbrawner.simplemarkdown.view.fragment.MarkdownInfoFragment"
android:label="@string/action_libraries"
tools:layout="@layout/fragment_markdown_info">
<argument
android:name="file"
android:defaultValue="Libraries.md"
app:argType="string" />
</fragment>
</navigation>

View file

@ -1,27 +0,0 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
android:defaultValue="true"
android:key="autosave"
android:summaryOff="@string/pref_autosave_off"
android:summaryOn="@string/pref_autosave_on"
android:title="@string/pref_title_autosave" />
<EditTextPreference
android:defaultValue="@string/pref_custom_css_default"
android:key="@string/pref_custom_css"
android:summary="@string/pref_description_custom_css"
android:title="@string/pref_title_custom_css" />
<ListPreference
android:entries="@array/pref_entries_dark_mode"
android:entryValues="@array/pref_values_dark_mode"
android:defaultValue="@string/pref_key_dark_mode_auto"
android:key="@string/pref_key_dark_mode"
android:title="@string/title_dark_mode" />
<SwitchPreference
android:defaultValue="false"
android:key="@string/readability_enabled"
android:summaryOff="@string/pref_readability_off"
android:summaryOn="@string/pref_readability_on"
android:title="@string/pref_title_readability" />
</PreferenceScreen>

View file

@ -1,105 +1,107 @@
package com.wbrawner.simplemarkdown.utility
import android.app.Activity
import android.app.Application
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.MutableLiveData
import com.android.billingclient.api.*
import com.google.android.material.button.MaterialButton
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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 com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ConsumeParams
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.QueryProductDetailsParams
import com.wbrawner.simplemarkdown.R
class SupportLinkProvider(private val activity: Activity) : BillingClientStateListener,
PurchasesUpdatedListener {
val supportLinks = MutableLiveData<List<MaterialButton>>()
private val billingClient: BillingClient = BillingClient.newBuilder(activity.applicationContext)
.setListener(this)
@Composable
fun SupportLinks() {
val context = LocalContext.current
var products by remember { mutableStateOf(emptyList<ProductDetails>()) }
var billingClient by remember { mutableStateOf<BillingClient?>(null) }
DisposableEffect(context) {
billingClient = BillingClient.newBuilder(context.applicationContext)
.setListener { _, purchases ->
purchases?.forEach { purchase ->
val consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient?.consumeAsync(consumeParams) { _, _ ->
Toast.makeText(
context,
context.getString(R.string.support_thank_you),
Toast.LENGTH_SHORT
).show()
}
}
}
.enablePendingPurchases()
.build()
init {
billingClient.startConnection(this)
activity.application.registerActivityLifecycleCallbacks(
object : Application.ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityDestroyed(activity: Activity) {
billingClient.endConnection()
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
}
override fun onActivityResumed(activity: Activity) {
}
}
)
billingClient?.startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
billingClient?.startConnection(this)
}
override fun onBillingSetupFinished(result: BillingResult) {
if (result.responseCode != BillingClient.BillingResponseCode.OK) {
return
}
val skuDetails = SkuDetailsParams.newBuilder()
.setSkusList(listOf("support_the_developer", "tip_coffee", "tip_beer"))
.setType(BillingClient.SkuType.INAPP)
val productsQuery = listOf("support_the_developer", "tip_coffee", "tip_beer")
.map {
QueryProductDetailsParams.Product.newBuilder()
.setProductId(it)
.setProductType(BillingClient.ProductType.INAPP)
.build()
billingClient.querySkuDetailsAsync(skuDetails) { skuDetailsResponse, skuDetailsList ->
// Process the result.
if (skuDetailsResponse.responseCode != BillingClient.BillingResponseCode.OK || skuDetailsList.isNullOrEmpty()) {
return@querySkuDetailsAsync
}
val productDetailsQuery = QueryProductDetailsParams.newBuilder()
.setProductList(productsQuery)
.build()
billingClient?.queryProductDetailsAsync(productDetailsQuery) { result, productDetails ->
if (result.responseCode != BillingClient.BillingResponseCode.OK || productDetails.isEmpty()) {
return@queryProductDetailsAsync
}
products =
productDetails.sortedBy { it.oneTimePurchaseOfferDetails?.priceAmountMicros }
.toList()
}
}
})
onDispose {
billingClient?.endConnection()
}
}
skuDetailsList.sortedBy { it.priceAmountMicros }
.map { skuDetails ->
val supportButton = MaterialButton(activity)
supportButton.text = activity.getString(
R.string.support_button_purchase,
skuDetails.title,
skuDetails.price
)
supportButton.setOnClickListener {
products.forEach { product ->
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
val productDetails = ProductDetailsParams.newBuilder()
.setProductDetails(product)
.build()
val flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.setProductDetailsParamsList(listOf(productDetails))
.build()
billingClient.launchBillingFlow(activity, flowParams)
}
supportButton
}
.let {
supportLinks.postValue(it)
}
}
}
override fun onBillingServiceDisconnected() {
billingClient.startConnection(this)
}
override fun onPurchasesUpdated(result: BillingResult, purchases: MutableList<Purchase>?) {
purchases?.forEach { purchase ->
val consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.consumeAsync(consumeParams) { _, _ ->
Toast.makeText(
activity,
activity.getString(R.string.support_thank_you),
Toast.LENGTH_SHORT
).show()
billingClient?.launchBillingFlow(context as Activity, flowParams)
}
) {
Text(
context.getString(
R.string.support_button_purchase,
product.name,
product.oneTimePurchaseOfferDetails?.formattedPrice
)
)
}
}
}