Address lint issues
Some checks failed
Build & Test / Validate (pull_request) Successful in 19s
Build & Test / Run Unit Tests (pull_request) Successful in 10m34s
Build & Test / Run UI Tests (pull_request) Failing after 4m1s

This commit is contained in:
William Brawner 2024-07-31 20:17:14 -06:00
parent 06cbc5ec31
commit 86cd33ff5f
Signed by: wbrawner
GPG key ID: 8FF12381C6C90D35
28 changed files with 157 additions and 134 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ interface PreferenceHelper {
fun <T> observe(preference: Preference): StateFlow<T> fun <T> observe(preference: Preference): StateFlow<T>
} }
class AndroidPreferenceHelper(context: Context, val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)): PreferenceHelper { class AndroidPreferenceHelper(context: Context, private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)): PreferenceHelper {
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val states = mapOf( private val states = mapOf(
Preference.ANALYTICS_ENABLED to MutableStateFlow(get(Preference.ANALYTICS_ENABLED)), Preference.ANALYTICS_ENABLED to MutableStateFlow(get(Preference.ANALYTICS_ENABLED)),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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