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] Brightness control menu (LightControlDialog)
- [x] Button removal from brightness menu (Auto-save on release) - [x] Button removal from brightness menu (Auto-save on release)
- [x] Dismiss menu on outside touch - [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 ## [PHASE 5] UX Polish & Stability
- [x] Internationalization (PL/EN support) - [x] Internationalization (PL/EN support)
- [x] Kiosk Mode (Wake Lock - prevent screen sleep) - [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] `onDraw` performance optimization (zero allocation in draw loop)
- [x] Stability testing (Monkey Stress Test) - [x] Stability testing (Monkey Stress Test)
## Future Development ## Future Development
- [ ] WebSocket implementation for real-time updates - [x] WebSocket implementation for real-time updates
- [ ] Support for `climate` and `media_player` domains - [ ] Support for `climate` and `media_player` domains
- [ ] RGB color support for lighting - [ ] 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 package com.example.retroha
import android.app.Activity import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.* import android.widget.*
import com.example.retroha.data.Prefs import com.example.retroha.data.Prefs
import com.example.retroha.i18n.AndroidStrings import com.example.retroha.i18n.AndroidStrings
import com.example.retroha.i18n.StringKey import com.example.retroha.i18n.StringKey
import com.example.retroha.network.HaClient import com.example.retroha.network.HaClient
import com.example.retroha.network.HaState import com.example.retroha.network.HaState
import com.example.retroha.theme.Colors
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
/** /**
* Activity for configuring the connection to the Home Assistant server. * Activity for configuring the connection to the Home Assistant server.
* Handles server URL, authentication token, and refresh interval settings. * Handles server URL, authentication token, and refresh interval settings.
@@ -19,37 +24,47 @@ class ConnectionSettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_connection_settings) setContentView(R.layout.activity_connection_settings)
applyTheme()
val etUrl = findViewById<EditText>(R.id.etUrl) val etUrl = findViewById<EditText>(R.id.etUrl)
val etToken = findViewById<EditText>(R.id.etToken) val etToken = findViewById<EditText>(R.id.etToken)
val etRefreshInterval = findViewById<EditText>(R.id.etRefreshInterval) 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 btnSave = findViewById<Button>(R.id.btnTestAndSave)
val tvStatus = findViewById<TextView>(R.id.tvStatus) val tvStatus = findViewById<TextView>(R.id.tvStatus)
etUrl.setText(Prefs.getUrl(this)) etUrl.setText(Prefs.getUrl(this))
etToken.setText(Prefs.getToken(this)) etToken.setText(Prefs.getToken(this))
etRefreshInterval.setText((Prefs.getRefreshInterval(this) / 1000).toString()) etRefreshInterval.setText((Prefs.getRefreshInterval(this) / 1000).toString())
cbWebSocket.isChecked = Prefs.isWebSocketEnabled(this)
cbWebSocket.setOnClickListener { cbWebSocket.isChecked = !cbWebSocket.isChecked }
btnSave.setOnClickListener { btnSave.setOnClickListener {
val url = etUrl.text.toString() val url = etUrl.text.toString()
val token = etToken.text.toString() val token = etToken.text.toString()
val intervalSec = etRefreshInterval.text.toString().toLongOrNull() ?: 30L val intervalSec = etRefreshInterval.text.toString().toLongOrNull() ?: 30L
val wsEnabled = cbWebSocket.isChecked
val stringsProvider = AndroidStrings(this) val stringsProvider = AndroidStrings(this)
android.app.AlertDialog.Builder(this) android.app.AlertDialog.Builder(this)
.setTitle(stringsProvider.get(StringKey.DIALOG_WARNING)) .setTitle(stringsProvider.get(StringKey.DIALOG_WARNING))
.setMessage(stringsProvider.get(StringKey.CONFIRM_CHANGE_CONN)) .setMessage(stringsProvider.get(StringKey.CONFIRM_CHANGE_CONN))
.setPositiveButton(stringsProvider.get(StringKey.DIALOG_YES_CHANGE)) { _, _ -> .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) .setNegativeButton(stringsProvider.get(StringKey.DIALOG_CANCEL), null)
.show() .show()
} }
} }
/** /**
* Attempts to connect to HA with the provided credentials. * Attempts to connect to HA with the provided credentials.
* If successful, saves the configuration and clears the widget cache. * 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) val stringsProvider = AndroidStrings(this)
tvStatus.text = stringsProvider.get(StringKey.STATUS_CONNECTING) tvStatus.text = stringsProvider.get(StringKey.STATUS_CONNECTING)
tvStatus.setTextColor(0xFF000000.toInt()) tvStatus.setTextColor(Colors.getTextColor(this))
val testClient = HaClient.getServiceForTest(url, token) val testClient = HaClient.getServiceForTest(url, token)
testClient.getStates().enqueue(object : Callback<List<HaState>> { testClient.getStates().enqueue(object : Callback<List<HaState>> {
override fun onResponse(call: Call<List<HaState>>, response: Response<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.setUrl(this@ConnectionSettingsActivity, url)
Prefs.setToken(this@ConnectionSettingsActivity, token) Prefs.setToken(this@ConnectionSettingsActivity, token)
Prefs.setRefreshInterval(this@ConnectionSettingsActivity, intervalSec * 1000L) Prefs.setRefreshInterval(this@ConnectionSettingsActivity, intervalSec * 1000L)
Prefs.setWebSocketEnabled(this@ConnectionSettingsActivity, wsEnabled)
Prefs.setSelectedEntities(this@ConnectionSettingsActivity, emptySet()) Prefs.setSelectedEntities(this@ConnectionSettingsActivity, emptySet())
HaClient.clearCache() HaClient.clearCache()
runOnUiThread { runOnUiThread {
tvStatus.text = stringsProvider.get(StringKey.STATUS_SUCCESS) 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() Toast.makeText(this@ConnectionSettingsActivity, stringsProvider.get(StringKey.TOAST_SAVED_CLEARED), Toast.LENGTH_SHORT).show()
finish() finish()
} }
} else { } else {
runOnUiThread { runOnUiThread {
tvStatus.text = "${stringsProvider.get(StringKey.STATUS_ERROR)}: ${response.code()}" 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) { override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
runOnUiThread { runOnUiThread {
tvStatus.text = "${stringsProvider.get(StringKey.STATUS_ERROR_NETWORK)}: ${t.message}" 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 package com.example.retroha
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.View
import android.widget.* import android.widget.*
import com.example.retroha.data.Prefs import com.example.retroha.data.Prefs
import com.example.retroha.i18n.AndroidStrings import com.example.retroha.i18n.AndroidStrings
import com.example.retroha.i18n.StringKey import com.example.retroha.i18n.StringKey
import com.example.retroha.network.HaClient import com.example.retroha.network.HaClient
import com.example.retroha.network.HaState import com.example.retroha.network.HaState
import com.example.retroha.theme.Colors
import com.example.retroha.ui.EntitySelectionAdapter import com.example.retroha.ui.EntitySelectionAdapter
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
/** /**
* Activity for browsing and selecting Home Assistant entities to be displayed as widgets. * Activity for browsing and selecting Home Assistant entities to be displayed as widgets.
* Features real-time filtering and persistent selection storage. * Features real-time filtering and persistent selection storage.
@@ -22,51 +26,62 @@ class EntitySelectionActivity : BaseActivity() {
private var allEntities = mutableListOf<HaState>() private var allEntities = mutableListOf<HaState>()
private var filteredEntities = mutableListOf<HaState>() private var filteredEntities = mutableListOf<HaState>()
private var selectedEntities = mutableSetOf<String>() private var selectedEntities = mutableSetOf<String>()
private var adapter: EntitySelectionAdapter? = null private lateinit var adapter: EntitySelectionAdapter
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entity_selection) setContentView(R.layout.activity_entity_selection)
applyTheme()
val stringsProvider = AndroidStrings(this) val stringsProvider = AndroidStrings(this)
etSearch = findViewById(R.id.etSearch) etSearch = findViewById(R.id.etSearch)
lvEntities = findViewById(R.id.lvEntities) lvEntities = findViewById(R.id.lvEntities)
val btnSave = findViewById<Button>(R.id.btnSave) val btnSave = findViewById<Button>(R.id.btnSave)
selectedEntities.addAll(Prefs.getSelectedEntities(this)) selectedEntities.addAll(Prefs.getSelectedEntities(this))
btnSave.setOnClickListener { btnSave.setOnClickListener {
Prefs.setSelectedEntities(this, selectedEntities) Prefs.setSelectedEntities(this, selectedEntities)
Toast.makeText(this, stringsProvider.get(StringKey.TOAST_ENTITIES_SAVED), Toast.LENGTH_SHORT).show() Toast.makeText(this, stringsProvider.get(StringKey.TOAST_ENTITIES_SAVED), Toast.LENGTH_SHORT).show()
finish() finish()
} }
etSearch.addTextChangedListener(object : TextWatcher { etSearch.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) { filterEntities(s.toString()) } override fun afterTextChanged(s: Editable?) { filterEntities(s.toString()) }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
}) })
lvEntities.onItemClickListener = AdapterView.OnItemClickListener { _, view, position, _ ->
val entityId = filteredEntities[position].entity_id lvEntities.setOnItemClickListener { _, view, position, _ ->
if (selectedEntities.contains(entityId)) { val item = filteredEntities[position]
selectedEntities.remove(entityId) 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 { } 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() fetchEntities()
} }
/** /**
* Fetches the complete list of available entities from the HA server. * Fetches the complete list of available entities from the HA server.
*/ */
private fun fetchEntities() { private fun fetchEntities() {
val url = Prefs.getUrl(this)
val token = Prefs.getToken(this) val token = Prefs.getToken(this)
if (token.isEmpty()) return if (token.isEmpty()) return
HaClient.getService(this).getStates().enqueue(object : Callback<List<HaState>> { HaClient.getService(this).getStates().enqueue(object : Callback<List<HaState>> {
override fun onResponse(call: Call<List<HaState>>, response: Response<List<HaState>>) { override fun onResponse(call: Call<List<HaState>>, response: Response<List<HaState>>) {
if (response.isSuccessful) { if (response.isSuccessful) {
val list = response.body() ?: emptyList()
allEntities.clear() allEntities.clear()
allEntities.addAll(response.body() ?: emptyList()) allEntities.addAll(list)
allEntities.sortBy { it.entity_id } filterEntities("")
runOnUiThread { filterEntities(etSearch.text.toString()) }
} else { } else {
val stringsProvider = AndroidStrings(this@EntitySelectionActivity) val stringsProvider = AndroidStrings(this@EntitySelectionActivity)
runOnUiThread { Toast.makeText(this@EntitySelectionActivity, "${stringsProvider.get(StringKey.STATUS_ERROR_HA)}: ${response.code()}", Toast.LENGTH_SHORT).show() } 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. * Filters the [allEntities] list based on the user's search query.
*/ */
@@ -89,15 +105,37 @@ class EntitySelectionActivity : BaseActivity() {
filteredEntities.addAll(allEntities) filteredEntities.addAll(allEntities)
} else { } else {
val q = query.lowercase() val q = query.lowercase()
allEntities.filterTo(filteredEntities) { allEntities.forEach { item ->
it.entity_id.lowercase().contains(q) || if (item.entity_id.lowercase().contains(q) ||
(it.attributes.friendly_name?.lowercase()?.contains(q) == true) item.attributes.friendly_name?.lowercase()?.contains(q) == true) {
filteredEntities.add(item)
}
} }
} }
updateList() updateList()
} }
private fun updateList() { private fun updateList() {
adapter = EntitySelectionAdapter(this, filteredEntities, selectedEntities) adapter = EntitySelectionAdapter(this, filteredEntities, selectedEntities)
lvEntities.adapter = adapter 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 package com.example.retroha
import android.os.Bundle 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. * Activity for displaying the user manual and basic operation instructions.
*/ */
@@ -7,5 +11,24 @@ class InstructionsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_instructions) 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 { val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER gravity = Gravity.CENTER
setBackgroundColor(Colors.WHITE) setBackgroundColor(Colors.getBgColor(this@LanguageActivity))
setPadding(dp(32), dp(32), dp(32), dp(32)) setPadding(dp(32), dp(32), dp(32), dp(32))
} }
val stringsProvider = AndroidStrings(this) val stringsProvider = AndroidStrings(this)
@@ -40,7 +40,7 @@ class LanguageActivity : BaseActivity() {
text = stringsProvider.get(StringKey.TITLE_SELECT_LANGUAGE) text = stringsProvider.get(StringKey.TITLE_SELECT_LANGUAGE)
typeface = android.graphics.Typeface.MONOSPACE typeface = android.graphics.Typeface.MONOSPACE
textSize = 18f textSize = 18f
setTextColor(Colors.BLACK) setTextColor(Colors.getTextColor(this@LanguageActivity))
gravity = Gravity.CENTER gravity = Gravity.CENTER
setPadding(0, 0, 0, dp(64)) setPadding(0, 0, 0, dp(64))
} }

View File

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

View File

@@ -1,11 +1,16 @@
package com.example.retroha package com.example.retroha
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import com.example.retroha.data.Prefs import com.example.retroha.data.Prefs
import com.example.retroha.i18n.AndroidStrings import com.example.retroha.i18n.AndroidStrings
import com.example.retroha.i18n.StringKey import com.example.retroha.i18n.StringKey
import com.example.retroha.theme.Colors
/** /**
* Main settings menu activity. * Main settings menu activity.
* Provides navigation to connection configuration, entity selection, * Provides navigation to connection configuration, entity selection,
@@ -15,6 +20,9 @@ class SettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings) setContentView(R.layout.activity_settings)
applyTheme()
findViewById<Button>(R.id.btnEntitySelection).setOnClickListener { findViewById<Button>(R.id.btnEntitySelection).setOnClickListener {
startActivity(Intent(this, EntitySelectionActivity::class.java)) startActivity(Intent(this, EntitySelectionActivity::class.java))
} }
@@ -29,6 +37,16 @@ class SettingsActivity : BaseActivity() {
intent.putExtra("from_settings", true) intent.putExtra("from_settings", true)
startActivity(intent) 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 { findViewById<Button>(R.id.btnDeleteAll).setOnClickListener {
val stringsProvider = AndroidStrings(this) val stringsProvider = AndroidStrings(this)
android.app.AlertDialog.Builder(this) android.app.AlertDialog.Builder(this)
@@ -42,4 +60,13 @@ class SettingsActivity : BaseActivity() {
.show() .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_SELECTED_ENTITIES = "selected_entities"
private const val KEY_REFRESH_INTERVAL = "refresh_interval" private const val KEY_REFRESH_INTERVAL = "refresh_interval"
private const val KEY_LANGUAGE = "app_language" 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 = private fun getPrefs(context: Context): SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
@@ -43,4 +45,14 @@ object Prefs {
/** Saves the set of selected HA entity IDs. */ /** Saves the set of selected HA entity IDs. */
fun setSelectedEntities(context: Context, entities: Set<String>) = fun setSelectedEntities(context: Context, entities: Set<String>) =
getPrefs(context).edit().putStringSet(KEY_SELECTED_ENTITIES, entities).apply() 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_URL -> R.string.label_url
StringKey.LABEL_TOKEN -> R.string.label_token StringKey.LABEL_TOKEN -> R.string.label_token
StringKey.LABEL_REFRESH -> R.string.label_refresh 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_TEST_SAVE -> R.string.btn_test_save
StringKey.BTN_DELETE_ALL -> R.string.btn_delete_all StringKey.BTN_DELETE_ALL -> R.string.btn_delete_all
StringKey.BTN_SAVE_SELECTED -> R.string.btn_save_selected 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,12 +29,18 @@ class EntitySelectionAdapter(
val textContainer = layout.getChildAt(1) as LinearLayout val textContainer = layout.getChildAt(1) as LinearLayout
val tvName = textContainer.getChildAt(0) as TextView val tvName = textContainer.getChildAt(0) as TextView
val tvId = textContainer.getChildAt(1) as TextView val tvId = textContainer.getChildAt(1) as TextView
val item = items[position] val item = items[position]
checkbox.isChecked = selectedEntities.contains(item.entity_id) checkbox.isChecked = selectedEntities.contains(item.entity_id)
tvName.text = item.attributes.friendly_name ?: item.entity_id tvName.text = item.attributes.friendly_name ?: item.entity_id
tvId.text = item.entity_id tvId.text = item.entity_id
val textColor = Colors.getTextColor(context)
tvName.setTextColor(textColor)
tvId.setTextColor(Colors.GRAY_MID)
return layout return layout
} }
private fun createLayout(): LinearLayout { private fun createLayout(): LinearLayout {
return LinearLayout(context).apply { return LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL orientation = LinearLayout.HORIZONTAL

View File

@@ -31,17 +31,20 @@ class LightControlDialog(
private val entityName: String, private val entityName: String,
private val initialBrightness: Int, private val initialBrightness: Int,
private val onBrightnessChanged: (Int) -> Unit private val onBrightnessChanged: (Int) -> Unit
) : Dialog(context) { ) : Dialog(context, android.R.style.Theme_Holo_Light_Dialog_NoActionBar) {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
// Disable hardware acceleration for this dialog window to avoid EGL errors on old hardware // Force OPAQUE format and disable hardware acceleration to fix EGL_BAD_MATCH on old drivers
window?.setFlags( window?.let { win ->
android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, win.setFormat(android.graphics.PixelFormat.RGB_565)
android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED 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 val density = context.resources.displayMetrics.density
fun dp(v: Int) = (v * density + 0.5f).toInt() fun dp(v: Int) = (v * density + 0.5f).toInt()
@@ -50,7 +53,10 @@ class LightControlDialog(
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
setBackgroundColor(Colors.WHITE) setBackgroundColor(Colors.WHITE)
setPadding(dp(24), dp(24), dp(24), dp(24)) 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 { root.addView(TextView(context).apply {

View File

@@ -39,13 +39,13 @@ class WidgetCardView(context: Context) : View(context) {
private val execIconPath = Path() private val execIconPath = Path()
private val paintFill = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } private val paintFill = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
private val tpLabel = TextPaint(Paint.ANTI_ALIAS_FLAG).apply { 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 { 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 { 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() private val pulseInterpolator = DecelerateInterpolator()
@@ -99,8 +99,11 @@ class WidgetCardView(context: Context) : View(context) {
fun bind(cfg: WidgetConfig) { fun bind(cfg: WidgetConfig) {
config = cfg config = cfg
currentInteraction = cfg.domain.toWidgetInteraction() 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) { if (cfg.state == EntityState.TOGGLING) {
startPulse() startPulse()
@@ -136,17 +139,21 @@ class WidgetCardView(context: Context) : View(context) {
val labelUp = if (labelChanged) cfg.label.uppercase() else cachedLabel val labelUp = if (labelChanged) cfg.label.uppercase() else cachedLabel
// For sensors/numbers, append unit to value to prevent line wrapping // Use non-breaking space to keep value and unit together
val valueText = if (cfg.secondary.length <= 3 && cfg.secondary.isNotEmpty()) { val valueText = if (cfg.secondary.length <= 4 && cfg.secondary.isNotEmpty()) {
"${cfg.value} ${cfg.secondary}" "${cfg.value}\u00A0${cfg.secondary}"
} else { } else {
cfg.value 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) 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) lSecondary = makeLayout(secondaryText, tpSecondary, textW)
cachedLabel = cfg.label cachedLabel = cfg.label
@@ -162,27 +169,50 @@ class WidgetCardView(context: Context) : View(context) {
val b = borderPx.toFloat() val b = borderPx.toFloat()
val s = shadowPx.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 // Shadow
paintFill.color = Colors.BLACK paintFill.color = Colors.getShadowColor(context)
canvas.drawRect(s, s, width.toFloat(), cardH + s, paintFill) canvas.drawRect(s, s, width.toFloat(), cardH + s, paintFill)
// Border // Border
paintFill.color = when (cfg.state) { paintFill.color = when (cfg.state) {
EntityState.TOGGLING -> Colors.BORDER_TOGGLING EntityState.TOGGLING -> Colors.BLUE
EntityState.UNAVAILABLE -> Colors.BORDER_UNAVAILABLE EntityState.UNAVAILABLE -> Colors.GRAY_MID
else -> Colors.BORDER_DEFAULT else -> Colors.getBorderColor(context)
} }
canvas.drawRect(0f, 0f, cardW, cardH, paintFill) canvas.drawRect(0f, 0f, cardW, cardH, paintFill)
// Background // Background
paintFill.color = when (cfg.state) { val baseBgColor = when (cfg.state) {
EntityState.ON -> Colors.STATUS_ON
EntityState.OFF -> Colors.STATUS_OFF
EntityState.UNAVAILABLE -> Colors.STATUS_UNAVAILABLE 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) 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 // Domain Stripe
paintFill.color = stripeColor(cfg.domain) paintFill.color = stripeColor(cfg.domain)
canvas.drawRect(b, b, b + stripePx, cardH - b, paintFill) canvas.drawRect(b, b, b + stripePx, cardH - b, paintFill)
@@ -228,7 +258,6 @@ class WidgetCardView(context: Context) : View(context) {
private fun startPulse() { private fun startPulse() {
if (pulseAnim?.isRunning == true) return if (pulseAnim?.isRunning == true) return
setLayerType(LAYER_TYPE_HARDWARE, null)
pulseAnim = ObjectAnimator.ofFloat(this, ALPHA, 1f, 0.35f).apply { pulseAnim = ObjectAnimator.ofFloat(this, ALPHA, 1f, 0.35f).apply {
duration = 550 duration = 550
repeatMode = ObjectAnimator.REVERSE repeatMode = ObjectAnimator.REVERSE
@@ -242,7 +271,6 @@ class WidgetCardView(context: Context) : View(context) {
pulseAnim?.cancel() pulseAnim?.cancel()
pulseAnim = null pulseAnim = null
alpha = 1f alpha = 1f
setLayerType(LAYER_TYPE_NONE, null)
} }
private fun stripeColor(domain: String): Int = when (domain) { private fun stripeColor(domain: String): Int = when (domain) {

View File

@@ -7,6 +7,7 @@
android:padding="16dp"> android:padding="16dp">
<TextView <TextView
android:id="@+id/tvConnectionTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/title_connection" android:text="@string/title_connection"
@@ -16,6 +17,7 @@
android:textColor="@color/ha_black" /> android:textColor="@color/ha_black" />
<View <View
android:id="@+id/vBorderTitle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="2dp" android:layout_height="2dp"
android:background="@color/ha_black" android:background="@color/ha_black"
@@ -23,6 +25,7 @@
android:layout_marginBottom="16dp" /> android:layout_marginBottom="16dp" />
<TextView <TextView
android:id="@+id/labelUrl"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/label_url" android:text="@string/label_url"
@@ -43,12 +46,14 @@
android:textSize="16sp" /> android:textSize="16sp" />
<View <View
android:id="@+id/vBorderUrl"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:background="@color/ha_black" android:background="@color/ha_black"
android:layout_marginBottom="16dp" /> android:layout_marginBottom="16dp" />
<TextView <TextView
android:id="@+id/labelToken"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/label_token" android:text="@string/label_token"
@@ -69,12 +74,14 @@
android:textSize="16sp" /> android:textSize="16sp" />
<View <View
android:id="@+id/vBorderToken"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:background="@color/ha_black" android:background="@color/ha_black"
android:layout_marginBottom="16dp" /> android:layout_marginBottom="16dp" />
<TextView <TextView
android:id="@+id/labelRefresh"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/label_refresh" android:text="@string/label_refresh"
@@ -95,10 +102,35 @@
android:textSize="16sp" /> android:textSize="16sp" />
<View <View
android:id="@+id/vBorderRefresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:background="@color/ha_black" 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 <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

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

View File

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

View File

@@ -1,131 +1,151 @@
<?xml version="1.0" encoding="utf-8"?> <?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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical"
android:background="@color/ha_white">
<!-- TOP BAR -->
<LinearLayout <LinearLayout
android:id="@+id/mainLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:background="@color/ha_white"> android:background="@color/ha_white">
<RelativeLayout <!-- TOP BAR -->
android:layout_width="match_parent" <LinearLayout
android:layout_height="54dp" android:id="@+id/topBar"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_title"
android:textSize="18sp"
android:textStyle="bold"
android:typeface="monospace"
android:textColor="@color/ha_black" />
<TextView
android:id="@+id/tvStatusIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ONLINE"
android:textSize="9sp"
android:textStyle="bold"
android:typeface="monospace"
android:textColor="@color/ha_blue" />
</LinearLayout>
<FrameLayout
android:id="@+id/btnSettingsContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true">
<!-- hard shadow -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_settings"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:layout_marginLeft="3dp"
android:layout_marginTop="3dp"
android:background="@color/ha_black"
android:textSize="12sp"
android:typeface="monospace"
android:textColor="@color/ha_black" />
<!-- button -->
<TextView
android:id="@+id/btnSettings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_settings"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:background="@color/ha_blue"
android:textSize="12sp"
android:typeface="monospace"
android:textColor="@color/ha_white"
android:clickable="true" />
</FrameLayout>
</RelativeLayout>
<!-- TABS -->
<HorizontalScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:orientation="vertical"
android:background="@color/ha_white"> android:background="@color/ha_white">
<LinearLayout
android:id="@+id/tabContainer"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:orientation="horizontal"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<!-- Tabs will be added programmatically -->
</LinearLayout>
</HorizontalScrollView>
<!-- 2dp bottom border --> <RelativeLayout
<View android:id="@+id/topBarContent"
android:layout_width="match_parent"
android:layout_height="54dp"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_title"
android:textSize="18sp"
android:textStyle="bold"
android:typeface="monospace"
android:textColor="@color/ha_black" />
<TextView
android:id="@+id/tvStatusIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ONLINE"
android:textSize="9sp"
android:textStyle="bold"
android:typeface="monospace"
android:textColor="@color/ha_blue" />
</LinearLayout>
<FrameLayout
android:id="@+id/btnSettingsContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true">
<!-- hard shadow -->
<TextView
android:id="@+id/btnSettingsShadow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_settings"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:layout_marginLeft="3dp"
android:layout_marginTop="3dp"
android:background="@color/ha_black"
android:textSize="12sp"
android:typeface="monospace"
android:textColor="@color/ha_black" />
<!-- button -->
<TextView
android:id="@+id/btnSettings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_settings"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:background="@color/ha_blue"
android:textSize="12sp"
android:typeface="monospace"
android:textColor="@color/ha_white"
android:clickable="true" />
</FrameLayout>
</RelativeLayout>
<!-- TABS -->
<HorizontalScrollView
android:id="@+id/tabScrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
android:background="@color/ha_white">
<LinearLayout
android:id="@+id/tabContainer"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:orientation="horizontal"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<!-- Tabs will be added programmatically -->
</LinearLayout>
</HorizontalScrollView>
<!-- 2dp bottom border -->
<View
android:id="@+id/vTopBarBorder"
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/ha_black" />
</LinearLayout>
<!-- WIDGET GRID -->
<GridView
android:id="@+id/gridView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="2dp" android:layout_height="0dp"
android:background="@color/ha_black" /> android:layout_weight="1"
android:numColumns="2"
android:horizontalSpacing="8dp"
android:verticalSpacing="8dp"
android:padding="8dp"
android:background="@color/ha_white"
android:listSelector="@android:color/transparent"
android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical"
android:clipToPadding="false" />
</LinearLayout> </LinearLayout>
<!-- WIDGET GRID --> <!-- CONTROL OVERLAY (Hidden by default) -->
<GridView <com.example.retroha.ui.BauhausControlOverlay
android:id="@+id/gridView" android:id="@+id/controlOverlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:layout_weight="1" android:visibility="gone" />
android:numColumns="2"
android:horizontalSpacing="8dp"
android:verticalSpacing="8dp"
android:padding="8dp"
android:background="@color/ha_white"
android:listSelector="@android:color/transparent"
android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical"
android:clipToPadding="false" />
</LinearLayout> </FrameLayout>

View File

@@ -7,6 +7,7 @@
android:padding="16dp"> android:padding="16dp">
<TextView <TextView
android:id="@+id/tvSettingsTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/btn_settings" android:text="@string/btn_settings"
@@ -109,6 +110,38 @@
android:textStyle="bold" /> android:textStyle="bold" />
</FrameLayout> </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 --> <!-- DELETE ALL BUTTON -->
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -23,6 +23,8 @@
<string name="label_url">URL ADDRESS</string> <string name="label_url">URL ADDRESS</string>
<string name="label_token">ACCESS TOKEN</string> <string name="label_token">ACCESS TOKEN</string>
<string name="label_refresh">REFRESH (SECONDS)</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_test_save">TEST AND SAVE</string>
<string name="btn_delete_all">DELETE ALL WIDGETS</string> <string name="btn_delete_all">DELETE ALL WIDGETS</string>
<string name="btn_save_selected">SAVE SELECTED</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_url">ADRES URL</string>
<string name="label_token">TOKEN DOSTĘPU</string> <string name="label_token">TOKEN DOSTĘPU</string>
<string name="label_refresh">ODŚWIEŻANIE (SEKUNDY)</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_test_save">TESTUJ I ZAPISZ</string>
<string name="btn_delete_all">USUŃ WSZYSTKIE WIDŻETY</string> <string name="btn_delete_all">USUŃ WSZYSTKIE WIDŻETY</string>
<string name="btn_save_selected">ZAPISZ WYBRANE</string> <string name="btn_save_selected">ZAPISZ WYBRANE</string>

View File

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

View File

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