feat(ui): add vertical brightness fills, dark mode, and websocket toggle
All checks were successful
Update Wiki Documentation / generate-docs (push) Successful in 2m26s

This commit is contained in:
Krzysztof Cieślik
2026-06-14 17:51:46 +02:00
parent ff8b94feea
commit 948ad4a425
23 changed files with 678 additions and 238 deletions

View File

@@ -29,15 +29,18 @@ Methodology: Incremental Kanban (Simple .md file)
- [x] Brightness control menu (LightControlDialog)
- [x] Button removal from brightness menu (Auto-save on release)
- [x] Dismiss menu on outside touch
- [x] Vertical brightness-based tile fill for lights
- [x] WebSocket toggle in connection settings
## [PHASE 5] UX Polish & Stability
- [x] Internationalization (PL/EN support)
- [x] Kiosk Mode (Wake Lock - prevent screen sleep)
- [x] Dark Mode support across all screens
- [x] `onDraw` performance optimization (zero allocation in draw loop)
- [x] Stability testing (Monkey Stress Test)
## Future Development
- [ ] WebSocket implementation for real-time updates
- [x] WebSocket implementation for real-time updates
- [ ] Support for `climate` and `media_player` domains
- [ ] RGB color support for lighting

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

View File

@@ -1,15 +1,20 @@
package com.example.retroha
import android.app.Activity
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.*
import com.example.retroha.data.Prefs
import com.example.retroha.i18n.AndroidStrings
import com.example.retroha.i18n.StringKey
import com.example.retroha.network.HaClient
import com.example.retroha.network.HaState
import com.example.retroha.theme.Colors
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
/**
* Activity for configuring the connection to the Home Assistant server.
* Handles server URL, authentication token, and refresh interval settings.
@@ -19,37 +24,47 @@ class ConnectionSettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_connection_settings)
applyTheme()
val etUrl = findViewById<EditText>(R.id.etUrl)
val etToken = findViewById<EditText>(R.id.etToken)
val etRefreshInterval = findViewById<EditText>(R.id.etRefreshInterval)
val cbWebSocket = findViewById<com.example.retroha.ui.BauhausCheckbox>(R.id.cbWebSocket)
val btnSave = findViewById<Button>(R.id.btnTestAndSave)
val tvStatus = findViewById<TextView>(R.id.tvStatus)
etUrl.setText(Prefs.getUrl(this))
etToken.setText(Prefs.getToken(this))
etRefreshInterval.setText((Prefs.getRefreshInterval(this) / 1000).toString())
cbWebSocket.isChecked = Prefs.isWebSocketEnabled(this)
cbWebSocket.setOnClickListener { cbWebSocket.isChecked = !cbWebSocket.isChecked }
btnSave.setOnClickListener {
val url = etUrl.text.toString()
val token = etToken.text.toString()
val intervalSec = etRefreshInterval.text.toString().toLongOrNull() ?: 30L
val wsEnabled = cbWebSocket.isChecked
val stringsProvider = AndroidStrings(this)
android.app.AlertDialog.Builder(this)
.setTitle(stringsProvider.get(StringKey.DIALOG_WARNING))
.setMessage(stringsProvider.get(StringKey.CONFIRM_CHANGE_CONN))
.setPositiveButton(stringsProvider.get(StringKey.DIALOG_YES_CHANGE)) { _, _ ->
performTestAndSave(url, token, intervalSec, tvStatus)
performTestAndSave(url, token, intervalSec, wsEnabled, tvStatus)
}
.setNegativeButton(stringsProvider.get(StringKey.DIALOG_CANCEL), null)
.show()
}
}
/**
* Attempts to connect to HA with the provided credentials.
* If successful, saves the configuration and clears the widget cache.
*/
private fun performTestAndSave(url: String, token: String, intervalSec: Long, tvStatus: TextView) {
private fun performTestAndSave(url: String, token: String, intervalSec: Long, wsEnabled: Boolean, tvStatus: TextView) {
val stringsProvider = AndroidStrings(this)
tvStatus.text = stringsProvider.get(StringKey.STATUS_CONNECTING)
tvStatus.setTextColor(0xFF000000.toInt())
tvStatus.setTextColor(Colors.getTextColor(this))
val testClient = HaClient.getServiceForTest(url, token)
testClient.getStates().enqueue(object : Callback<List<HaState>> {
override fun onResponse(call: Call<List<HaState>>, response: Response<List<HaState>>) {
@@ -57,27 +72,52 @@ class ConnectionSettingsActivity : BaseActivity() {
Prefs.setUrl(this@ConnectionSettingsActivity, url)
Prefs.setToken(this@ConnectionSettingsActivity, token)
Prefs.setRefreshInterval(this@ConnectionSettingsActivity, intervalSec * 1000L)
Prefs.setWebSocketEnabled(this@ConnectionSettingsActivity, wsEnabled)
Prefs.setSelectedEntities(this@ConnectionSettingsActivity, emptySet())
HaClient.clearCache()
runOnUiThread {
tvStatus.text = stringsProvider.get(StringKey.STATUS_SUCCESS)
tvStatus.setTextColor(0xFF0056B3.toInt())
tvStatus.setTextColor(if (Prefs.isDarkMode(this@ConnectionSettingsActivity)) Colors.YELLOW else Colors.BLUE)
Toast.makeText(this@ConnectionSettingsActivity, stringsProvider.get(StringKey.TOAST_SAVED_CLEARED), Toast.LENGTH_SHORT).show()
finish()
}
} else {
runOnUiThread {
tvStatus.text = "${stringsProvider.get(StringKey.STATUS_ERROR)}: ${response.code()}"
tvStatus.setTextColor(0xFFE23A24.toInt())
tvStatus.setTextColor(Colors.RED)
}
}
}
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
runOnUiThread {
tvStatus.text = "${stringsProvider.get(StringKey.STATUS_ERROR_NETWORK)}: ${t.message}"
tvStatus.setTextColor(0xFFE23A24.toInt())
tvStatus.setTextColor(Colors.RED)
}
}
})
}
private fun applyTheme() {
val root = findViewById<View>(android.R.id.content)
root.setBackgroundColor(Colors.getBgColor(this))
val textColor = Colors.getTextColor(this)
val borderColor = Colors.getBorderColor(this)
findViewById<TextView>(R.id.tvConnectionTitle).setTextColor(textColor)
findViewById<TextView>(R.id.labelUrl).setTextColor(textColor)
findViewById<TextView>(R.id.labelToken).setTextColor(textColor)
findViewById<TextView>(R.id.labelRefresh).setTextColor(textColor)
findViewById<TextView>(R.id.labelWebSocket).setTextColor(textColor)
findViewById<TextView>(R.id.tvStatus).setTextColor(textColor)
findViewById<EditText>(R.id.etUrl).setTextColor(textColor)
findViewById<EditText>(R.id.etToken).setTextColor(textColor)
findViewById<EditText>(R.id.etRefreshInterval).setTextColor(textColor)
findViewById<View>(R.id.vBorderTitle).setBackgroundColor(borderColor)
findViewById<View>(R.id.vBorderUrl).setBackgroundColor(borderColor)
findViewById<View>(R.id.vBorderToken).setBackgroundColor(borderColor)
findViewById<View>(R.id.vBorderRefresh).setBackgroundColor(borderColor)
}
}

View File

@@ -1,17 +1,21 @@
package com.example.retroha
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.*
import com.example.retroha.data.Prefs
import com.example.retroha.i18n.AndroidStrings
import com.example.retroha.i18n.StringKey
import com.example.retroha.network.HaClient
import com.example.retroha.network.HaState
import com.example.retroha.theme.Colors
import com.example.retroha.ui.EntitySelectionAdapter
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
/**
* Activity for browsing and selecting Home Assistant entities to be displayed as widgets.
* Features real-time filtering and persistent selection storage.
@@ -22,51 +26,62 @@ class EntitySelectionActivity : BaseActivity() {
private var allEntities = mutableListOf<HaState>()
private var filteredEntities = mutableListOf<HaState>()
private var selectedEntities = mutableSetOf<String>()
private var adapter: EntitySelectionAdapter? = null
private lateinit var adapter: EntitySelectionAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entity_selection)
applyTheme()
val stringsProvider = AndroidStrings(this)
etSearch = findViewById(R.id.etSearch)
lvEntities = findViewById(R.id.lvEntities)
val btnSave = findViewById<Button>(R.id.btnSave)
selectedEntities.addAll(Prefs.getSelectedEntities(this))
btnSave.setOnClickListener {
Prefs.setSelectedEntities(this, selectedEntities)
Toast.makeText(this, stringsProvider.get(StringKey.TOAST_ENTITIES_SAVED), Toast.LENGTH_SHORT).show()
finish()
}
etSearch.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) { filterEntities(s.toString()) }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
lvEntities.onItemClickListener = AdapterView.OnItemClickListener { _, view, position, _ ->
val entityId = filteredEntities[position].entity_id
if (selectedEntities.contains(entityId)) {
selectedEntities.remove(entityId)
lvEntities.setOnItemClickListener { _, view, position, _ ->
val item = filteredEntities[position]
val checkbox = view.findViewById<com.example.retroha.ui.BauhausCheckbox>(R.id.cbEntity)
if (selectedEntities.contains(item.entity_id)) {
selectedEntities.remove(item.entity_id)
checkbox.isChecked = false
} else {
selectedEntities.add(entityId)
selectedEntities.add(item.entity_id)
checkbox.isChecked = true
}
val layout = view as? LinearLayout
val checkbox = layout?.getChildAt(0) as? com.example.retroha.ui.BauhausCheckbox
checkbox?.isChecked = selectedEntities.contains(entityId)
}
fetchEntities()
}
/**
* Fetches the complete list of available entities from the HA server.
*/
private fun fetchEntities() {
val url = Prefs.getUrl(this)
val token = Prefs.getToken(this)
if (token.isEmpty()) return
HaClient.getService(this).getStates().enqueue(object : Callback<List<HaState>> {
override fun onResponse(call: Call<List<HaState>>, response: Response<List<HaState>>) {
if (response.isSuccessful) {
val list = response.body() ?: emptyList()
allEntities.clear()
allEntities.addAll(response.body() ?: emptyList())
allEntities.sortBy { it.entity_id }
runOnUiThread { filterEntities(etSearch.text.toString()) }
allEntities.addAll(list)
filterEntities("")
} else {
val stringsProvider = AndroidStrings(this@EntitySelectionActivity)
runOnUiThread { Toast.makeText(this@EntitySelectionActivity, "${stringsProvider.get(StringKey.STATUS_ERROR_HA)}: ${response.code()}", Toast.LENGTH_SHORT).show() }
@@ -80,6 +95,7 @@ class EntitySelectionActivity : BaseActivity() {
}
})
}
/**
* Filters the [allEntities] list based on the user's search query.
*/
@@ -89,15 +105,37 @@ class EntitySelectionActivity : BaseActivity() {
filteredEntities.addAll(allEntities)
} else {
val q = query.lowercase()
allEntities.filterTo(filteredEntities) {
it.entity_id.lowercase().contains(q) ||
(it.attributes.friendly_name?.lowercase()?.contains(q) == true)
allEntities.forEach { item ->
if (item.entity_id.lowercase().contains(q) ||
item.attributes.friendly_name?.lowercase()?.contains(q) == true) {
filteredEntities.add(item)
}
}
}
updateList()
}
private fun updateList() {
adapter = EntitySelectionAdapter(this, filteredEntities, selectedEntities)
lvEntities.adapter = adapter
}
private fun applyTheme() {
val root = findViewById<View>(R.id.rootLayout)
root.setBackgroundColor(Colors.getBgColor(this))
val textColor = Colors.getTextColor(this)
val borderColor = Colors.getBorderColor(this)
findViewById<TextView>(R.id.tvSelectionTitle).setTextColor(textColor)
findViewById<EditText>(R.id.etSearch).setTextColor(textColor)
findViewById<EditText>(R.id.etSearch).setHintTextColor(Colors.GRAY_MID)
findViewById<View>(R.id.vBorderTitle).setBackgroundColor(borderColor)
findViewById<View>(R.id.vBorderSearch).setBackgroundColor(borderColor)
lvEntities = findViewById(R.id.lvEntities)
lvEntities.divider = android.graphics.drawable.ColorDrawable(borderColor)
lvEntities.dividerHeight = (1 * resources.displayMetrics.density).toInt()
}
}

View File

@@ -1,5 +1,9 @@
package com.example.retroha
import android.os.Bundle
import android.view.View
import android.widget.TextView
import com.example.retroha.theme.Colors
/**
* Activity for displaying the user manual and basic operation instructions.
*/
@@ -7,5 +11,24 @@ class InstructionsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_instructions)
applyTheme()
}
private fun applyTheme() {
val root = findViewById<View>(R.id.scrollView)
root.setBackgroundColor(Colors.getBgColor(this))
val textColor = Colors.getTextColor(this)
val borderColor = Colors.getBorderColor(this)
findViewById<TextView>(R.id.tvInstructionsTitle).setTextColor(textColor)
findViewById<TextView>(R.id.tvHeader1).setTextColor(textColor)
findViewById<TextView>(R.id.tvBody1).setTextColor(textColor)
findViewById<TextView>(R.id.tvHeader2).setTextColor(textColor)
findViewById<TextView>(R.id.tvBody2).setTextColor(textColor)
findViewById<TextView>(R.id.tvHeader3).setTextColor(textColor)
findViewById<TextView>(R.id.tvBody3).setTextColor(textColor)
findViewById<View>(R.id.vBorderTitle).setBackgroundColor(borderColor)
}
}

View File

@@ -32,7 +32,7 @@ class LanguageActivity : BaseActivity() {
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
setBackgroundColor(Colors.WHITE)
setBackgroundColor(Colors.getBgColor(this@LanguageActivity))
setPadding(dp(32), dp(32), dp(32), dp(32))
}
val stringsProvider = AndroidStrings(this)
@@ -40,7 +40,7 @@ class LanguageActivity : BaseActivity() {
text = stringsProvider.get(StringKey.TITLE_SELECT_LANGUAGE)
typeface = android.graphics.Typeface.MONOSPACE
textSize = 18f
setTextColor(Colors.BLACK)
setTextColor(Colors.getTextColor(this@LanguageActivity))
gravity = Gravity.CENTER
setPadding(0, 0, 0, dp(64))
}

View File

@@ -1,4 +1,5 @@
package com.example.retroha
import android.app.Activity
import android.os.Bundle
import android.os.Handler
@@ -21,6 +22,8 @@ import com.example.retroha.network.HaClient
import com.example.retroha.network.HaState
import com.example.retroha.network.ToggleRequest
import com.example.retroha.network.HaWebSocketManager
import com.example.retroha.ui.BauhausControlOverlay
import com.example.retroha.theme.Colors
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@@ -31,6 +34,7 @@ import android.view.HapticFeedbackConstants
import android.view.WindowManager
import android.graphics.Color
import android.view.Gravity
/**
* The primary dashboard activity.
* Displays a responsive grid of Home Assistant widgets and handles real-time updates.
@@ -46,6 +50,7 @@ class MainActivity : BaseActivity() {
private lateinit var tvStatusIndicator: TextView
private lateinit var tabContainer: LinearLayout
private var webSocketManager: HaWebSocketManager? = null
private lateinit var controlOverlay: BauhausControlOverlay
private val refreshRunnable = object : Runnable {
override fun run() {
@@ -54,13 +59,19 @@ class MainActivity : BaseActivity() {
mainHandler.postDelayed(this, interval)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
applyTheme()
stringsProvider = AndroidStrings(this)
tvStatusIndicator = findViewById(R.id.tvStatusIndicator)
tabContainer = findViewById(R.id.tabContainer)
controlOverlay = findViewById(R.id.controlOverlay)
val gridView = findViewById<GridView>(R.id.gridView)
gridView.numColumns = resolveColumns()
adapter = WidgetAdapter(this, displayedEntities)
@@ -73,27 +84,48 @@ class MainActivity : BaseActivity() {
handleLongToggle(cfg)
}
gridView.adapter = adapter
findViewById<android.view.View>(R.id.btnSettings).setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java))
}
findViewById<TextView>(R.id.btnSettings).apply {
val bg = android.graphics.drawable.GradientDrawable().apply {
shape = android.graphics.drawable.GradientDrawable.RECTANGLE
setColor(com.example.retroha.theme.Colors.BLUE)
setStroke(dp(2), com.example.retroha.theme.Colors.BLACK)
setColor(Colors.BLUE)
setStroke(dp(2), Colors.BLACK)
}
background = bg
}
findViewById<android.view.View>(R.id.tvTitle).setOnClickListener {
fetchHaStates()
Toast.makeText(this, stringsProvider.get(StringKey.STATUS_REFRESHING), Toast.LENGTH_SHORT).show()
}
setupTabs()
webSocketManager = HaWebSocketManager(this) { haState ->
handleStateUpdate(haState)
}
}
private fun applyTheme() {
val root = findViewById<View>(R.id.mainLayout)
root.setBackgroundColor(Colors.getBgColor(this))
val textColor = Colors.getTextColor(this)
val borderColor = Colors.getBorderColor(this)
findViewById<TextView>(R.id.tvTitle).setTextColor(textColor)
findViewById<View>(R.id.topBar).setBackgroundColor(Colors.getBgColor(this))
findViewById<View>(R.id.tabScrollView).setBackgroundColor(Colors.getBgColor(this))
findViewById<View>(R.id.vTopBarBorder).setBackgroundColor(borderColor)
findViewById<View>(R.id.gridView).setBackgroundColor(Colors.getBgColor(this))
findViewById<TextView>(R.id.btnSettingsShadow).setTextColor(Colors.BLACK) // Shadow is always black
}
/**
* Initializes the category filtering tabs.
* Tabs are dynamically generated based on supported domains and current selection.
@@ -118,7 +150,7 @@ class MainActivity : BaseActivity() {
}
val shadow = View(this).apply {
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
setBackgroundColor(com.example.retroha.theme.Colors.BLACK)
setBackgroundColor(Colors.getShadowColor(this@MainActivity))
translationX = dp(2).toFloat()
translationY = dp(2).toFloat()
}
@@ -131,11 +163,11 @@ class MainActivity : BaseActivity() {
gravity = Gravity.CENTER
val bgDrawable = android.graphics.drawable.GradientDrawable().apply {
shape = android.graphics.drawable.GradientDrawable.RECTANGLE
setColor(if (isSelected) com.example.retroha.theme.Colors.YELLOW else com.example.retroha.theme.Colors.WHITE)
setStroke(dp(2), com.example.retroha.theme.Colors.BLACK)
setColor(if (isSelected) Colors.YELLOW else Colors.getStatusOff(this@MainActivity))
setStroke(dp(2), Colors.getBorderColor(this@MainActivity))
}
background = bgDrawable
setTextColor(com.example.retroha.theme.Colors.BLACK)
setTextColor(Colors.getTextColor(this@MainActivity))
setOnClickListener {
if (currentCategory == cat) return@setOnClickListener
currentCategory = cat
@@ -150,7 +182,9 @@ class MainActivity : BaseActivity() {
}
}
}
private fun dp(v: Int) = (v * resources.displayMetrics.density + 0.5f).toInt()
/**
* Filters the list of all entities based on the [currentCategory] and updates the grid.
*/
@@ -167,20 +201,29 @@ class MainActivity : BaseActivity() {
else -> allEntities
}
runOnUiThread {
adapter.updateItems(filtered)
displayedEntities.clear()
displayedEntities.addAll(filtered)
adapter.notifyDataSetChanged()
}
}
override fun onResume() {
super.onResume()
applyTheme()
setupTabs()
mainHandler.removeCallbacks(refreshRunnable)
mainHandler.post(refreshRunnable)
if (Prefs.isWebSocketEnabled(this)) {
webSocketManager?.connect()
}
}
override fun onPause() {
super.onPause()
mainHandler.removeCallbacks(refreshRunnable)
webSocketManager?.disconnect()
}
/**
* Fetches the latest states from Home Assistant and updates the UI.
* Handles authentication errors and network failures.
@@ -189,7 +232,7 @@ class MainActivity : BaseActivity() {
val token = Prefs.getToken(this)
if (token.isEmpty()) {
tvStatusIndicator.text = stringsProvider.get(StringKey.STATUS_NO_TOKEN)
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.RED)
tvStatusIndicator.setTextColor(Colors.RED)
return
}
HaClient.getService(this).getStates().enqueue(object : Callback<List<HaState>> {
@@ -199,23 +242,24 @@ class MainActivity : BaseActivity() {
updateEntities(states)
runOnUiThread {
tvStatusIndicator.text = stringsProvider.get(StringKey.STATUS_CONNECTED)
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.BLUE)
tvStatusIndicator.setTextColor(if (Prefs.isDarkMode(this@MainActivity)) Colors.YELLOW else Colors.BLUE)
}
} else {
runOnUiThread {
tvStatusIndicator.text = "${stringsProvider.get(StringKey.STATUS_ERROR_HA)}: ${response.code()}"
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.RED)
tvStatusIndicator.setTextColor(Colors.RED)
}
}
}
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
runOnUiThread {
tvStatusIndicator.text = stringsProvider.get(StringKey.STATUS_OFFLINE)
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.RED)
tvStatusIndicator.setTextColor(Colors.RED)
}
}
})
}
/**
* Handles a single entity state update from WebSocket.
*/
@@ -248,10 +292,8 @@ class MainActivity : BaseActivity() {
} else {
allEntities.add(updatedConfig)
}
runOnUiThread {
filterEntities()
}
}
/**
* Updates the local [allEntities] list with new data from HA.
@@ -281,21 +323,17 @@ class MainActivity : BaseActivity() {
brightness = ha.attributes.brightness
))
}
runOnUiThread {
filterEntities()
}
}
private fun handleLongToggle(cfg: WidgetConfig) {
// Light control is now available even if state is OFF
if (cfg.domain != "light") return
com.example.retroha.ui.LightControlDialog(
this,
cfg.label,
cfg.brightness ?: 0,
onBrightnessChanged = { brightness ->
controlOverlay.showBrightness(cfg.label, cfg.brightness ?: 0) { brightness ->
setLightBrightness(cfg.entityId, brightness)
}
).show()
}
private fun setLightBrightness(entityId: String, brightness: Int) {
HaClient.getService(this).setBrightness(com.example.retroha.network.BrightnessRequest(entityId, brightness))
.enqueue(object : Callback<List<HaState>> {
@@ -305,6 +343,7 @@ class MainActivity : BaseActivity() {
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {}
})
}
private fun handleToggle(cfg: WidgetConfig) {
val idx = allEntities.indexOfFirst { it.entityId == cfg.entityId }
if (idx < 0) return
@@ -314,6 +353,7 @@ class MainActivity : BaseActivity() {
WidgetInteraction.READ_ONLY -> Unit
}
}
private fun doToggle(idx: Int, cfg: WidgetConfig) {
val domain = cfg.entityId.split(".")[0]
if (idx >= 0 && idx < allEntities.size) {
@@ -336,6 +376,7 @@ class MainActivity : BaseActivity() {
}
})
}
private fun doExecute(idx: Int, cfg: WidgetConfig) {
val domain = cfg.entityId.split(".")[0]
if (idx >= 0 && idx < allEntities.size) {
@@ -358,12 +399,22 @@ class MainActivity : BaseActivity() {
}
})
}
override fun onBackPressed() {
if (controlOverlay.visibility == View.VISIBLE) {
controlOverlay.visibility = View.GONE
} else {
super.onBackPressed()
}
}
override fun onDestroy() {
super.onDestroy()
mainHandler.removeCallbacks(refreshRunnable)
mainHandler.removeCallbacksAndMessages(null)
webSocketManager?.disconnect()
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
@@ -374,6 +425,7 @@ class MainActivity : BaseActivity() {
adapter.notifyDataSetChanged()
}
}
/**
* Calculates the optimal number of columns based on screen width and orientation.
*/

View File

@@ -1,11 +1,16 @@
package com.example.retroha
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import com.example.retroha.data.Prefs
import com.example.retroha.i18n.AndroidStrings
import com.example.retroha.i18n.StringKey
import com.example.retroha.theme.Colors
/**
* Main settings menu activity.
* Provides navigation to connection configuration, entity selection,
@@ -15,6 +20,9 @@ class SettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
applyTheme()
findViewById<Button>(R.id.btnEntitySelection).setOnClickListener {
startActivity(Intent(this, EntitySelectionActivity::class.java))
}
@@ -29,6 +37,16 @@ class SettingsActivity : BaseActivity() {
intent.putExtra("from_settings", true)
startActivity(intent)
}
val cbDarkMode = findViewById<com.example.retroha.ui.BauhausCheckbox>(R.id.cbDarkMode)
cbDarkMode.isChecked = Prefs.isDarkMode(this)
cbDarkMode.setOnClickListener {
val newState = !cbDarkMode.isChecked
cbDarkMode.isChecked = newState
Prefs.setDarkMode(this, newState)
applyTheme() // Immediate visual feedback
}
findViewById<Button>(R.id.btnDeleteAll).setOnClickListener {
val stringsProvider = AndroidStrings(this)
android.app.AlertDialog.Builder(this)
@@ -42,4 +60,13 @@ class SettingsActivity : BaseActivity() {
.show()
}
}
private fun applyTheme() {
val root = findViewById<View>(android.R.id.content)
root.setBackgroundColor(Colors.getBgColor(this))
val textColor = Colors.getTextColor(this)
findViewById<TextView>(R.id.tvSettingsTitle).setTextColor(textColor)
findViewById<TextView>(R.id.labelDarkMode).setTextColor(textColor)
}
}

View File

@@ -13,6 +13,8 @@ object Prefs {
private const val KEY_SELECTED_ENTITIES = "selected_entities"
private const val KEY_REFRESH_INTERVAL = "refresh_interval"
private const val KEY_LANGUAGE = "app_language"
private const val KEY_WEBSOCKET_ENABLED = "websocket_enabled"
private const val KEY_DARK_MODE = "dark_mode"
private fun getPrefs(context: Context): SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
@@ -43,4 +45,14 @@ object Prefs {
/** Saves the set of selected HA entity IDs. */
fun setSelectedEntities(context: Context, entities: Set<String>) =
getPrefs(context).edit().putStringSet(KEY_SELECTED_ENTITIES, entities).apply()
/** Checks if WebSocket real-time updates are enabled. Defaults to true. */
fun isWebSocketEnabled(context: Context): Boolean = getPrefs(context).getBoolean(KEY_WEBSOCKET_ENABLED, true)
/** Sets the WebSocket enabled status. */
fun setWebSocketEnabled(context: Context, enabled: Boolean) = getPrefs(context).edit().putBoolean(KEY_WEBSOCKET_ENABLED, enabled).apply()
/** Checks if Dark Mode is enabled. Defaults to false. */
fun isDarkMode(context: Context): Boolean = getPrefs(context).getBoolean(KEY_DARK_MODE, false)
/** Sets the Dark Mode status. */
fun setDarkMode(context: Context, enabled: Boolean) = getPrefs(context).edit().putBoolean(KEY_DARK_MODE, enabled).apply()
}

View File

@@ -34,6 +34,8 @@ class AndroidStrings(private val context: Context) : Strings {
StringKey.LABEL_URL -> R.string.label_url
StringKey.LABEL_TOKEN -> R.string.label_token
StringKey.LABEL_REFRESH -> R.string.label_refresh
StringKey.LABEL_WEBSOCKET_ENABLE -> R.string.label_websocket_enable
StringKey.LABEL_DARK_MODE -> R.string.label_dark_mode
StringKey.BTN_TEST_SAVE -> R.string.btn_test_save
StringKey.BTN_DELETE_ALL -> R.string.btn_delete_all
StringKey.BTN_SAVE_SELECTED -> R.string.btn_save_selected

View File

@@ -0,0 +1,110 @@
package com.example.retroha.ui
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.SeekBar
import android.widget.TextView
import com.example.retroha.theme.Colors
/**
* A full-screen overlay view that provides entity controls (like brightness).
* Replaces Dialogs to avoid window surface bugs (EGL_BAD_MATCH) on old hardware.
*/
class BauhausControlOverlay @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val density = resources.displayMetrics.density
private fun dp(v: Int) = (v * density + 0.5f).toInt()
private var onBrightnessChanged: ((Int) -> Unit)? = null
init {
// Semi-transparent background
setBackgroundColor(0xAA000000.toInt())
visibility = View.GONE
// Close on click outside the center box
setOnClickListener {
visibility = View.GONE
}
}
/**
* Shows the brightness control UI for a specific entity.
*/
fun showBrightness(entityName: String, initialBrightness: Int, callback: (Int) -> Unit) {
this.onBrightnessChanged = callback
removeAllViews()
val bgColor = Colors.getBgColor(context)
val textColor = Colors.getTextColor(context)
val borderColor = Colors.getBorderColor(context)
val root = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(bgColor)
setPadding(dp(24), dp(24), dp(24), dp(24))
layoutParams = LayoutParams(dp(300), ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)
// Prevent click events from reaching the overlay's background click listener
setOnClickListener { /* Consume */ }
}
// Title
root.addView(TextView(context).apply {
text = entityName.uppercase()
typeface = Typeface.MONOSPACE
textSize = 16f
setTextColor(textColor)
setPadding(0, 0, 0, dp(16))
})
// Border line
root.addView(View(context).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(2))
setBackgroundColor(borderColor)
})
val brightnessLabel = context.getString(com.example.retroha.R.string.dialog_brightness)
val tvBrightness = TextView(context).apply {
text = "$brightnessLabel: ${(initialBrightness * 100 / 255)}%"
typeface = Typeface.MONOSPACE
textSize = 14f
setTextColor(textColor)
setPadding(0, dp(24), 0, dp(8))
}
root.addView(tvBrightness)
// SeekBar
val seekBar = SeekBar(context).apply {
max = 255
progress = initialBrightness
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(s: SeekBar?, p: Int, fromUser: Boolean) {
tvBrightness.text = "$brightnessLabel: ${(p * 100 / 255)}%"
}
override fun onStartTrackingTouch(s: SeekBar?) {}
override fun onStopTrackingTouch(s: SeekBar?) {
onBrightnessChanged?.invoke(progress)
}
})
}
root.addView(seekBar)
// Padding
root.addView(View(context).apply {
layoutParams = LinearLayout.LayoutParams(1, dp(16))
})
addView(root)
visibility = View.VISIBLE
}
}

View File

@@ -29,10 +29,16 @@ class EntitySelectionAdapter(
val textContainer = layout.getChildAt(1) as LinearLayout
val tvName = textContainer.getChildAt(0) as TextView
val tvId = textContainer.getChildAt(1) as TextView
val item = items[position]
checkbox.isChecked = selectedEntities.contains(item.entity_id)
tvName.text = item.attributes.friendly_name ?: item.entity_id
tvId.text = item.entity_id
val textColor = Colors.getTextColor(context)
tvName.setTextColor(textColor)
tvId.setTextColor(Colors.GRAY_MID)
return layout
}
private fun createLayout(): LinearLayout {

View File

@@ -31,17 +31,20 @@ class LightControlDialog(
private val entityName: String,
private val initialBrightness: Int,
private val onBrightnessChanged: (Int) -> Unit
) : Dialog(context) {
) : Dialog(context, android.R.style.Theme_Holo_Light_Dialog_NoActionBar) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
// Disable hardware acceleration for this dialog window to avoid EGL errors on old hardware
window?.setFlags(
android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
// Force OPAQUE format and disable hardware acceleration to fix EGL_BAD_MATCH on old drivers
window?.let { win ->
win.setFormat(android.graphics.PixelFormat.RGB_565)
win.setFlags(
0, // Remove all flags that might trigger HW acceleration bugs
android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
)
win.setBackgroundDrawable(ColorDrawable(Colors.WHITE))
}
val density = context.resources.displayMetrics.density
fun dp(v: Int) = (v * density + 0.5f).toInt()
@@ -50,7 +53,10 @@ class LightControlDialog(
orientation = LinearLayout.VERTICAL
setBackgroundColor(Colors.WHITE)
setPadding(dp(24), dp(24), dp(24), dp(24))
layoutParams = LinearLayout.LayoutParams(dp(320), ViewGroup.LayoutParams.WRAP_CONTENT)
layoutParams = LinearLayout.LayoutParams(dp(300), ViewGroup.LayoutParams.WRAP_CONTENT)
// Force software rendering for the content of the dialog
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
}
root.addView(TextView(context).apply {

View File

@@ -39,13 +39,13 @@ class WidgetCardView(context: Context) : View(context) {
private val execIconPath = Path()
private val paintFill = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
private val tpLabel = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = Colors.GRAY_MID; textSize = sp(11); typeface = Fonts.REGULAR
typeface = Fonts.REGULAR
}
private val tpValue = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = Colors.BLACK; textSize = sp(18); typeface = Fonts.BOLD
typeface = Fonts.BOLD
}
private val tpSecondary = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = Colors.GRAY_MID; textSize = sp(10); typeface = Fonts.REGULAR
typeface = Fonts.REGULAR
}
private val pulseInterpolator = DecelerateInterpolator()
@@ -99,8 +99,11 @@ class WidgetCardView(context: Context) : View(context) {
fun bind(cfg: WidgetConfig) {
config = cfg
currentInteraction = cfg.domain.toWidgetInteraction()
isClickable = currentInteraction != WidgetInteraction.READ_ONLY
&& cfg.state != EntityState.UNAVAILABLE
// Ensure interactive widgets are clickable even when OFF.
// Only UNAVAILABLE state blocks interaction.
isClickable = cfg.state != EntityState.UNAVAILABLE && currentInteraction != WidgetInteraction.READ_ONLY
isLongClickable = cfg.domain == "light" && cfg.state != EntityState.UNAVAILABLE
if (cfg.state == EntityState.TOGGLING) {
startPulse()
@@ -136,17 +139,21 @@ class WidgetCardView(context: Context) : View(context) {
val labelUp = if (labelChanged) cfg.label.uppercase() else cachedLabel
// For sensors/numbers, append unit to value to prevent line wrapping
val valueText = if (cfg.secondary.length <= 3 && cfg.secondary.isNotEmpty()) {
"${cfg.value} ${cfg.secondary}"
// Use non-breaking space to keep value and unit together
val valueText = if (cfg.secondary.length <= 4 && cfg.secondary.isNotEmpty()) {
"${cfg.value}\u00A0${cfg.secondary}"
} else {
cfg.value
}
val secondaryText = if (cfg.secondary.length <= 3) "" else cfg.secondary
val secondaryText = if (cfg.secondary.length <= 4) "" else cfg.secondary
lLabel = makeLayout(labelUp, tpLabel, textW)
lValue = makeLayout(valueText, tpValue, textW)
// Forced single line: manually ellipsize before making layout
val truncatedValue = TextUtils.ellipsize(valueText, tpValue, textW.toFloat(), TextUtils.TruncateAt.END)
lValue = makeLayout(truncatedValue.toString(), tpValue, textW)
lSecondary = makeLayout(secondaryText, tpSecondary, textW)
cachedLabel = cfg.label
@@ -162,27 +169,50 @@ class WidgetCardView(context: Context) : View(context) {
val b = borderPx.toFloat()
val s = shadowPx.toFloat()
// Update paint colors based on theme
tpLabel.color = Colors.GRAY_MID
tpValue.color = Colors.getTextColor(context)
tpSecondary.color = Colors.GRAY_MID
tpLabel.textSize = sp(11)
tpValue.textSize = sp(18)
tpSecondary.textSize = sp(10)
// Shadow
paintFill.color = Colors.BLACK
paintFill.color = Colors.getShadowColor(context)
canvas.drawRect(s, s, width.toFloat(), cardH + s, paintFill)
// Border
paintFill.color = when (cfg.state) {
EntityState.TOGGLING -> Colors.BORDER_TOGGLING
EntityState.UNAVAILABLE -> Colors.BORDER_UNAVAILABLE
else -> Colors.BORDER_DEFAULT
EntityState.TOGGLING -> Colors.BLUE
EntityState.UNAVAILABLE -> Colors.GRAY_MID
else -> Colors.getBorderColor(context)
}
canvas.drawRect(0f, 0f, cardW, cardH, paintFill)
// Background
paintFill.color = when (cfg.state) {
EntityState.ON -> Colors.STATUS_ON
EntityState.OFF -> Colors.STATUS_OFF
val baseBgColor = when (cfg.state) {
EntityState.UNAVAILABLE -> Colors.STATUS_UNAVAILABLE
EntityState.TOGGLING -> Colors.STATUS_TOGGLING
else -> Colors.getStatusOff(context)
}
paintFill.color = baseBgColor
canvas.drawRect(b, b, cardW - b, cardH - b, paintFill)
// Brightness fill for lights that are ON
if (cfg.domain == "light" && cfg.state == EntityState.ON) {
paintFill.color = Colors.STATUS_ON
val brightness = cfg.brightness ?: 255
val fillPercent = brightness / 255f
val fillTop = cardH - b - ((cardH - 2 * b) * fillPercent)
canvas.drawRect(b, fillTop, cardW - b, cardH - b, paintFill)
} else if (cfg.state == EntityState.ON) {
// Full fill for other ON entities (switches, etc)
paintFill.color = Colors.STATUS_ON
canvas.drawRect(b, b, cardW - b, cardH - b, paintFill)
} else if (cfg.state == EntityState.TOGGLING) {
paintFill.color = Colors.getStatusToggling(context)
canvas.drawRect(b, b, cardW - b, cardH - b, paintFill)
}
// Domain Stripe
paintFill.color = stripeColor(cfg.domain)
canvas.drawRect(b, b, b + stripePx, cardH - b, paintFill)
@@ -228,7 +258,6 @@ class WidgetCardView(context: Context) : View(context) {
private fun startPulse() {
if (pulseAnim?.isRunning == true) return
setLayerType(LAYER_TYPE_HARDWARE, null)
pulseAnim = ObjectAnimator.ofFloat(this, ALPHA, 1f, 0.35f).apply {
duration = 550
repeatMode = ObjectAnimator.REVERSE
@@ -242,7 +271,6 @@ class WidgetCardView(context: Context) : View(context) {
pulseAnim?.cancel()
pulseAnim = null
alpha = 1f
setLayerType(LAYER_TYPE_NONE, null)
}
private fun stripeColor(domain: String): Int = when (domain) {

View File

@@ -7,6 +7,7 @@
android:padding="16dp">
<TextView
android:id="@+id/tvConnectionTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_connection"
@@ -16,6 +17,7 @@
android:textColor="@color/ha_black" />
<View
android:id="@+id/vBorderTitle"
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/ha_black"
@@ -23,6 +25,7 @@
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/labelUrl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_url"
@@ -43,12 +46,14 @@
android:textSize="16sp" />
<View
android:id="@+id/vBorderUrl"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/ha_black"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/labelToken"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_token"
@@ -69,12 +74,14 @@
android:textSize="16sp" />
<View
android:id="@+id/vBorderToken"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/ha_black"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/labelRefresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_refresh"
@@ -95,10 +102,35 @@
android:textSize="16sp" />
<View
android:id="@+id/vBorderRefresh"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/ha_black"
android:layout_marginBottom="24dp" />
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="24dp">
<com.example.retroha.ui.BauhausCheckbox
android:id="@+id/cbWebSocket"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/labelWebSocket"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_websocket_enable"
android:typeface="monospace"
android:textSize="12sp"
android:textColor="@color/ha_black"
android:layout_marginLeft="12dp" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rootLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@@ -7,6 +8,7 @@
android:padding="16dp">
<TextView
android:id="@+id/tvSelectionTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_entity_selection"
@@ -16,6 +18,7 @@
android:textColor="@color/ha_black" />
<View
android:id="@+id/vBorderTitle"
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/ha_black"
@@ -26,20 +29,27 @@
android:id="@+id/etSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Szukaj encji..."
android:inputType="text"
android:background="@null"
android:padding="8dp"
android:hint="Szukaj / Search..."
android:typeface="monospace"
android:textSize="14sp"
android:drawableLeft="@android:drawable/ic_menu_search" />
android:textSize="14sp" />
<View
android:id="@+id/vBorderSearch"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/ha_black"
android:layout_marginBottom="16dp" />
<ListView
android:id="@+id/lvEntities"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="8dp"
android:divider="@color/ha_black"
android:dividerHeight="1dp" />
android:dividerHeight="1dp"
android:listSelector="@android:color/transparent" />
<FrameLayout
android:layout_width="match_parent"

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/ha_white">
@@ -11,6 +12,7 @@
android:padding="16dp">
<TextView
android:id="@+id/tvInstructionsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_instructions"
@@ -21,12 +23,14 @@
android:layout_marginBottom="16dp" />
<View
android:id="@+id/vBorderTitle"
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/ha_black"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/tvHeader1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inst_header_1"
@@ -37,6 +41,7 @@
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvBody1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inst_body_1"
@@ -47,6 +52,7 @@
android:layout_marginBottom="24dp" />
<TextView
android:id="@+id/tvHeader2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inst_header_2"
@@ -57,6 +63,7 @@
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvBody2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inst_body_2"
@@ -67,6 +74,7 @@
android:layout_marginBottom="24dp" />
<TextView
android:id="@+id/tvHeader3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inst_header_3"
@@ -77,6 +85,7 @@
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvBody3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inst_body_3"

View File

@@ -1,5 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mainRoot"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/mainLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@@ -7,12 +13,14 @@
<!-- TOP BAR -->
<LinearLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/ha_white">
<RelativeLayout
android:id="@+id/topBarContent"
android:layout_width="match_parent"
android:layout_height="54dp"
android:paddingLeft="16dp"
@@ -54,6 +62,7 @@
<!-- hard shadow -->
<TextView
android:id="@+id/btnSettingsShadow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_settings"
@@ -89,6 +98,7 @@
<!-- TABS -->
<HorizontalScrollView
android:id="@+id/tabScrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
@@ -106,6 +116,7 @@
<!-- 2dp bottom border -->
<View
android:id="@+id/vTopBarBorder"
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/ha_black" />
@@ -128,4 +139,13 @@
android:scrollbars="vertical"
android:clipToPadding="false" />
</LinearLayout>
</LinearLayout>
<!-- CONTROL OVERLAY (Hidden by default) -->
<com.example.retroha.ui.BauhausControlOverlay
android:id="@+id/controlOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>

View File

@@ -7,6 +7,7 @@
android:padding="16dp">
<TextView
android:id="@+id/tvSettingsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_settings"
@@ -109,6 +110,38 @@
android:textStyle="bold" />
</FrameLayout>
<!-- DARK MODE TOGGLE -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp">
<com.example.retroha.ui.BauhausCheckbox
android:id="@+id/cbDarkMode"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/labelDarkMode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_dark_mode"
android:typeface="monospace"
android:textSize="12sp"
android:textColor="@color/ha_black"
android:layout_marginLeft="12dp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/ha_black"
android:layout_marginBottom="16dp" />
<!-- DELETE ALL BUTTON -->
<FrameLayout
android:layout_width="match_parent"

View File

@@ -23,6 +23,8 @@
<string name="label_url">URL ADDRESS</string>
<string name="label_token">ACCESS TOKEN</string>
<string name="label_refresh">REFRESH (SECONDS)</string>
<string name="label_websocket_enable">ENABLE WEBSOCKET (REAL-TIME)</string>
<string name="label_dark_mode">DARK MODE</string>
<string name="btn_test_save">TEST AND SAVE</string>
<string name="btn_delete_all">DELETE ALL WIDGETS</string>
<string name="btn_save_selected">SAVE SELECTED</string>

View File

@@ -23,6 +23,8 @@
<string name="label_url">ADRES URL</string>
<string name="label_token">TOKEN DOSTĘPU</string>
<string name="label_refresh">ODŚWIEŻANIE (SEKUNDY)</string>
<string name="label_websocket_enable">WŁĄCZ WEBSOCKET (REAL-TIME)</string>
<string name="label_dark_mode">TRYB CIEMNY</string>
<string name="btn_test_save">TESTUJ I ZAPISZ</string>
<string name="btn_delete_all">USUŃ WSZYSTKIE WIDŻETY</string>
<string name="btn_save_selected">ZAPISZ WYBRANE</string>

View File

@@ -49,6 +49,10 @@ enum class StringKey {
LABEL_TOKEN,
/** Input label for refresh interval. */
LABEL_REFRESH,
/** Label for WebSocket toggle. */
LABEL_WEBSOCKET_ENABLE,
/** Label for Dark Mode toggle. */
LABEL_DARK_MODE,
/** Button text for testing and saving connection. */
BTN_TEST_SAVE,
/** Button text for deleting all widgets. */

View File

@@ -1,65 +1,46 @@
package com.example.retroha.theme
import android.content.Context
import com.example.retroha.data.Prefs
/**
* Bauhaus-inspired color palette for the application.
* All colors are defined as ARGB integers.
* Supports both Light and Dark themes.
*/
object Colors {
/** Pure black for borders, shadows, and text. */
// Fixed Bauhaus Colors
const val BLACK = 0xFF000000.toInt()
/** Pure white for backgrounds and default states. */
const val WHITE = 0xFFFFFFFF.toInt()
/** Bauhaus red. Used for scripts and high-priority states. */
const val RED = 0xFFE23A24.toInt()
/** Bauhaus yellow. Used for active (ON) highlights. */
const val YELLOW = 0xFFFAD02C.toInt()
/** Bauhaus blue. Used for switches and interactive elements. */
const val BLUE = 0xFF0056B3.toInt()
/** Bauhaus orange. Used for light-domain entities. */
const val ORANGE = 0xFFF4801A.toInt()
/** Bauhaus green. Secondary status color. */
const val GREEN = 0xFF2D7D46.toInt()
/** Bauhaus violet. Used for sensors and measurements. */
const val VIOLET = 0xFF6B3FA0.toInt()
/** Light gray for background elements. */
const val GRAY_LIGHT = 0xFFCCCCCC.toInt()
/** Mid gray for disabled borders. */
const val GRAY_MID = 0xFF888888.toInt()
/** Dark gray for default category stripes. */
const val GRAY_DARK = 0xFF444444.toInt()
const val DARK_BG = 0xFF1A1A1A.toInt()
const val DARK_CARD = 0xFF2D2D2D.toInt()
// Semantic status colors
/** Card background color when entity is ON. */
// Dynamic semantic colors
fun getBgColor(context: Context): Int = if (Prefs.isDarkMode(context)) DARK_BG else WHITE
fun getTextColor(context: Context): Int = if (Prefs.isDarkMode(context)) WHITE else BLACK
fun getBorderColor(context: Context): Int = if (Prefs.isDarkMode(context)) GRAY_DARK else BLACK
fun getShadowColor(context: Context): Int = if (Prefs.isDarkMode(context)) BLACK else BLACK
// Status colors
const val STATUS_ON = YELLOW
/** Card background color when entity is OFF. */
const val STATUS_OFF = WHITE
/** Card background color when entity is unavailable. */
fun getStatusOff(context: Context): Int = if (Prefs.isDarkMode(context)) DARK_CARD else WHITE
const val STATUS_UNAVAILABLE = GRAY_LIGHT
/** Card background color during state transition. */
const val STATUS_TOGGLING = WHITE
fun getStatusToggling(context: Context): Int = getStatusOff(context)
// Semantic border colors
/** Default card border color. */
const val BORDER_DEFAULT = BLACK
/** Border color during state transition. */
const val BORDER_TOGGLING = BLUE
/** Border color when entity is unavailable. */
const val BORDER_UNAVAILABLE = GRAY_MID
// Domain-specific side stripes
/** Accent stripe for light domain. */
// Domain stripes (remain mostly fixed for Bauhaus style)
const val STRIPE_LIGHT = ORANGE
/** Accent stripe for switch domain. */
const val STRIPE_SWITCH = BLUE
/** Accent stripe for sensor domain. */
const val STRIPE_SENSOR = VIOLET
/** Accent stripe for binary_sensor domain. */
const val STRIPE_BINARY_SENSOR = VIOLET
/** Accent stripe for script domain. */
const val STRIPE_SCRIPT = RED
/** Accent stripe for automation domain. */
const val STRIPE_AUTOMATION = RED
/** Default accent stripe for unknown domains. */
const val STRIPE_DEFAULT = GRAY_DARK
}