Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ ktlint_standard_max-line-length = disabled
# For generated code
[**/build/generated/**/*]
ktlint = disabled

# For buildkonfig generated code
[**/build/buildkonfig/**/*]
ktlint = disabled
12 changes: 0 additions & 12 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,13 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.plugin.serialization)
alias(libs.plugins.koin.compiler)
alias(libs.plugins.secrets.gradle.plugin)
alias(libs.plugins.appVersioning)
alias(libs.plugins.aboutlibraries)
alias(libs.plugins.aboutlibraries.android)
alias(libs.plugins.compose.compiler)
alias(androidx.plugins.room3)
}

secrets {
defaultPropertiesFileName = "local.defaults.properties"
ignoreList +=
listOf(
"SENTRY_DSN",
"SIGNING_STORE_PASSWORD",
"SIGNING_KEY_ALIAS",
"SIGNING_KEY_PASSWORD",
)
}

// https://stackoverflow.com/a/69268957/16484891, CC BY-SA 4.0
val localProperties =
Properties().apply {
Expand Down
2 changes: 0 additions & 2 deletions app/src/main/java/xyz/sevive/arcaeaoffline/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ import xyz.sevive.arcaeaoffline.ui.screens.ocr.queue.preferences.OcrQueuePrefere
import xyz.sevive.arcaeaoffline.ui.screens.ocr.queue.staging.OcrQueueStagingViewModel
import xyz.sevive.arcaeaoffline.ui.screens.overview.OverviewViewModel
import xyz.sevive.arcaeaoffline.ui.screens.settings.SettingsViewModel
import xyz.sevive.arcaeaoffline.ui.screens.settings.about.SettingsAboutViewModel
import xyz.sevive.arcaeaoffline.ui.screens.settings.unstablealert.SettingsUnstableAlertScreenViewModel
import xyz.sevive.arcaeaoffline.ui.screens.utilities.UtilitiesChartRecommendScreenViewModel

Expand Down Expand Up @@ -135,7 +134,6 @@ val appModule =

single<DiagnosticsCollector> { create(::createDiagnosticsCollector) }

viewModel<SettingsAboutViewModel>()
viewModel<EmergencyModeActivityViewModel>()
viewModel<OverviewViewModel>()
viewModel<DatabaseNavEntryViewModel>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ enum class SettingsSubScreen(
val route: String,
@StringRes val title: Int,
) {
General("$SETTINGS_NAV_ROUTE_ROOT/general", R.string.settings_general_title),
About("$SETTINGS_NAV_ROUTE_ROOT/about", R.string.settings_about_title),
License("$SETTINGS_NAV_ROUTE_ROOT/license", R.string.settings_license_title),
Aboutlibraries("$SETTINGS_NAV_ROUTE_ROOT/aboutlibraries", R.string.settings_aboutlibraries_title),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package xyz.sevive.arcaeaoffline.ui.screens.settings

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.CloudDownload
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.github.vinceglb.filekit.dialogs.compose.rememberDirectoryPickerLauncher
import org.koin.compose.viewmodel.koinViewModel
import xyz.sevive.arcaeaoffline.R
import xyz.sevive.arcaeaoffline.core.api.ArcaeaResourcesApiClient
import xyz.sevive.arcaeaoffline.data.IS_UNSTABLE_VERSION
import xyz.sevive.arcaeaoffline.ui.components.OperationTextItem
import xyz.sevive.arcaeaoffline.ui.components.SettingsGroupHeader
import xyz.sevive.arcaeaoffline.ui.components.SwitchItem
import xyz.sevive.arcaeaoffline.ui.components.TextItem
import xyz.sevive.arcaeaoffline.ui.components.dialogs.DialogConfirmButton
import xyz.sevive.arcaeaoffline.ui.components.dialogs.DialogDismissTextButton
import xyz.sevive.arcaeaoffline.ui.navigation.LocalListDetailNavigationContext
import xyz.sevive.arcaeaoffline.ui.navigation.SettingsSubScreen
import xyz.sevive.arcaeaoffline.ui.screens.NavEntryNavigateButton
import xyz.sevive.arcaeaoffline.ui.screens.settings.unstablealert.UnstableBuildAlertCard
import xyz.sevive.arcaeaoffline.ui.theme.spacing

@Composable
private fun ResourcesApiBaseUrlEditDialog(
initialValue: String,
onConfirm: (String) -> Unit,
onReset: () -> Unit,
onDismiss: () -> Unit,
) {
var value by rememberSaveable { mutableStateOf(initialValue) }

val trimmed = value.trim()
val isValid = trimmed.startsWith("http://") || trimmed.startsWith("https://")

AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.settings_general_pref_resources_api_base_url_dialog_title)) },
text = {
TextField(
value = value,
onValueChange = { value = it },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
isError = value.isNotBlank() && !isValid,
supportingText = {
if (value.isNotBlank() && !isValid) {
Text(stringResource(R.string.settings_general_pref_resources_api_base_url_invalid))
} else if (trimmed.startsWith("http://")) {
// Android 9+ silently blocks cleartext: an http URL is "valid" here, but
// every probe and download would fail with a generic network error.
Text(stringResource(R.string.settings_general_pref_resources_api_base_url_http_hint))
}
},
modifier = Modifier.fillMaxWidth(),
)
},
dismissButton = {
DialogDismissTextButton(
onClick = {
onReset()
onDismiss()
},
customIcon = { Icon(Icons.AutoMirrored.Filled.Undo, contentDescription = null) },
customLabel = { Text(stringResource(R.string.general_reset)) },
)
},
confirmButton = {
DialogConfirmButton(
onClick = {
onConfirm(trimmed)
onDismiss()
},
enabled = isValid,
customIcon = { Icon(Icons.Default.Edit, contentDescription = null) },
customLabel = { Text(stringResource(R.string.general_save)) },
)
},
)
}

@Composable
internal fun SettingsContent(
onNavigateToEmergencyModeActivity: () -> Unit,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
) {
val navContext = LocalListDetailNavigationContext.current
val viewModel: SettingsViewModel = koinViewModel()
val uiState by viewModel.appPreferencesUiState.collectAsStateWithLifecycle()
val diagnosticsExportState by viewModel.diagnosticsExportState.collectAsStateWithLifecycle()
val diagnosticsDirPicker =
rememberDirectoryPickerLauncher { dir ->
dir?.let(viewModel::exportDiagnostics)
}

var showBaseUrlEditDialog by rememberSaveable { mutableStateOf(false) }
if (showBaseUrlEditDialog) {
ResourcesApiBaseUrlEditDialog(
initialValue = uiState.resourcesApiBaseUrl,
onConfirm = viewModel::setResourcesApiBaseUrl,
onReset = { viewModel.setResourcesApiBaseUrl(ArcaeaResourcesApiClient.DEFAULT_BASE_URL) },
onDismiss = { showBaseUrlEditDialog = false },
)
}

Column(
modifier
.verticalScroll(rememberScrollState())
.padding(contentPadding),
) {
if (IS_UNSTABLE_VERSION) {
UnstableBuildAlertCard(
onClick = { navContext.navigateToDetail(SettingsSubScreen.UnstableAlert.route) },
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = MaterialTheme.spacing.pagePadding),
showDetails = true,
)
}

SettingsGroupHeader(stringResource(R.string.settings_general_title))
TextItem(
onClick = { showBaseUrlEditDialog = true },
leadingIcon = Icons.Default.CloudDownload,
title = stringResource(R.string.settings_general_pref_resources_api_base_url),
content = uiState.resourcesApiBaseUrl,
)

SettingsGroupHeader(stringResource(R.string.settings_debug_title))
SwitchItem(
value = uiState.autoSendCrashReports,
onValueChange = viewModel::setAutoSendCrashReports,
icon = Icons.Default.BugReport,
title = stringResource(R.string.settings_app_pref_enable_sentry),
)
OperationTextItem(
title = stringResource(R.string.diagnostics_export),
content = stringResource(R.string.diagnostics_export_content),
state = diagnosticsExportState,
leadingIcon = Icons.Default.BugReport,
onClick = { diagnosticsDirPicker.launch() },
)
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
TextItem(
title = stringResource(R.string.emergency_mode_title),
leadingIcon = ImageVector.vectorResource(R.drawable.ic_activity_emergency_mode),
leadingIconTint = LocalContentColor.current,
trailingIcon = Icons.AutoMirrored.Default.ArrowForward,
onClick = onNavigateToEmergencyModeActivity,
)
}

HorizontalDivider()

NavEntryNavigateButton(
titleResId = SettingsSubScreen.About.title,
icon = Icons.Outlined.Info,
) {
navContext.navigateToDetail(SettingsSubScreen.About.route)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,100 +2,34 @@ package xyz.sevive.arcaeaoffline.ui.screens.settings

import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Apps
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import xyz.sevive.arcaeaoffline.R
import xyz.sevive.arcaeaoffline.data.IS_UNSTABLE_VERSION
import xyz.sevive.arcaeaoffline.ui.components.TextItem
import xyz.sevive.arcaeaoffline.ui.navigation.LocalListDetailNavigationContext
import xyz.sevive.arcaeaoffline.ui.navigation.MainScreen
import xyz.sevive.arcaeaoffline.ui.navigation.SettingsSubScreen
import xyz.sevive.arcaeaoffline.ui.screens.NavEntryNavigateButton
import xyz.sevive.arcaeaoffline.ui.screens.settings.unstablealert.UnstableBuildAlertCard
import xyz.sevive.arcaeaoffline.ui.theme.spacing

@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun SettingsNavEntry(
onNavigateToEmergencyModeActivity: () -> Unit,
modifier: Modifier = Modifier,
) {
val navContext = LocalListDetailNavigationContext.current

Scaffold(
modifier,
topBar = {
TopAppBar(title = { Text(stringResource(MainScreen.Settings.title)) })
},
) {
LazyColumn(
Modifier
.fillMaxSize()
.consumeWindowInsets(it),
contentPadding = it,
) {
if (IS_UNSTABLE_VERSION) {
item {
UnstableBuildAlertCard(
onClick = { navContext.navigateToDetail(SettingsSubScreen.UnstableAlert.route) },
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = MaterialTheme.spacing.pagePadding),
showDetails = true,
)
}
}

item {
NavEntryNavigateButton(
titleResId = SettingsSubScreen.General.title,
icon = Icons.Default.Apps,
) {
navContext.navigateToDetail(SettingsSubScreen.General.route)
}
}

item {
NavEntryNavigateButton(
titleResId = SettingsSubScreen.About.title,
icon = Icons.Outlined.Info,
) {
navContext.navigateToDetail(SettingsSubScreen.About.route)
}
}

item {
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.error,
) {
TextItem(
title = stringResource(R.string.emergency_mode_title),
leadingIcon = ImageVector.vectorResource(R.drawable.ic_activity_emergency_mode),
leadingIconTint = LocalContentColor.current,
trailingIcon = Icons.AutoMirrored.Default.ArrowForward,
) {
onNavigateToEmergencyModeActivity()
}
}
}
}
) { padding ->
SettingsContent(
onNavigateToEmergencyModeActivity = onNavigateToEmergencyModeActivity,
modifier =
Modifier
.fillMaxSize()
.consumeWindowInsets(padding),
contentPadding = padding,
)
}
}
Loading
Loading