fix(settings): About tab crash + update-row UI (#889)
- AppUpdateViewModel has no (Application) ctor; the default viewModel()
factory's AndroidViewModelFactory does getConstructor(Application)
-> NoSuchMethodException -> crash on opening About. Build it with an
explicit viewModel { } initializer + APPLICATION_KEY instead.
- Add instrumented regression test that renders SettingsAboutPage with
the real default factory (the exact path that crashed).
- UpdateRow: status text and action button no longer fight for one line;
Update now / Open settings / Retry get a full-width button of their
own below the row, Downloading gets a full-width progress bar.
- Strip the leading 'v' from GitHub release tags before formatting
(templates already prepend 'v' -> was rendering 'vv1.21.1').
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
683a24becf
commit
ff8370bb8d
3 files changed
+136
-53
No files matched your search
@@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.m57.hermescontrol.R
|
||||
@@ -199,7 +200,17 @@ internal fun SettingsBehaviorPage(
|
||||
@Composable
|
||||
internal fun SettingsAboutPage(
|
||||
onBack: () -> Unit,
|
||||
viewModel: AppUpdateViewModel = viewModel(),
|
||||
viewModel: AppUpdateViewModel =
|
||||
viewModel {
|
||||
val app =
|
||||
this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]
|
||||
?: error("Application not available")
|
||||
// Explicit factory lambda: the default AndroidViewModelFactory
|
||||
// resolves ctors via reflection, which cannot see Kotlin
|
||||
// default-arg synthetic constructors — opening About crashed with
|
||||
// NoSuchMethodException AppUpdateViewModel.<init>(Application).
|
||||
AppUpdateViewModel(app)
|
||||
},
|
||||
) {
|
||||
val updateState by viewModel.state.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package com.m57.hermescontrol.ui.settings.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.width
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -83,11 +84,6 @@ internal fun AboutSection(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-app update row (issue #867): tap to check GitHub releases; when a newer
|
||||
* release exists, download + install from here. Idle/up-to-date/error states
|
||||
* are tappable to re-check; actionable states show a labeled button.
|
||||
*/
|
||||
@Composable
|
||||
private fun UpdateRow(
|
||||
state: AppUpdateState,
|
||||
@@ -95,76 +91,118 @@ private fun UpdateRow(
|
||||
onStartUpdate: () -> Unit,
|
||||
onOpenInstallSettings: () -> Unit,
|
||||
) {
|
||||
// GitHub release tags carry a leading "v" (e.g. "v1.21.1"); the string
|
||||
// templates below already prepend one, so strip it here to avoid "vv".
|
||||
val tag =
|
||||
(state as? AppUpdateState.UpToDate)?.latestTag
|
||||
?: (state as? AppUpdateState.UpdateAvailable)?.latestTag
|
||||
?: (state as? AppUpdateState.Installing)?.latestTag
|
||||
val displayTag = tag?.trimStart('v').orEmpty()
|
||||
val tappable =
|
||||
state is AppUpdateState.Idle ||
|
||||
state is AppUpdateState.UpToDate ||
|
||||
state is AppUpdateState.Error
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clickable(enabled = tappable) { onCheckUpdate() },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.settings_about_update),
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clickable(enabled = tappable) { onCheckUpdate() },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.settings_about_update),
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
when (state) {
|
||||
is AppUpdateState.Idle -> {
|
||||
ValueText(stringResource(R.string.settings_about_update_check))
|
||||
}
|
||||
|
||||
is AppUpdateState.Checking -> {
|
||||
ValueText(stringResource(R.string.settings_about_update_checking))
|
||||
}
|
||||
|
||||
is AppUpdateState.UpToDate -> {
|
||||
ValueText(
|
||||
stringResource(R.string.settings_about_update_uptodate, displayTag),
|
||||
)
|
||||
}
|
||||
|
||||
is AppUpdateState.UpdateAvailable -> {
|
||||
ValueText(
|
||||
stringResource(R.string.settings_about_update_available, displayTag),
|
||||
)
|
||||
}
|
||||
|
||||
is AppUpdateState.Downloading -> {
|
||||
ValueText(
|
||||
stringResource(
|
||||
R.string.settings_about_update_downloading,
|
||||
(state.progress * 100).toInt(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is AppUpdateState.Installing -> {
|
||||
ValueText(
|
||||
stringResource(R.string.settings_about_update_installing, displayTag),
|
||||
)
|
||||
}
|
||||
|
||||
is AppUpdateState.NeedsUnknownSourcesPermission -> {
|
||||
ValueText(stringResource(R.string.settings_about_update_allow_sources))
|
||||
}
|
||||
|
||||
is AppUpdateState.Error -> {
|
||||
ValueText(state.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action area: the button/progress gets its own full-width line so a
|
||||
// long status text or error message never fights it for space.
|
||||
when (state) {
|
||||
is AppUpdateState.Idle -> {
|
||||
ValueText(stringResource(R.string.settings_about_update_check))
|
||||
}
|
||||
|
||||
is AppUpdateState.Checking -> {
|
||||
ValueText(stringResource(R.string.settings_about_update_checking))
|
||||
}
|
||||
|
||||
is AppUpdateState.UpToDate -> {
|
||||
ValueText(stringResource(R.string.settings_about_update_uptodate, state.latestTag))
|
||||
}
|
||||
|
||||
is AppUpdateState.UpdateAvailable -> {
|
||||
ValueText(stringResource(R.string.settings_about_update_available, state.latestTag))
|
||||
TextButton(onClick = onStartUpdate) {
|
||||
Button(
|
||||
onClick = onStartUpdate,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.settings_about_update_action))
|
||||
}
|
||||
}
|
||||
|
||||
is AppUpdateState.Downloading -> {
|
||||
ValueText(
|
||||
stringResource(
|
||||
R.string.settings_about_update_downloading,
|
||||
(state.progress * 100).toInt(),
|
||||
),
|
||||
)
|
||||
LinearProgressIndicator(
|
||||
progress = { state.progress },
|
||||
modifier = Modifier.width(80.dp).padding(start = 8.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
is AppUpdateState.Installing -> {
|
||||
ValueText(stringResource(R.string.settings_about_update_installing, state.latestTag))
|
||||
}
|
||||
|
||||
is AppUpdateState.NeedsUnknownSourcesPermission -> {
|
||||
ValueText(stringResource(R.string.settings_about_update_allow_sources))
|
||||
TextButton(onClick = onOpenInstallSettings) {
|
||||
OutlinedButton(
|
||||
onClick = onOpenInstallSettings,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.settings_about_update_open_settings))
|
||||
}
|
||||
}
|
||||
|
||||
is AppUpdateState.Error -> {
|
||||
ValueText(state.message)
|
||||
TextButton(onClick = onCheckUpdate) {
|
||||
OutlinedButton(
|
||||
onClick = onCheckUpdate,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.settings_about_update_retry))
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user