Voeg thema-selector toe (DayNight, Radiogroep, teal accent)

This commit is contained in:
eddy 2026-07-27 00:22:43 +02:00
commit c977dc25a2
12 changed files with 201 additions and 24 deletions

View file

@ -5,6 +5,9 @@ plugins {
android {
namespace 'com.eddy.atomrelay'
// compileSdk/targetSdk stays on 34 (Android 14) by deliberate choice. The target audience
// consists of physically disabled users who are very reluctant to update their phones. Staying
// on API 34 maximises device compatibility. Re-evaluate when the user base has migrated.
compileSdk 34
defaultConfig {

View file

@ -14,7 +14,7 @@
android:allowBackup="false"
android:label="Atom Relay"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light">
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"

View file

@ -54,6 +54,7 @@ class MainActivity : AppCompatActivity() {
private var seq = 1
private var hmacKey: ByteArray? = null
private var lastTheme: String? = null
private lateinit var statusText: TextView
private lateinit var relayButton: Button
@ -144,6 +145,8 @@ class MainActivity : AppCompatActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
lastTheme = PrefsHelper.getPrefs(this).getString(PrefsHelper.PREF_THEME, "system") ?: "system"
PrefsHelper.applyTheme(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@ -173,6 +176,12 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
val current = PrefsHelper.getPrefs(this).getString(PrefsHelper.PREF_THEME, "system") ?: "system"
if (current != lastTheme) {
lastTheme = current
recreate()
return
}
loadState()
if (hmacKey == null) {
statusText.text = getString(R.string.status_no_key)
@ -321,6 +330,13 @@ class MainActivity : AppCompatActivity() {
val cmd: Byte = if (on) 0x01 else 0x00
val packet = buildPacket(cmd, key)
char.value = packet
// WRITE_TYPE_NO_RESPONSE is intentional. Previously, WRITE_TYPE_DEFAULT (with ACK) was
// used, but the BLE write acknowledgement took long enough that the user could release
// the button before the ACK arrived. When ACTION_UP fired during a pending write, the
// release was silently ignored and the relay (and app) got stuck in the ON state.
// WRITE_TYPE_NO_RESPONSE removes that latency and allows immediate ON→OFF sequences.
// This means the UI reflects the user's intent ("pressed" / "released") rather than
// confirmed relay state — an acceptable trade-off for a hold-to-activate interaction.
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
gatt.writeCharacteristic(char)
seq++

View file

@ -4,6 +4,7 @@ package com.eddy.atomrelay
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
@ -13,8 +14,19 @@ object PrefsHelper {
const val PREFS_NAME = "relay_prefs"
const val PREF_KEY = "hmac_key"
const val PREF_SEQ = "seq_counter"
const val PREF_THEME = "theme"
const val SEQ_MAX = 1_000_000
fun applyTheme(context: Context) {
val prefs = getPrefs(context)
val mode = when (prefs.getString(PREF_THEME, "system")) {
"light" -> AppCompatDelegate.MODE_NIGHT_NO
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
AppCompatDelegate.setDefaultNightMode(mode)
}
fun getPrefs(context: Context): SharedPreferences {
return try {
val masterKey = MasterKey.Builder(context)

View file

@ -14,9 +14,12 @@ import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Button
import android.widget.ProgressBar
import android.widget.RadioGroup
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
@ -52,6 +55,7 @@ class SettingsActivity : AppCompatActivity() {
private var progressDialog: AlertDialog? = null
private var dialog: AlertDialog? = null
private var flasherJob: Job? = null
private var lastTheme: String? = null
private val usbAttachedReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@ -104,6 +108,8 @@ class SettingsActivity : AppCompatActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
lastTheme = PrefsHelper.getPrefs(this).getString(PrefsHelper.PREF_THEME, "system") ?: "system"
PrefsHelper.applyTheme(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
@ -123,6 +129,55 @@ class SettingsActivity : AppCompatActivity() {
registerReceiver(usbAttachedReceiver, IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED), Context.RECEIVER_EXPORTED)
registerReceiver(usbPermissionReceiver, IntentFilter(ACTION_USB_PERMISSION), Context.RECEIVER_NOT_EXPORTED)
registerReceiver(usbDetachedReceiver, IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED), Context.RECEIVER_EXPORTED)
setupThemeButtons()
}
override fun onResume() {
super.onResume()
val current = PrefsHelper.getPrefs(this).getString(PrefsHelper.PREF_THEME, "system") ?: "system"
if (current != lastTheme) {
lastTheme = current
recreate()
}
}
private fun setupThemeButtons() {
val current = PrefsHelper.getPrefs(this).getString(PrefsHelper.PREF_THEME, "system") ?: "system"
val group = findViewById<RadioGroup>(R.id.themeGroup)
group.check(when (current) {
"light" -> R.id.themeLight
"dark" -> R.id.themeDark
else -> R.id.themeSystem
})
data class ThemeOpt(val id: Int, val key: String, val label: String)
val options = listOf(
ThemeOpt(R.id.themeSystem, "system", getString(R.string.theme_system)),
ThemeOpt(R.id.themeLight, "light", getString(R.string.theme_light)),
ThemeOpt(R.id.themeDark, "dark", getString(R.string.theme_dark))
)
options.forEach { (id, key, label) ->
val rb = findViewById<android.widget.RadioButton>(id)
val isActive = key == current
rb.contentDescription = if (isActive)
getString(R.string.theme_applied, label)
else
label
rb.setOnClickListener {
selectTheme(key)
}
}
}
private fun selectTheme(theme: String) {
PrefsHelper.getPrefs(this).edit().putString(PrefsHelper.PREF_THEME, theme).apply()
Handler(Looper.getMainLooper()).post {
PrefsHelper.applyTheme(this)
}
}
override fun onDestroy() {

View file

@ -38,6 +38,59 @@
android:layout_height="wrap_content"
android:text="Firmware Flashen" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/section_theme"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="3">
<RadioGroup
android:id="@+id/themeGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/themeSystem"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/theme_system" />
<RadioButton
android:id="@+id/themeLight"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/theme_light" />
<RadioButton
android:id="@+id/themeDark"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/theme_dark" />
</RadioGroup>
</LinearLayout>
<View
android:layout_width="0dp"
android:layout_height="0dp"

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#4DB6AC</color>
<color name="colorPrimaryDark">#00897B</color>
<color name="colorAccent">#80CBC4</color>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#00695C</color>
<color name="colorPrimaryDark">#004D40</color>
<color name="colorAccent">#009688</color>
</resources>

View file

@ -39,4 +39,11 @@
<string name="msg_flash_complete">Firmware geflashed!\n\nOntkoppel de ESP en sluit opnieuw aan om de sleutel in te stellen.</string>
<string name="button_close">Sluiten</string>
<string name="button_cancel">Annuleren</string>
<!-- Theme -->
<string name="section_theme">Weergave</string>
<string name="theme_system">Systeem</string>
<string name="theme_light">Licht</string>
<string name="theme_dark">Donker</string>
<string name="theme_applied">Thema: %s</string>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>