Rörelsevakt: accelerometern fryser spåret när man står stilla
All checks were successful
release / build-release (push) Successful in 4m19s
All checks were successful
release / build-release (push) Successful in 4m19s
GPS-drift vid stillastående gav fejkspår/-distans/-höjdmeter (och 2 km/h snittfart från en soffa). Nu mäter accelerometern vibrationsmönstret (glidande 4 s-fönster av |accel|−g): under tröskeln = stilla → kartpositionen uppdateras men spårpunkter, distans och höjdmeter fryses. GPS-rutan visar 'stilla'. Övergångar loggas i tekniska loggen med uppmätt nivå. Inställningar → GPS-spårning: av/på-switch + känslighet (0,35 m/s² standard, justerbar 0,10–1,50). Version 0.8.4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2GA94E1f3cmrvdQLQhYdg
This commit is contained in:
@@ -29,7 +29,7 @@ android {
|
|||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = ciRunNumber ?: 1
|
versionCode = ciRunNumber ?: 1
|
||||||
versionName = "0.8.3" + (ciRunNumber?.let { "+build.$it" } ?: "-dev")
|
versionName = "0.8.4" + (ciRunNumber?.let { "+build.$it" } ?: "-dev")
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ data class AppSettings(
|
|||||||
val gpsSpeedFactor: Double = 2.0,
|
val gpsSpeedFactor: Double = 2.0,
|
||||||
/** GPS-filter: fartgolv (km/h) när Doppler-fart saknas/är noll */
|
/** GPS-filter: fartgolv (km/h) när Doppler-fart saknas/är noll */
|
||||||
val gpsSpeedFloorKmh: Int = 12,
|
val gpsSpeedFloorKmh: Int = 12,
|
||||||
|
/** Rörelsevakt: frys spår/distans när accelerometern säger stilla */
|
||||||
|
val motionGuardEnabled: Boolean = true,
|
||||||
|
/** Rörelsevaktens känslighet (m/s² avvikelse från vila; lägre = känsligare) */
|
||||||
|
val motionThreshold: Double = 0.35,
|
||||||
)
|
)
|
||||||
|
|
||||||
class SettingsStore(private val context: Context) {
|
class SettingsStore(private val context: Context) {
|
||||||
@@ -70,9 +74,21 @@ class SettingsStore(private val context: Context) {
|
|||||||
gpsAccuracyLimitM = prefs[KEY_GPS_ACC] ?: 35,
|
gpsAccuracyLimitM = prefs[KEY_GPS_ACC] ?: 35,
|
||||||
gpsSpeedFactor = prefs[KEY_GPS_FACTOR]?.toDoubleOrNull() ?: 2.0,
|
gpsSpeedFactor = prefs[KEY_GPS_FACTOR]?.toDoubleOrNull() ?: 2.0,
|
||||||
gpsSpeedFloorKmh = prefs[KEY_GPS_FLOOR] ?: 12,
|
gpsSpeedFloorKmh = prefs[KEY_GPS_FLOOR] ?: 12,
|
||||||
|
motionGuardEnabled = prefs[KEY_MOTION_GUARD]?.toBooleanStrictOrNull() ?: true,
|
||||||
|
motionThreshold = prefs[KEY_MOTION_THRESHOLD]?.toDoubleOrNull() ?: 0.35,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setMotionGuardEnabled(value: Boolean) {
|
||||||
|
context.settingsDataStore.edit { it[KEY_MOTION_GUARD] = value.toString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setMotionThreshold(value: Double) {
|
||||||
|
context.settingsDataStore.edit {
|
||||||
|
it[KEY_MOTION_THRESHOLD] = (Math.round(value.coerceIn(0.1, 1.5) * 100) / 100.0).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setGpsAccuracyLimit(value: Int) {
|
suspend fun setGpsAccuracyLimit(value: Int) {
|
||||||
context.settingsDataStore.edit { it[KEY_GPS_ACC] = value.coerceIn(10, 100) }
|
context.settingsDataStore.edit { it[KEY_GPS_ACC] = value.coerceIn(10, 100) }
|
||||||
}
|
}
|
||||||
@@ -164,5 +180,7 @@ class SettingsStore(private val context: Context) {
|
|||||||
private val KEY_GPS_ACC = intPreferencesKey("gps_accuracy_limit_m")
|
private val KEY_GPS_ACC = intPreferencesKey("gps_accuracy_limit_m")
|
||||||
private val KEY_GPS_FACTOR = stringPreferencesKey("gps_speed_factor")
|
private val KEY_GPS_FACTOR = stringPreferencesKey("gps_speed_factor")
|
||||||
private val KEY_GPS_FLOOR = intPreferencesKey("gps_speed_floor_kmh")
|
private val KEY_GPS_FLOOR = intPreferencesKey("gps_speed_floor_kmh")
|
||||||
|
private val KEY_MOTION_GUARD = stringPreferencesKey("motion_guard_enabled")
|
||||||
|
private val KEY_MOTION_THRESHOLD = stringPreferencesKey("motion_threshold")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.hardware.Sensor
|
||||||
|
import android.hardware.SensorEvent
|
||||||
|
import android.hardware.SensorEventListener
|
||||||
|
import android.hardware.SensorManager
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationListener
|
import android.location.LocationListener
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
@@ -53,6 +57,8 @@ data class TrackingState(
|
|||||||
val currentSpeedKmh: Double? = null,
|
val currentSpeedKmh: Double? = null,
|
||||||
val kcal: Double? = null,
|
val kcal: Double? = null,
|
||||||
val gpsFix: Boolean = false,
|
val gpsFix: Boolean = false,
|
||||||
|
/** false = accelerometern säger stilla → spår/distans fryst */
|
||||||
|
val isMoving: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,7 +67,7 @@ data class TrackingState(
|
|||||||
* timern — ingen GPS. UI:t läser [state]; servicen äger sanningen så
|
* timern — ingen GPS. UI:t läser [state]; servicen äger sanningen så
|
||||||
* spårningen överlever att appen swipas bort från recents.
|
* spårningen överlever att appen swipas bort från recents.
|
||||||
*/
|
*/
|
||||||
class TrackingService : Service(), LocationListener {
|
class TrackingService : Service(), LocationListener, SensorEventListener {
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
private var locationManager: LocationManager? = null
|
private var locationManager: LocationManager? = null
|
||||||
@@ -76,6 +82,13 @@ class TrackingService : Service(), LocationListener {
|
|||||||
// Glidande Doppler-fart (m/s) — GPS:ens egen fartmätning, stabilare än positionerna
|
// Glidande Doppler-fart (m/s) — GPS:ens egen fartmätning, stabilare än positionerna
|
||||||
private var dopplerEmaMs = 0.0
|
private var dopplerEmaMs = 0.0
|
||||||
private var rejectStreak = 0
|
private var rejectStreak = 0
|
||||||
|
// Rörelsevakt (accelerometer): står man stilla fryses spår/distans/höjd
|
||||||
|
private var sensorManager: SensorManager? = null
|
||||||
|
private var motionGuard = true
|
||||||
|
private var motionThreshold = 0.35
|
||||||
|
private val motionWindow = ArrayDeque<Double>()
|
||||||
|
private var isMoving = true
|
||||||
|
private var lastMotionLogged = true
|
||||||
// Glidande medel av höjd för att filtrera GPS-brus innan höjdmeter summeras
|
// Glidande medel av höjd för att filtrera GPS-brus innan höjdmeter summeras
|
||||||
private val altitudeWindow = ArrayDeque<Double>()
|
private val altitudeWindow = ArrayDeque<Double>()
|
||||||
private var smoothedAltitude: Double? = null
|
private var smoothedAltitude: Double? = null
|
||||||
@@ -109,8 +122,13 @@ class TrackingService : Service(), LocationListener {
|
|||||||
accuracyLimitM = intent.getIntExtra(EXTRA_GPS_ACC_LIMIT, 35).toFloat()
|
accuracyLimitM = intent.getIntExtra(EXTRA_GPS_ACC_LIMIT, 35).toFloat()
|
||||||
speedFactor = intent.getDoubleExtra(EXTRA_GPS_SPEED_FACTOR, 2.0)
|
speedFactor = intent.getDoubleExtra(EXTRA_GPS_SPEED_FACTOR, 2.0)
|
||||||
speedFloorMs = intent.getIntExtra(EXTRA_GPS_SPEED_FLOOR, 12) / 3.6
|
speedFloorMs = intent.getIntExtra(EXTRA_GPS_SPEED_FLOOR, 12) / 3.6
|
||||||
|
motionGuard = intent.getBooleanExtra(EXTRA_MOTION_GUARD, true)
|
||||||
|
motionThreshold = intent.getDoubleExtra(EXTRA_MOTION_THRESHOLD, 0.35)
|
||||||
dopplerEmaMs = 0.0
|
dopplerEmaMs = 0.0
|
||||||
rejectStreak = 0
|
rejectStreak = 0
|
||||||
|
motionWindow.clear()
|
||||||
|
isMoving = true
|
||||||
|
lastMotionLogged = true
|
||||||
|
|
||||||
_state.value = TrackingState(
|
_state.value = TrackingState(
|
||||||
isActive = true,
|
isActive = true,
|
||||||
@@ -143,7 +161,10 @@ class TrackingService : Service(), LocationListener {
|
|||||||
}
|
}
|
||||||
startForeground(NOTIFICATION_ID, buildNotification(), type)
|
startForeground(NOTIFICATION_ID, buildNotification(), type)
|
||||||
|
|
||||||
if (distanceBased) startGps()
|
if (distanceBased) {
|
||||||
|
startGps()
|
||||||
|
if (motionGuard) startMotionGuard()
|
||||||
|
}
|
||||||
|
|
||||||
// Tick: tid + kcal + notis. Klockan räknar från att fasen blev ACTIVE.
|
// Tick: tid + kcal + notis. Klockan räknar från att fasen blev ACTIVE.
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -216,6 +237,44 @@ class TrackingService : Service(), LocationListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startMotionGuard() {
|
||||||
|
val sm = getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||||
|
sensorManager = sm
|
||||||
|
val accel = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||||
|
if (accel == null) {
|
||||||
|
TrackLog.log("rörelsevakt: ingen accelerometer — avstängd")
|
||||||
|
motionGuard = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sm.registerListener(this, accel, SensorManager.SENSOR_DELAY_UI)
|
||||||
|
TrackLog.log("rörelsevakt: på (tröskel ${motionThreshold} m/s²)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSensorChanged(event: SensorEvent) {
|
||||||
|
if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) return
|
||||||
|
val x = event.values[0].toDouble()
|
||||||
|
val y = event.values[1].toDouble()
|
||||||
|
val z = event.values[2].toDouble()
|
||||||
|
// Avvikelse från vilomagnituden (gravitationen) — steg ger tydliga utslag
|
||||||
|
val deviation = kotlin.math.abs(kotlin.math.sqrt(x * x + y * y + z * z) - 9.81)
|
||||||
|
motionWindow.addLast(deviation)
|
||||||
|
if (motionWindow.size > 20) motionWindow.removeFirst() // ~4 s vid UI-takt
|
||||||
|
if (motionWindow.size < 8) return
|
||||||
|
|
||||||
|
val avg = motionWindow.average()
|
||||||
|
val moving = avg > motionThreshold
|
||||||
|
if (moving != isMoving) {
|
||||||
|
isMoving = moving
|
||||||
|
_state.value = _state.value.copy(isMoving = moving)
|
||||||
|
if (moving != lastMotionLogged) {
|
||||||
|
TrackLog.log("rörelsevakt: ${if (moving) "i rörelse" else "stilla"} (nivå ${"%.2f".format(avg)})")
|
||||||
|
lastMotionLogged = moving
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit
|
||||||
|
|
||||||
/** Gå från GPS-sökning till aktiv (klockan börjar ticka). */
|
/** Gå från GPS-sökning till aktiv (klockan börjar ticka). */
|
||||||
private fun activate(reason: String) {
|
private fun activate(reason: String) {
|
||||||
val s = _state.value
|
val s = _state.value
|
||||||
@@ -274,6 +333,13 @@ class TrackingService : Service(), LocationListener {
|
|||||||
}
|
}
|
||||||
rejectStreak = 0
|
rejectStreak = 0
|
||||||
|
|
||||||
|
// Rörelsevakten: stilla ⇒ uppdatera bara kartpositionen, frys resten.
|
||||||
|
// GPS-drift när man står still blir annars fejkdistans/-spår/-höjdmeter.
|
||||||
|
if (motionGuard && !isMoving) {
|
||||||
|
lastLocation = location
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
TrackLog.log(
|
TrackLog.log(
|
||||||
"${location.provider}: fix acc=${"%.0f".format(acc)} m" +
|
"${location.provider}: fix acc=${"%.0f".format(acc)} m" +
|
||||||
" alt=${if (location.hasAltitude()) "%.0f".format(location.altitude) else "?"}" +
|
" alt=${if (location.hasAltitude()) "%.0f".format(location.altitude) else "?"}" +
|
||||||
@@ -321,6 +387,7 @@ class TrackingService : Service(), LocationListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopTracking() {
|
private fun stopTracking() {
|
||||||
|
sensorManager?.unregisterListener(this)
|
||||||
val s = _state.value
|
val s = _state.value
|
||||||
TrackLog.log(
|
TrackLog.log(
|
||||||
"stopp: ${s.elapsedSeconds}s, ${"%.0f".format(s.distanceMeters)} m, " +
|
"stopp: ${s.elapsedSeconds}s, ${"%.0f".format(s.distanceMeters)} m, " +
|
||||||
@@ -333,6 +400,7 @@ class TrackingService : Service(), LocationListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
sensorManager?.unregisterListener(this)
|
||||||
locationManager?.removeUpdates(this)
|
locationManager?.removeUpdates(this)
|
||||||
if (_state.value.isActive) _state.value = _state.value.copy(isActive = false)
|
if (_state.value.isActive) _state.value = _state.value.copy(isActive = false)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
@@ -393,6 +461,8 @@ class TrackingService : Service(), LocationListener {
|
|||||||
const val EXTRA_GPS_ACC_LIMIT = "gpsAccLimit"
|
const val EXTRA_GPS_ACC_LIMIT = "gpsAccLimit"
|
||||||
const val EXTRA_GPS_SPEED_FACTOR = "gpsSpeedFactor"
|
const val EXTRA_GPS_SPEED_FACTOR = "gpsSpeedFactor"
|
||||||
const val EXTRA_GPS_SPEED_FLOOR = "gpsSpeedFloor"
|
const val EXTRA_GPS_SPEED_FLOOR = "gpsSpeedFloor"
|
||||||
|
const val EXTRA_MOTION_GUARD = "motionGuard"
|
||||||
|
const val EXTRA_MOTION_THRESHOLD = "motionThreshold"
|
||||||
|
|
||||||
private val _state = MutableStateFlow(TrackingState())
|
private val _state = MutableStateFlow(TrackingState())
|
||||||
/** Läses av UI:t — servicen äger sanningen. */
|
/** Läses av UI:t — servicen äger sanningen. */
|
||||||
@@ -405,6 +475,8 @@ class TrackingService : Service(), LocationListener {
|
|||||||
gpsAccuracyLimitM: Int = 35,
|
gpsAccuracyLimitM: Int = 35,
|
||||||
gpsSpeedFactor: Double = 2.0,
|
gpsSpeedFactor: Double = 2.0,
|
||||||
gpsSpeedFloorKmh: Int = 12,
|
gpsSpeedFloorKmh: Int = 12,
|
||||||
|
motionGuardEnabled: Boolean = true,
|
||||||
|
motionThreshold: Double = 0.35,
|
||||||
) {
|
) {
|
||||||
val intent = Intent(context, TrackingService::class.java).apply {
|
val intent = Intent(context, TrackingService::class.java).apply {
|
||||||
putExtra(EXTRA_TYPE_ID, type.id)
|
putExtra(EXTRA_TYPE_ID, type.id)
|
||||||
@@ -417,6 +489,8 @@ class TrackingService : Service(), LocationListener {
|
|||||||
putExtra(EXTRA_GPS_ACC_LIMIT, gpsAccuracyLimitM)
|
putExtra(EXTRA_GPS_ACC_LIMIT, gpsAccuracyLimitM)
|
||||||
putExtra(EXTRA_GPS_SPEED_FACTOR, gpsSpeedFactor)
|
putExtra(EXTRA_GPS_SPEED_FACTOR, gpsSpeedFactor)
|
||||||
putExtra(EXTRA_GPS_SPEED_FLOOR, gpsSpeedFloorKmh)
|
putExtra(EXTRA_GPS_SPEED_FLOOR, gpsSpeedFloorKmh)
|
||||||
|
putExtra(EXTRA_MOTION_GUARD, motionGuardEnabled)
|
||||||
|
putExtra(EXTRA_MOTION_THRESHOLD, motionThreshold)
|
||||||
}
|
}
|
||||||
context.startForegroundService(intent)
|
context.startForegroundService(intent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ class SettingsViewModel(
|
|||||||
fun adjustGpsSpeedFloor(delta: Int) = viewModelScope.launch {
|
fun adjustGpsSpeedFloor(delta: Int) = viewModelScope.launch {
|
||||||
store.setGpsSpeedFloor(settings.value.gpsSpeedFloorKmh + delta)
|
store.setGpsSpeedFloor(settings.value.gpsSpeedFloorKmh + delta)
|
||||||
}
|
}
|
||||||
|
fun setMotionGuard(value: Boolean) = viewModelScope.launch { store.setMotionGuardEnabled(value) }
|
||||||
|
fun adjustMotionThreshold(delta: Double) = viewModelScope.launch {
|
||||||
|
store.setMotionThreshold(settings.value.motionThreshold + delta)
|
||||||
|
}
|
||||||
|
|
||||||
fun setAutoRelogin(value: Boolean) = viewModelScope.launch {
|
fun setAutoRelogin(value: Boolean) = viewModelScope.launch {
|
||||||
store.setAutoRelogin(value)
|
store.setAutoRelogin(value)
|
||||||
@@ -184,8 +188,35 @@ fun SettingsScreen(
|
|||||||
onMinus = { viewModel.adjustGpsSpeedFloor(-2) },
|
onMinus = { viewModel.adjustGpsSpeedFloor(-2) },
|
||||||
onPlus = { viewModel.adjustGpsSpeedFloor(2) },
|
onPlus = { viewModel.adjustGpsSpeedFloor(2) },
|
||||||
)
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text("Rörelsevakt (accelerometer)", style = MaterialTheme.typography.bodyLarge)
|
||||||
|
Text(
|
||||||
|
"Fryser spår och distans när telefonen känner att du står stilla.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
androidx.compose.material3.Switch(
|
||||||
|
checked = settings.motionGuardEnabled,
|
||||||
|
onCheckedChange = viewModel::setMotionGuard,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (settings.motionGuardEnabled) {
|
||||||
|
StepperRow(
|
||||||
|
label = "Rörelsekänslighet",
|
||||||
|
value = "${settings.motionThreshold} m/s²",
|
||||||
|
onMinus = { viewModel.adjustMotionThreshold(-0.05) },
|
||||||
|
onPlus = { viewModel.adjustMotionThreshold(0.05) },
|
||||||
|
)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
"Gäller från nästa startade aktivitet. Standard: 35 m / 2,0× / 12 km/h.",
|
"Gäller från nästa startade aktivitet. Standard: 35 m / 2,0× / 12 km/h / 0,35 m/s².",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ class TrackViewModel(
|
|||||||
gpsAccuracyLimitM = cfg.gpsAccuracyLimitM,
|
gpsAccuracyLimitM = cfg.gpsAccuracyLimitM,
|
||||||
gpsSpeedFactor = cfg.gpsSpeedFactor,
|
gpsSpeedFactor = cfg.gpsSpeedFactor,
|
||||||
gpsSpeedFloorKmh = cfg.gpsSpeedFloorKmh,
|
gpsSpeedFloorKmh = cfg.gpsSpeedFloorKmh,
|
||||||
|
motionGuardEnabled = cfg.motionGuardEnabled,
|
||||||
|
motionThreshold = cfg.motionThreshold,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,7 +427,15 @@ private fun LiveContent(
|
|||||||
} else "—",
|
} else "—",
|
||||||
Modifier.weight(1f),
|
Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
StatBox("GPS", if (tracking.gpsFix) "OK" else "söker…", Modifier.weight(1f))
|
StatBox(
|
||||||
|
"GPS",
|
||||||
|
when {
|
||||||
|
!tracking.gpsFix -> "söker…"
|
||||||
|
!tracking.isMoving -> "stilla"
|
||||||
|
else -> "OK"
|
||||||
|
},
|
||||||
|
Modifier.weight(1f),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
|
|||||||
Reference in New Issue
Block a user