docs: complete 100% kdoc coverage and update app branding
All checks were successful
Update Wiki Documentation / generate-docs (push) Successful in 2m12s
All checks were successful
Update Wiki Documentation / generate-docs (push) Successful in 2m12s
This commit is contained in:
@@ -1,29 +0,0 @@
|
|||||||
package com.example.retroha
|
|
||||||
import androidx.test.espresso.Espresso.onView
|
|
||||||
import androidx.test.espresso.action.ViewActions.click
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
|
||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class AutomatedClickTest {
|
|
||||||
@get:Rule
|
|
||||||
val activityRule = ActivityScenarioRule(MainActivity::class.java)
|
|
||||||
@Test
|
|
||||||
fun clickThroughAllTabs() {
|
|
||||||
Thread.sleep(1000)
|
|
||||||
val tabResIds = listOf(
|
|
||||||
R.string.tab_lighting,
|
|
||||||
R.string.tab_sockets,
|
|
||||||
R.string.tab_power,
|
|
||||||
R.string.tab_weather,
|
|
||||||
R.string.tab_all
|
|
||||||
)
|
|
||||||
tabResIds.forEach { resId ->
|
|
||||||
onView(withText(resId)).perform(click())
|
|
||||||
Thread.sleep(500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.example.retroha
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.junit.Assert.*
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
fun useAppContext() {
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
assertEquals("com.example.retroha", appContext.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,7 @@ import android.view.ViewGroup
|
|||||||
import android.widget.GridView
|
import android.widget.GridView
|
||||||
import org.hamcrest.Description
|
import org.hamcrest.Description
|
||||||
import org.hamcrest.TypeSafeMatcher
|
import org.hamcrest.TypeSafeMatcher
|
||||||
|
import com.example.retroha.R
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class MonkeyStressTest {
|
class MonkeyStressTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import android.content.Context
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.example.retroha.i18n.LocaleHelper
|
import com.example.retroha.i18n.LocaleHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base activity class that provides automatic localization support.
|
||||||
|
* All activities in the application should extend this class to ensure
|
||||||
|
* dynamic language changes are applied correctly.
|
||||||
|
*/
|
||||||
abstract class BaseActivity : Activity() {
|
abstract class BaseActivity : Activity() {
|
||||||
override fun attachBaseContext(newBase: Context) {
|
override fun attachBaseContext(newBase: Context) {
|
||||||
super.attachBaseContext(LocaleHelper.setLocale(newBase))
|
super.attachBaseContext(LocaleHelper.setLocale(newBase))
|
||||||
|
|||||||
@@ -3,11 +3,18 @@ import android.app.Activity
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
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.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 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.
|
||||||
|
* Handles server URL, authentication token, and refresh interval settings.
|
||||||
|
* Includes a validation step before saving.
|
||||||
|
*/
|
||||||
class ConnectionSettingsActivity : BaseActivity() {
|
class ConnectionSettingsActivity : BaseActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -24,20 +31,24 @@ class ConnectionSettingsActivity : BaseActivity() {
|
|||||||
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 strings = com.example.retroha.i18n.AndroidStrings(this)
|
val stringsProvider = AndroidStrings(this)
|
||||||
android.app.AlertDialog.Builder(this)
|
android.app.AlertDialog.Builder(this)
|
||||||
.setTitle(strings.get(com.example.retroha.i18n.StringKey.DIALOG_WARNING))
|
.setTitle(stringsProvider.get(StringKey.DIALOG_WARNING))
|
||||||
.setMessage(strings.get(com.example.retroha.i18n.StringKey.CONFIRM_CHANGE_CONN))
|
.setMessage(stringsProvider.get(StringKey.CONFIRM_CHANGE_CONN))
|
||||||
.setPositiveButton(strings.get(com.example.retroha.i18n.StringKey.DIALOG_YES_CHANGE)) { _, _ ->
|
.setPositiveButton(stringsProvider.get(StringKey.DIALOG_YES_CHANGE)) { _, _ ->
|
||||||
performTestAndSave(url, token, intervalSec, tvStatus)
|
performTestAndSave(url, token, intervalSec, tvStatus)
|
||||||
}
|
}
|
||||||
.setNegativeButton(strings.get(com.example.retroha.i18n.StringKey.DIALOG_CANCEL), null)
|
.setNegativeButton(stringsProvider.get(StringKey.DIALOG_CANCEL), null)
|
||||||
.show()
|
.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, tvStatus: TextView) {
|
||||||
val strings = com.example.retroha.i18n.AndroidStrings(this)
|
val stringsProvider = AndroidStrings(this)
|
||||||
tvStatus.text = strings.get(com.example.retroha.i18n.StringKey.STATUS_CONNECTING)
|
tvStatus.text = stringsProvider.get(StringKey.STATUS_CONNECTING)
|
||||||
tvStatus.setTextColor(0xFF000000.toInt())
|
tvStatus.setTextColor(0xFF000000.toInt())
|
||||||
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>> {
|
||||||
@@ -49,21 +60,21 @@ class ConnectionSettingsActivity : BaseActivity() {
|
|||||||
Prefs.setSelectedEntities(this@ConnectionSettingsActivity, emptySet())
|
Prefs.setSelectedEntities(this@ConnectionSettingsActivity, emptySet())
|
||||||
HaClient.clearCache()
|
HaClient.clearCache()
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
tvStatus.text = strings.get(StringKey.STATUS_SUCCESS)
|
tvStatus.text = stringsProvider.get(StringKey.STATUS_SUCCESS)
|
||||||
tvStatus.setTextColor(0xFF0056B3.toInt())
|
tvStatus.setTextColor(0xFF0056B3.toInt())
|
||||||
Toast.makeText(this@ConnectionSettingsActivity, strings.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 = "${strings.get(StringKey.STATUS_ERROR)}: ${response.code()}"
|
tvStatus.text = "${stringsProvider.get(StringKey.STATUS_ERROR)}: ${response.code()}"
|
||||||
tvStatus.setTextColor(0xFFE23A24.toInt())
|
tvStatus.setTextColor(0xFFE23A24.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
|
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
tvStatus.text = "${strings.get(StringKey.STATUS_ERROR_NETWORK)}: ${t.message}"
|
tvStatus.text = "${stringsProvider.get(StringKey.STATUS_ERROR_NETWORK)}: ${t.message}"
|
||||||
tvStatus.setTextColor(0xFFE23A24.toInt())
|
tvStatus.setTextColor(0xFFE23A24.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,18 @@ import android.text.Editable
|
|||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
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.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.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.
|
||||||
|
* Features real-time filtering and persistent selection storage.
|
||||||
|
*/
|
||||||
class EntitySelectionActivity : BaseActivity() {
|
class EntitySelectionActivity : BaseActivity() {
|
||||||
private lateinit var etSearch: EditText
|
private lateinit var etSearch: EditText
|
||||||
private lateinit var lvEntities: ListView
|
private lateinit var lvEntities: ListView
|
||||||
@@ -20,14 +26,14 @@ class EntitySelectionActivity : BaseActivity() {
|
|||||||
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)
|
||||||
val strings = com.example.retroha.i18n.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, strings.get(com.example.retroha.i18n.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 {
|
||||||
@@ -48,6 +54,9 @@ class EntitySelectionActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
fetchEntities()
|
fetchEntities()
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Fetches the complete list of available entities from the HA server.
|
||||||
|
*/
|
||||||
private fun fetchEntities() {
|
private fun fetchEntities() {
|
||||||
val token = Prefs.getToken(this)
|
val token = Prefs.getToken(this)
|
||||||
if (token.isEmpty()) return
|
if (token.isEmpty()) return
|
||||||
@@ -59,18 +68,21 @@ class EntitySelectionActivity : BaseActivity() {
|
|||||||
allEntities.sortBy { it.entity_id }
|
allEntities.sortBy { it.entity_id }
|
||||||
runOnUiThread { filterEntities(etSearch.text.toString()) }
|
runOnUiThread { filterEntities(etSearch.text.toString()) }
|
||||||
} else {
|
} else {
|
||||||
val strings = com.example.retroha.i18n.AndroidStrings(this@EntitySelectionActivity)
|
val stringsProvider = AndroidStrings(this@EntitySelectionActivity)
|
||||||
runOnUiThread { Toast.makeText(this@EntitySelectionActivity, "${strings.get(com.example.retroha.i18n.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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
|
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
|
||||||
val strings = com.example.retroha.i18n.AndroidStrings(this@EntitySelectionActivity)
|
val stringsProvider = AndroidStrings(this@EntitySelectionActivity)
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
Toast.makeText(this@EntitySelectionActivity, "${strings.get(com.example.retroha.i18n.StringKey.STATUS_ERROR_NETWORK)}: ${t.message}", Toast.LENGTH_LONG).show()
|
Toast.makeText(this@EntitySelectionActivity, "${stringsProvider.get(StringKey.STATUS_ERROR_NETWORK)}: ${t.message}", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Filters the [allEntities] list based on the user's search query.
|
||||||
|
*/
|
||||||
private fun filterEntities(query: String) {
|
private fun filterEntities(query: String) {
|
||||||
filteredEntities.clear()
|
filteredEntities.clear()
|
||||||
if (query.isEmpty()) {
|
if (query.isEmpty()) {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.example.retroha
|
package com.example.retroha
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
/**
|
||||||
|
* Activity for displaying the user manual and basic operation instructions.
|
||||||
|
*/
|
||||||
class InstructionsActivity : BaseActivity() {
|
class InstructionsActivity : BaseActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|||||||
@@ -10,8 +10,14 @@ import android.widget.FrameLayout
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.example.retroha.data.Prefs
|
import com.example.retroha.data.Prefs
|
||||||
|
import com.example.retroha.i18n.AndroidStrings
|
||||||
|
import com.example.retroha.i18n.StringKey
|
||||||
import com.example.retroha.i18n.LocaleHelper
|
import com.example.retroha.i18n.LocaleHelper
|
||||||
import com.example.retroha.theme.Colors
|
import com.example.retroha.theme.Colors
|
||||||
|
/**
|
||||||
|
* Activity for selecting the application language.
|
||||||
|
* Serves as the initial entry point on first launch or when triggered from settings.
|
||||||
|
*/
|
||||||
class LanguageActivity : BaseActivity() {
|
class LanguageActivity : BaseActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -29,9 +35,9 @@ class LanguageActivity : BaseActivity() {
|
|||||||
setBackgroundColor(Colors.WHITE)
|
setBackgroundColor(Colors.WHITE)
|
||||||
setPadding(dp(32), dp(32), dp(32), dp(32))
|
setPadding(dp(32), dp(32), dp(32), dp(32))
|
||||||
}
|
}
|
||||||
val strings = AndroidStrings(this)
|
val stringsProvider = AndroidStrings(this)
|
||||||
val title = TextView(this).apply {
|
val title = TextView(this).apply {
|
||||||
text = strings.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.BLACK)
|
||||||
|
|||||||
@@ -30,8 +30,13 @@ 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.
|
||||||
|
* Displays a responsive grid of Home Assistant widgets and handles real-time updates.
|
||||||
|
* Manages domain filtering via tabs and coordinates network requests with [HaClient].
|
||||||
|
*/
|
||||||
class MainActivity : BaseActivity() {
|
class MainActivity : BaseActivity() {
|
||||||
private lateinit var strings: AndroidStrings
|
private lateinit var stringsProvider: AndroidStrings
|
||||||
private lateinit var adapter: WidgetAdapter
|
private lateinit var adapter: WidgetAdapter
|
||||||
private val allEntities = mutableListOf<WidgetConfig>()
|
private val allEntities = mutableListOf<WidgetConfig>()
|
||||||
private val displayedEntities = mutableListOf<WidgetConfig>()
|
private val displayedEntities = mutableListOf<WidgetConfig>()
|
||||||
@@ -50,7 +55,7 @@ class MainActivity : BaseActivity() {
|
|||||||
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)
|
||||||
strings = 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)
|
||||||
val gridView = findViewById<GridView>(R.id.gridView)
|
val gridView = findViewById<GridView>(R.id.gridView)
|
||||||
@@ -78,10 +83,14 @@ class MainActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
findViewById<android.view.View>(R.id.tvTitle).setOnClickListener {
|
findViewById<android.view.View>(R.id.tvTitle).setOnClickListener {
|
||||||
fetchHaStates()
|
fetchHaStates()
|
||||||
Toast.makeText(this, strings.get(StringKey.STATUS_REFRESHING), Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, stringsProvider.get(StringKey.STATUS_REFRESHING), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
setupTabs()
|
setupTabs()
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Initializes the category filtering tabs.
|
||||||
|
* Tabs are dynamically generated based on supported domains and current selection.
|
||||||
|
*/
|
||||||
private fun setupTabs() {
|
private fun setupTabs() {
|
||||||
val categories = listOf(
|
val categories = listOf(
|
||||||
StringKey.TAB_ALL,
|
StringKey.TAB_ALL,
|
||||||
@@ -108,7 +117,7 @@ class MainActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
val tabButton = TextView(this).apply {
|
val tabButton = TextView(this).apply {
|
||||||
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||||
text = strings.get(cat)
|
text = stringsProvider.get(cat)
|
||||||
typeface = android.graphics.Typeface.MONOSPACE
|
typeface = android.graphics.Typeface.MONOSPACE
|
||||||
textSize = 11f
|
textSize = 11f
|
||||||
setPadding(dp(12), 0, dp(12), 0)
|
setPadding(dp(12), 0, dp(12), 0)
|
||||||
@@ -135,6 +144,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.
|
||||||
|
*/
|
||||||
private fun filterEntities() {
|
private fun filterEntities() {
|
||||||
val filtered = when (currentCategory) {
|
val filtered = when (currentCategory) {
|
||||||
StringKey.TAB_ALL -> allEntities
|
StringKey.TAB_ALL -> allEntities
|
||||||
@@ -160,10 +172,14 @@ class MainActivity : BaseActivity() {
|
|||||||
super.onPause()
|
super.onPause()
|
||||||
mainHandler.removeCallbacks(refreshRunnable)
|
mainHandler.removeCallbacks(refreshRunnable)
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Fetches the latest states from Home Assistant and updates the UI.
|
||||||
|
* Handles authentication errors and network failures.
|
||||||
|
*/
|
||||||
private fun fetchHaStates() {
|
private fun fetchHaStates() {
|
||||||
val token = Prefs.getToken(this)
|
val token = Prefs.getToken(this)
|
||||||
if (token.isEmpty()) {
|
if (token.isEmpty()) {
|
||||||
tvStatusIndicator.text = strings.get(StringKey.STATUS_NO_TOKEN)
|
tvStatusIndicator.text = stringsProvider.get(StringKey.STATUS_NO_TOKEN)
|
||||||
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.RED)
|
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.RED)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -173,24 +189,28 @@ class MainActivity : BaseActivity() {
|
|||||||
val states = response.body() ?: return
|
val states = response.body() ?: return
|
||||||
updateEntities(states)
|
updateEntities(states)
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
tvStatusIndicator.text = strings.get(StringKey.STATUS_CONNECTED)
|
tvStatusIndicator.text = stringsProvider.get(StringKey.STATUS_CONNECTED)
|
||||||
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.BLUE)
|
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.BLUE)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
tvStatusIndicator.text = "${strings.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(com.example.retroha.theme.Colors.RED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
|
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
tvStatusIndicator.text = strings.get(StringKey.STATUS_OFFLINE)
|
tvStatusIndicator.text = stringsProvider.get(StringKey.STATUS_OFFLINE)
|
||||||
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.RED)
|
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.RED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Updates the local [allEntities] list with new data from HA.
|
||||||
|
* Maps raw HA states to [WidgetConfig] objects.
|
||||||
|
*/
|
||||||
private fun updateEntities(haStates: List<HaState>) {
|
private fun updateEntities(haStates: List<HaState>) {
|
||||||
val selectedIds = Prefs.getSelectedEntities(this)
|
val selectedIds = Prefs.getSelectedEntities(this)
|
||||||
allEntities.clear()
|
allEntities.clear()
|
||||||
@@ -253,19 +273,19 @@ class MainActivity : BaseActivity() {
|
|||||||
if (idx >= 0 && idx < allEntities.size) {
|
if (idx >= 0 && idx < allEntities.size) {
|
||||||
allEntities[idx] = allEntities[idx].copy(
|
allEntities[idx] = allEntities[idx].copy(
|
||||||
state = EntityState.TOGGLING,
|
state = EntityState.TOGGLING,
|
||||||
value = strings[StringKey.STATE_TOGGLING]
|
value = stringsProvider[StringKey.STATE_TOGGLING]
|
||||||
)
|
)
|
||||||
filterEntities()
|
filterEntities()
|
||||||
}
|
}
|
||||||
HaClient.getService(this).toggle(domain, ToggleRequest(cfg.entityId)).enqueue(object : Callback<List<HaState>> {
|
HaClient.getService(this).toggle(domain, ToggleRequest(cfg.entityId)).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) {
|
||||||
runOnUiThread { Toast.makeText(this@MainActivity, "${strings.get(StringKey.STATUS_ERROR_HA)}: ${response.code()}", Toast.LENGTH_SHORT).show() }
|
runOnUiThread { Toast.makeText(this@MainActivity, "${stringsProvider.get(StringKey.STATUS_ERROR_HA)}: ${response.code()}", Toast.LENGTH_SHORT).show() }
|
||||||
}
|
}
|
||||||
fetchHaStates()
|
fetchHaStates()
|
||||||
}
|
}
|
||||||
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
|
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
|
||||||
runOnUiThread { Toast.makeText(this@MainActivity, "${strings.get(StringKey.STATUS_ERROR_NETWORK)}: ${t.message}", Toast.LENGTH_SHORT).show() }
|
runOnUiThread { Toast.makeText(this@MainActivity, "${stringsProvider.get(StringKey.STATUS_ERROR_NETWORK)}: ${t.message}", Toast.LENGTH_SHORT).show() }
|
||||||
fetchHaStates()
|
fetchHaStates()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -275,19 +295,19 @@ class MainActivity : BaseActivity() {
|
|||||||
if (idx >= 0 && idx < allEntities.size) {
|
if (idx >= 0 && idx < allEntities.size) {
|
||||||
allEntities[idx] = allEntities[idx].copy(
|
allEntities[idx] = allEntities[idx].copy(
|
||||||
state = EntityState.TOGGLING,
|
state = EntityState.TOGGLING,
|
||||||
value = strings[StringKey.STATE_TOGGLING]
|
value = stringsProvider[StringKey.STATE_TOGGLING]
|
||||||
)
|
)
|
||||||
filterEntities()
|
filterEntities()
|
||||||
}
|
}
|
||||||
HaClient.getService(this).toggle(domain, ToggleRequest(cfg.entityId)).enqueue(object : Callback<List<HaState>> {
|
HaClient.getService(this).toggle(domain, ToggleRequest(cfg.entityId)).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) {
|
||||||
runOnUiThread { Toast.makeText(this@MainActivity, "${strings.get(StringKey.STATUS_ERROR_HA)}: ${response.code()}", Toast.LENGTH_SHORT).show() }
|
runOnUiThread { Toast.makeText(this@MainActivity, "${stringsProvider.get(StringKey.STATUS_ERROR_HA)}: ${response.code()}", Toast.LENGTH_SHORT).show() }
|
||||||
}
|
}
|
||||||
fetchHaStates()
|
fetchHaStates()
|
||||||
}
|
}
|
||||||
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
|
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
|
||||||
runOnUiThread { Toast.makeText(this@MainActivity, "${strings.get(StringKey.STATUS_ERROR_NETWORK)}: ${t.message}", Toast.LENGTH_SHORT).show() }
|
runOnUiThread { Toast.makeText(this@MainActivity, "${stringsProvider.get(StringKey.STATUS_ERROR_NETWORK)}: ${t.message}", Toast.LENGTH_SHORT).show() }
|
||||||
fetchHaStates()
|
fetchHaStates()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -306,6 +326,9 @@ class MainActivity : BaseActivity() {
|
|||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Calculates the optimal number of columns based on screen width and orientation.
|
||||||
|
*/
|
||||||
private fun resolveColumns(): Int {
|
private fun resolveColumns(): Int {
|
||||||
val config = resources.configuration
|
val config = resources.configuration
|
||||||
val sw = config.smallestScreenWidthDp
|
val sw = config.smallestScreenWidthDp
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import android.os.Bundle
|
|||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
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.StringKey
|
||||||
|
/**
|
||||||
|
* Main settings menu activity.
|
||||||
|
* Provides navigation to connection configuration, entity selection,
|
||||||
|
* instructions, and language settings.
|
||||||
|
*/
|
||||||
class SettingsActivity : BaseActivity() {
|
class SettingsActivity : BaseActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -23,15 +30,15 @@ class SettingsActivity : BaseActivity() {
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
findViewById<Button>(R.id.btnDeleteAll).setOnClickListener {
|
findViewById<Button>(R.id.btnDeleteAll).setOnClickListener {
|
||||||
val strings = com.example.retroha.i18n.AndroidStrings(this)
|
val stringsProvider = AndroidStrings(this)
|
||||||
android.app.AlertDialog.Builder(this)
|
android.app.AlertDialog.Builder(this)
|
||||||
.setTitle(strings.get(com.example.retroha.i18n.StringKey.DIALOG_WARNING))
|
.setTitle(stringsProvider.get(StringKey.DIALOG_WARNING))
|
||||||
.setMessage(strings.get(com.example.retroha.i18n.StringKey.CONFIRM_DELETE_ALL))
|
.setMessage(stringsProvider.get(StringKey.CONFIRM_DELETE_ALL))
|
||||||
.setPositiveButton(strings.get(com.example.retroha.i18n.StringKey.DIALOG_YES_DELETE)) { _, _ ->
|
.setPositiveButton(stringsProvider.get(StringKey.DIALOG_YES_DELETE)) { _, _ ->
|
||||||
Prefs.setSelectedEntities(this, emptySet())
|
Prefs.setSelectedEntities(this, emptySet())
|
||||||
Toast.makeText(this, strings.get(com.example.retroha.i18n.StringKey.TOAST_CLEARED), Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, stringsProvider.get(StringKey.TOAST_CLEARED), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
.setNegativeButton(strings.get(com.example.retroha.i18n.StringKey.DIALOG_CANCEL), null)
|
.setNegativeButton(stringsProvider.get(StringKey.DIALOG_CANCEL), null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package com.example.retroha.data
|
package com.example.retroha.data
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton object for managing application preferences and persistent state.
|
||||||
|
* Uses [SharedPreferences] to store connection settings and widget configurations.
|
||||||
|
*/
|
||||||
object Prefs {
|
object Prefs {
|
||||||
private const val PREFS_NAME = "retroha_prefs"
|
private const val PREFS_NAME = "retroha_prefs"
|
||||||
private const val KEY_URL = "ha_url"
|
private const val KEY_URL = "ha_url"
|
||||||
@@ -8,18 +13,34 @@ 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 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)
|
||||||
|
|
||||||
|
/** Gets the currently selected app language code (e.g., "pl", "en"). */
|
||||||
fun getLanguage(context: Context): String? = getPrefs(context).getString(KEY_LANGUAGE, null)
|
fun getLanguage(context: Context): String? = getPrefs(context).getString(KEY_LANGUAGE, null)
|
||||||
|
/** Saves the app language preference. */
|
||||||
fun setLanguage(context: Context, lang: String) = getPrefs(context).edit().putString(KEY_LANGUAGE, lang).apply()
|
fun setLanguage(context: Context, lang: String) = getPrefs(context).edit().putString(KEY_LANGUAGE, lang).apply()
|
||||||
|
|
||||||
|
/** Gets the Home Assistant server URL. Defaults to "http://". */
|
||||||
fun getUrl(context: Context): String = getPrefs(context).getString(KEY_URL, "http://") ?: "http://"
|
fun getUrl(context: Context): String = getPrefs(context).getString(KEY_URL, "http://") ?: "http://"
|
||||||
|
/** Saves the Home Assistant server URL. */
|
||||||
fun setUrl(context: Context, url: String) = getPrefs(context).edit().putString(KEY_URL, url).apply()
|
fun setUrl(context: Context, url: String) = getPrefs(context).edit().putString(KEY_URL, url).apply()
|
||||||
|
|
||||||
|
/** Gets the Home Assistant Long-Lived Access Token. */
|
||||||
fun getToken(context: Context): String = getPrefs(context).getString(KEY_TOKEN, "") ?: ""
|
fun getToken(context: Context): String = getPrefs(context).getString(KEY_TOKEN, "") ?: ""
|
||||||
|
/** Saves the Home Assistant Access Token. */
|
||||||
fun setToken(context: Context, token: String) = getPrefs(context).edit().putString(KEY_TOKEN, token).apply()
|
fun setToken(context: Context, token: String) = getPrefs(context).edit().putString(KEY_TOKEN, token).apply()
|
||||||
|
|
||||||
|
/** Gets the background refresh interval in milliseconds. Defaults to 30s. */
|
||||||
fun getRefreshInterval(context: Context): Long = getPrefs(context).getLong(KEY_REFRESH_INTERVAL, 30000L)
|
fun getRefreshInterval(context: Context): Long = getPrefs(context).getLong(KEY_REFRESH_INTERVAL, 30000L)
|
||||||
|
/** Saves the refresh interval in milliseconds. */
|
||||||
fun setRefreshInterval(context: Context, intervalMs: Long) = getPrefs(context).edit().putLong(KEY_REFRESH_INTERVAL, intervalMs).apply()
|
fun setRefreshInterval(context: Context, intervalMs: Long) = getPrefs(context).edit().putLong(KEY_REFRESH_INTERVAL, intervalMs).apply()
|
||||||
|
|
||||||
|
/** Gets the set of HA entity IDs selected to be displayed as widgets. */
|
||||||
fun getSelectedEntities(context: Context): Set<String> =
|
fun getSelectedEntities(context: Context): Set<String> =
|
||||||
getPrefs(context).getStringSet(KEY_SELECTED_ENTITIES, emptySet()) ?: emptySet()
|
getPrefs(context).getStringSet(KEY_SELECTED_ENTITIES, emptySet()) ?: emptySet()
|
||||||
|
/** 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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
package com.example.retroha.i18n
|
package com.example.retroha.i18n
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.example.retroha.R
|
import com.example.retroha.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android-specific implementation of the [Strings] interface.
|
||||||
|
* Bridges the universal [StringKey] to Android's `R.string` resource system.
|
||||||
|
*/
|
||||||
class AndroidStrings(private val context: Context) : Strings {
|
class AndroidStrings(private val context: Context) : Strings {
|
||||||
override fun get(key: StringKey): String = context.getString(
|
/**
|
||||||
|
* Resolves the [StringKey] to an actual string from Android resources.
|
||||||
|
*/
|
||||||
|
override operator fun get(key: StringKey): String = context.getString(
|
||||||
when (key) {
|
when (key) {
|
||||||
StringKey.STATE_ON -> R.string.state_on
|
StringKey.STATE_ON -> R.string.state_on
|
||||||
StringKey.STATE_OFF -> R.string.state_off
|
StringKey.STATE_OFF -> R.string.state_off
|
||||||
|
|||||||
@@ -5,13 +5,26 @@ import android.os.Build
|
|||||||
import com.example.retroha.data.Prefs
|
import com.example.retroha.data.Prefs
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for handling dynamic runtime locale changes in Android.
|
||||||
|
* Manages resource configuration updates and context wrapping.
|
||||||
|
*/
|
||||||
object LocaleHelper {
|
object LocaleHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the saved language preference to the given context.
|
||||||
|
*
|
||||||
|
* @param context The base context to wrap or update.
|
||||||
|
* @return A new context instance with the updated locale configuration.
|
||||||
|
*/
|
||||||
fun setLocale(context: Context): Context {
|
fun setLocale(context: Context): Context {
|
||||||
val lang = Prefs.getLanguage(context) ?: "pl"
|
val lang = Prefs.getLanguage(context) ?: "pl"
|
||||||
return updateResources(context, lang)
|
return updateResources(context, lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal logic for updating resources based on API level.
|
||||||
|
*/
|
||||||
private fun updateResources(context: Context, language: String): Context {
|
private fun updateResources(context: Context, language: String): Context {
|
||||||
val locale = Locale(language)
|
val locale = Locale(language)
|
||||||
Locale.setDefault(locale)
|
Locale.setDefault(locale)
|
||||||
|
|||||||
@@ -1,27 +1,52 @@
|
|||||||
package com.example.retroha.network
|
package com.example.retroha.network
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.*
|
import retrofit2.http.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrofit interface defining the Home Assistant REST API endpoints.
|
||||||
|
*/
|
||||||
interface HaApiService {
|
interface HaApiService {
|
||||||
|
/** Fetches the current state of all entities. */
|
||||||
@GET("api/states")
|
@GET("api/states")
|
||||||
fun getStates(): Call<List<HaState>>
|
fun getStates(): Call<List<HaState>>
|
||||||
|
|
||||||
|
/** Toggles the state of an entity in a specific domain. */
|
||||||
@POST("api/services/{domain}/toggle")
|
@POST("api/services/{domain}/toggle")
|
||||||
fun toggle(@Path("domain") domain: String, @Body body: ToggleRequest): Call<List<HaState>>
|
fun toggle(@Path("domain") domain: String, @Body body: ToggleRequest): Call<List<HaState>>
|
||||||
|
|
||||||
|
/** Sets the specific brightness level for a light entity. */
|
||||||
@POST("api/services/light/turn_on")
|
@POST("api/services/light/turn_on")
|
||||||
fun setBrightness(@Body body: BrightnessRequest): Call<List<HaState>>
|
fun setBrightness(@Body body: BrightnessRequest): Call<List<HaState>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Transfer Object representing the state of an entity returned by HA.
|
||||||
|
*/
|
||||||
data class HaState(
|
data class HaState(
|
||||||
val entity_id: String,
|
val entity_id: String,
|
||||||
val state: String,
|
val state: String,
|
||||||
val attributes: HaAttributes
|
val attributes: HaAttributes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Transfer Object representing entity attributes (metadata).
|
||||||
|
*/
|
||||||
data class HaAttributes(
|
data class HaAttributes(
|
||||||
val friendly_name: String?,
|
val friendly_name: String?,
|
||||||
val unit_of_measurement: String?,
|
val unit_of_measurement: String?,
|
||||||
val brightness: Int? = null
|
val brightness: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request body for the toggle service.
|
||||||
|
*/
|
||||||
data class ToggleRequest(
|
data class ToggleRequest(
|
||||||
val entity_id: String
|
val entity_id: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request body for setting light brightness.
|
||||||
|
*/
|
||||||
data class BrightnessRequest(
|
data class BrightnessRequest(
|
||||||
val entity_id: String,
|
val entity_id: String,
|
||||||
val brightness: Int
|
val brightness: Int
|
||||||
|
|||||||
@@ -5,22 +5,37 @@ import com.google.gson.GsonBuilder
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton factory for managing and providing the [HaApiService] instance.
|
||||||
|
* Handles Retrofit initialization, interceptors for authentication, and caching.
|
||||||
|
*/
|
||||||
object HaClient {
|
object HaClient {
|
||||||
private var serviceInstance: HaApiService? = null
|
private var serviceInstance: HaApiService? = null
|
||||||
|
|
||||||
private val gson = GsonBuilder()
|
private val gson = GsonBuilder()
|
||||||
.registerTypeAdapter(HaState::class.java, HaStateAdapter())
|
.registerTypeAdapter(HaState::class.java, HaStateAdapter())
|
||||||
.create()
|
.create()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a cached or newly created [HaApiService] based on saved preferences.
|
||||||
|
*/
|
||||||
fun getService(context: Context): HaApiService {
|
fun getService(context: Context): HaApiService {
|
||||||
val url = Prefs.getUrl(context)
|
val url = Prefs.getUrl(context)
|
||||||
val token = Prefs.getToken(context)
|
val token = Prefs.getToken(context)
|
||||||
return serviceInstance ?: buildService(url, token).also { serviceInstance = it }
|
return serviceInstance ?: buildService(url, token).also { serviceInstance = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Clears the current service instance, forcing a rebuild on next request. */
|
||||||
fun clearCache() {
|
fun clearCache() {
|
||||||
serviceInstance = null
|
serviceInstance = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates a temporary [HaApiService] instance with specific credentials for testing. */
|
||||||
fun getServiceForTest(url: String, token: String): HaApiService {
|
fun getServiceForTest(url: String, token: String): HaApiService {
|
||||||
return buildService(url, token)
|
return buildService(url, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildService(url: String, token: String): HaApiService {
|
private fun buildService(url: String, token: String): HaApiService {
|
||||||
val baseUrl = if (url.endsWith("/")) url else "$url/"
|
val baseUrl = if (url.endsWith("/")) url else "$url/"
|
||||||
val okHttpClient = OkHttpClient.Builder()
|
val okHttpClient = OkHttpClient.Builder()
|
||||||
@@ -32,6 +47,7 @@ object HaClient {
|
|||||||
chain.proceed(request)
|
chain.proceed(request)
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
.baseUrl(baseUrl)
|
.baseUrl(baseUrl)
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
|
|||||||
@@ -3,9 +3,18 @@ import com.google.gson.TypeAdapter
|
|||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
import com.google.gson.stream.JsonToken
|
import com.google.gson.stream.JsonToken
|
||||||
import com.google.gson.stream.JsonWriter
|
import com.google.gson.stream.JsonWriter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom GSON TypeAdapter for [HaState].
|
||||||
|
* Manually parses the Home Assistant state object to handle dynamic attributes
|
||||||
|
* and deep nesting while maintaining high performance.
|
||||||
|
*/
|
||||||
class HaStateAdapter : TypeAdapter<HaState>() {
|
class HaStateAdapter : TypeAdapter<HaState>() {
|
||||||
|
/** Writing is not implemented as this adapter is read-only for current states. */
|
||||||
override fun write(out: JsonWriter, value: HaState?) {
|
override fun write(out: JsonWriter, value: HaState?) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parses the JSON response from HA into a [HaState] object. */
|
||||||
override fun read(reader: JsonReader): HaState {
|
override fun read(reader: JsonReader): HaState {
|
||||||
var entityId = ""
|
var entityId = ""
|
||||||
var state = ""
|
var state = ""
|
||||||
|
|||||||
@@ -4,18 +4,27 @@ import android.graphics.Canvas
|
|||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.example.retroha.theme.Colors
|
import com.example.retroha.theme.Colors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom checkbox view designed with Bauhaus aesthetics.
|
||||||
|
* Uses sharp geometric shapes and high-contrast colors.
|
||||||
|
*/
|
||||||
class BauhausCheckbox(context: Context) : View(context) {
|
class BauhausCheckbox(context: Context) : View(context) {
|
||||||
private val density = resources.displayMetrics.density
|
private val density = resources.displayMetrics.density
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
|
||||||
|
/** Current state of the checkbox. Triggers redraw on change. */
|
||||||
var isChecked: Boolean = false
|
var isChecked: Boolean = false
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
val size = (24 * density).toInt()
|
val size = (24 * density).toInt()
|
||||||
setMeasuredDimension(size, size)
|
setMeasuredDimension(size, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
val size = width.toFloat()
|
val size = width.toFloat()
|
||||||
val b = 2 * density
|
val b = 2 * density
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.example.retroha.network.HaState
|
import com.example.retroha.network.HaState
|
||||||
import com.example.retroha.theme.Colors
|
import com.example.retroha.theme.Colors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter for the entity browser list.
|
||||||
|
* Displays Home Assistant entities with a [BauhausCheckbox] for selection.
|
||||||
|
*/
|
||||||
class EntitySelectionAdapter(
|
class EntitySelectionAdapter(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val items: List<HaState>,
|
private val items: List<HaState>,
|
||||||
@@ -16,6 +21,8 @@ class EntitySelectionAdapter(
|
|||||||
override fun getCount() = items.size
|
override fun getCount() = items.size
|
||||||
override fun getItem(position: Int) = items[position]
|
override fun getItem(position: Int) = items[position]
|
||||||
override fun getItemId(position: Int) = position.toLong()
|
override fun getItemId(position: Int) = position.toLong()
|
||||||
|
|
||||||
|
/** Resolves the view for a specific row in the list. */
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
val layout = (convertView as? LinearLayout) ?: createLayout()
|
val layout = (convertView as? LinearLayout) ?: createLayout()
|
||||||
val checkbox = layout.getChildAt(0) as BauhausCheckbox
|
val checkbox = layout.getChildAt(0) as BauhausCheckbox
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
package com.example.retroha.ui
|
package com.example.retroha.ui
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central repository for custom Typefaces used in the application.
|
||||||
|
* Ensures consistent typography across manually drawn UI components.
|
||||||
|
*/
|
||||||
object Fonts {
|
object Fonts {
|
||||||
|
/** The standard monospace typeface for regular text. */
|
||||||
val REGULAR: Typeface by lazy { Typeface.create("monospace", Typeface.NORMAL) }
|
val REGULAR: Typeface by lazy { Typeface.create("monospace", Typeface.NORMAL) }
|
||||||
|
/** The bold monospace typeface for headers and labels. */
|
||||||
val BOLD: Typeface by lazy { Typeface.create("monospace", Typeface.BOLD) }
|
val BOLD: Typeface by lazy { Typeface.create("monospace", Typeface.BOLD) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import android.graphics.Paint
|
|||||||
import android.graphics.Path
|
import android.graphics.Path
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import com.example.retroha.theme.Colors
|
import com.example.retroha.theme.Colors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for drawing Home Assistant domain icons directly onto a [Canvas].
|
||||||
|
* Uses purely geometric Bauhaus-style shapes for high performance and stylistic consistency.
|
||||||
|
*/
|
||||||
object HaIcons {
|
object HaIcons {
|
||||||
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
style = Paint.Style.FILL
|
style = Paint.Style.FILL
|
||||||
@@ -14,6 +19,17 @@ object HaIcons {
|
|||||||
}
|
}
|
||||||
private val tmpRect = RectF()
|
private val tmpRect = RectF()
|
||||||
private val tmpPath = Path()
|
private val tmpPath = Path()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a domain-specific icon.
|
||||||
|
*
|
||||||
|
* @param canvas The canvas to draw on.
|
||||||
|
* @param domain The HA domain string (e.g., "light", "switch").
|
||||||
|
* @param x X coordinate for the top-left corner of the icon bounds.
|
||||||
|
* @param y Y coordinate for the top-left corner of the icon bounds.
|
||||||
|
* @param size Diameter/Side length of the square icon area.
|
||||||
|
* @param color The color to use for rendering.
|
||||||
|
*/
|
||||||
fun draw(canvas: Canvas, domain: String, x: Float, y: Float, size: Float, color: Int) {
|
fun draw(canvas: Canvas, domain: String, x: Float, y: Float, size: Float, color: Int) {
|
||||||
fillPaint.color = color
|
fillPaint.color = color
|
||||||
strokePaint.color = color
|
strokePaint.color = color
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ import android.graphics.Paint
|
|||||||
import android.graphics.PixelFormat
|
import android.graphics.PixelFormat
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import com.example.retroha.theme.Colors
|
import com.example.retroha.theme.Colors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom [Drawable] representing a simplified globe icon.
|
||||||
|
* Used in the UI to indicate language settings.
|
||||||
|
*
|
||||||
|
* @param sizePx The intrinsic size of the drawable in pixels.
|
||||||
|
*/
|
||||||
class LanguageIconDrawable(private val sizePx: Int) : Drawable() {
|
class LanguageIconDrawable(private val sizePx: Int) : Drawable() {
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
style = Paint.Style.STROKE
|
style = Paint.Style.STROKE
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.example.retroha.theme.Colors
|
import com.example.retroha.theme.Colors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom dialog for controlling the brightness of a light entity.
|
||||||
|
* Features a minimalist Bauhaus design and automatic updates on slider release.
|
||||||
|
*
|
||||||
|
* @param context The activity context.
|
||||||
|
* @param entityName Friendly name of the light to display.
|
||||||
|
* @param initialBrightness The brightness level (0-255) when the dialog is opened.
|
||||||
|
* @param onBrightnessChanged Callback triggered when the user finishes adjusting the slider.
|
||||||
|
*/
|
||||||
class LightControlDialog(
|
class LightControlDialog(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val entityName: String,
|
private val entityName: String,
|
||||||
|
|||||||
@@ -4,22 +4,37 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.BaseAdapter
|
import android.widget.BaseAdapter
|
||||||
import com.example.retroha.model.WidgetConfig
|
import com.example.retroha.model.WidgetConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter for the main dashboard grid.
|
||||||
|
* Manages the lifecycle and data binding for [WidgetCardView] instances.
|
||||||
|
*/
|
||||||
class WidgetAdapter(
|
class WidgetAdapter(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
initialItems: List<WidgetConfig>
|
initialItems: List<WidgetConfig>
|
||||||
) : BaseAdapter() {
|
) : BaseAdapter() {
|
||||||
private val items = mutableListOf<WidgetConfig>().apply { addAll(initialItems) }
|
private val items = mutableListOf<WidgetConfig>().apply { addAll(initialItems) }
|
||||||
|
|
||||||
|
/** Callback for short clicks (toggle/execute action). */
|
||||||
var onToggle: ((WidgetConfig) -> Unit)? = null
|
var onToggle: ((WidgetConfig) -> Unit)? = null
|
||||||
|
/** Callback for long clicks (more info / brightness dialog). */
|
||||||
var onLongToggle: ((WidgetConfig) -> Unit)? = null
|
var onLongToggle: ((WidgetConfig) -> Unit)? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the list of items displayed in the grid.
|
||||||
|
*/
|
||||||
fun updateItems(newItems: List<WidgetConfig>) {
|
fun updateItems(newItems: List<WidgetConfig>) {
|
||||||
items.clear()
|
items.clear()
|
||||||
items.addAll(newItems)
|
items.addAll(newItems)
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCount() = items.size
|
override fun getCount() = items.size
|
||||||
override fun getItem(position: Int) = items[position]
|
override fun getItem(position: Int) = items[position]
|
||||||
override fun getItemId(position: Int) = items[position].id
|
override fun getItemId(position: Int) = items[position].id
|
||||||
override fun hasStableIds() = true
|
override fun hasStableIds() = true
|
||||||
|
|
||||||
|
/** Provides or recycles a [WidgetCardView] for the grid. */
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
val card = convertView as? WidgetCardView ?: WidgetCardView(context).also {
|
val card = convertView as? WidgetCardView ?: WidgetCardView(context).also {
|
||||||
it.onToggle = onToggle
|
it.onToggle = onToggle
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import com.example.retroha.model.WidgetConfig
|
|||||||
import com.example.retroha.model.WidgetInteraction
|
import com.example.retroha.model.WidgetInteraction
|
||||||
import com.example.retroha.model.toWidgetInteraction
|
import com.example.retroha.model.toWidgetInteraction
|
||||||
import com.example.retroha.theme.Colors
|
import com.example.retroha.theme.Colors
|
||||||
|
/**
|
||||||
|
* A highly optimized custom [View] that renders a single Home Assistant entity card.
|
||||||
|
* Part of the "Bauhaus Canvas" engine, it avoids XML overhead by manually drawing everything.
|
||||||
|
* Supports state-based coloring, pulsing animations for transitions, and haptic feedback.
|
||||||
|
*/
|
||||||
class WidgetCardView(context: Context) : View(context) {
|
class WidgetCardView(context: Context) : View(context) {
|
||||||
private val density = resources.displayMetrics.density
|
private val density = resources.displayMetrics.density
|
||||||
private val borderPx = dp(2)
|
private val borderPx = dp(2)
|
||||||
@@ -41,7 +46,10 @@ class WidgetCardView(context: Context) : View(context) {
|
|||||||
color = Colors.GRAY_MID; textSize = sp(10); typeface = Fonts.REGULAR
|
color = Colors.GRAY_MID; textSize = sp(10); typeface = Fonts.REGULAR
|
||||||
}
|
}
|
||||||
private val pulseInterpolator = DecelerateInterpolator()
|
private val pulseInterpolator = DecelerateInterpolator()
|
||||||
|
|
||||||
|
/** Callback for short clicks (toggle/execute action). */
|
||||||
var onToggle: ((WidgetConfig) -> Unit)? = null
|
var onToggle: ((WidgetConfig) -> Unit)? = null
|
||||||
|
/** Callback for long clicks (more info / brightness dialog). */
|
||||||
var onLongToggle: ((WidgetConfig) -> Unit)? = null
|
var onLongToggle: ((WidgetConfig) -> Unit)? = null
|
||||||
private var config: WidgetConfig? = null
|
private var config: WidgetConfig? = null
|
||||||
private var currentInteraction: WidgetInteraction = WidgetInteraction.READ_ONLY
|
private var currentInteraction: WidgetInteraction = WidgetInteraction.READ_ONLY
|
||||||
@@ -74,6 +82,12 @@ class WidgetCardView(context: Context) : View(context) {
|
|||||||
updateExecIconPath(w - shadowPx)
|
updateExecIconPath(w - shadowPx)
|
||||||
rebuildLayoutsIfNeeded(w)
|
rebuildLayoutsIfNeeded(w)
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Binds new data to the widget and updates the visual state.
|
||||||
|
* Triggers layout recalculation if text content or view width has changed.
|
||||||
|
*
|
||||||
|
* @param cfg The new configuration data for this widget.
|
||||||
|
*/
|
||||||
fun bind(cfg: WidgetConfig) {
|
fun bind(cfg: WidgetConfig) {
|
||||||
config = cfg
|
config = cfg
|
||||||
currentInteraction = cfg.domain.toWidgetInteraction()
|
currentInteraction = cfg.domain.toWidgetInteraction()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">RetroHA</string>
|
<string name="app_name">RetroHA</string>
|
||||||
<string name="app_title">HA_PANEL</string>
|
<string name="app_title">RetroHA</string>
|
||||||
<string name="state_on">ON</string>
|
<string name="state_on">ON</string>
|
||||||
<string name="state_off">OFF</string>
|
<string name="state_off">OFF</string>
|
||||||
<string name="state_unavailable">UNAVAILABLE</string>
|
<string name="state_unavailable">UNAVAILABLE</string>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<string name="instruction_1">1. CLICK TILE TO TOGGLE (ON/OFF).</string>
|
<string name="instruction_1">1. CLICK TILE TO TOGGLE (ON/OFF).</string>
|
||||||
<string name="instruction_2">2. LONG PRESS LIGHT TILE TO OPEN BRIGHTNESS MENU.</string>
|
<string name="instruction_2">2. LONG PRESS LIGHT TILE TO OPEN BRIGHTNESS MENU.</string>
|
||||||
<string name="instruction_3">3. USE TABS AT TOP TO FILTER DEVICES.</string>
|
<string name="instruction_3">3. USE TABS AT TOP TO FILTER DEVICES.</string>
|
||||||
<string name="instruction_4">4. CLICK \'HA_PANEL\' TITLE TO FORCE REFRESH.</string>
|
<string name="instruction_4">4. CLICK \'RetroHA\' TITLE TO FORCE REFRESH.</string>
|
||||||
<string name="instruction_5">5. CONNECTION CHANGE CLEARS WIDGET LIST.</string>
|
<string name="instruction_5">5. CONNECTION CHANGE CLEARS WIDGET LIST.</string>
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">RetroHA</string>
|
<string name="app_name">RetroHA</string>
|
||||||
<string name="app_title">HA_PANEL</string>
|
<string name="app_title">RetroHA</string>
|
||||||
<string name="state_on">WŁ.</string>
|
<string name="state_on">WŁ.</string>
|
||||||
<string name="state_off">WYŁ.</string>
|
<string name="state_off">WYŁ.</string>
|
||||||
<string name="state_unavailable">NIEOSIĄGALNE</string>
|
<string name="state_unavailable">NIEOSIĄGALNE</string>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<string name="instruction_1">1. KLIKNIJ KAFELEK, ABY PRZEŁĄCZYĆ (ON/OFF).</string>
|
<string name="instruction_1">1. KLIKNIJ KAFELEK, ABY PRZEŁĄCZYĆ (ON/OFF).</string>
|
||||||
<string name="instruction_2">2. PRZYTRZYMAJ KAFELEK ŚWIATŁA, ABY OTWORZYĆ MENU JASNOŚCI.</string>
|
<string name="instruction_2">2. PRZYTRZYMAJ KAFELEK ŚWIATŁA, ABY OTWORZYĆ MENU JASNOŚCI.</string>
|
||||||
<string name="instruction_3">3. UŻYWAJ ZAKŁADEK NA GÓRZE DO FILTROWANIA URZĄDZEŃ.</string>
|
<string name="instruction_3">3. UŻYWAJ ZAKŁADEK NA GÓRZE DO FILTROWANIA URZĄDZEŃ.</string>
|
||||||
<string name="instruction_4">4. KLIKNIJ TYTUŁ \'HA_PANEL\', ABY WYMUSIĆ ODŚWIEŻENIE.</string>
|
<string name="instruction_4">4. KLIKNIJ TYTUŁ \'RetroHA\', ABY WYMUSIĆ ODŚWIEŻENIE.</string>
|
||||||
<string name="instruction_5">5. ZMIANA POŁĄCZENIA CZYŚCI LISTĘ WIDŻETÓW.</string>
|
<string name="instruction_5">5. ZMIANA POŁĄCZENIA CZYŚCI LISTĘ WIDŻETÓW.</string>
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,50 +1,102 @@
|
|||||||
package com.example.retroha.i18n
|
package com.example.retroha.i18n
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Universal identifiers for translatable strings used across the application.
|
||||||
|
* Each key maps to a localized string in the platform-specific implementation.
|
||||||
|
*/
|
||||||
enum class StringKey {
|
enum class StringKey {
|
||||||
|
/** Label for 'ON' state. */
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
|
/** Label for 'OFF' state. */
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
|
/** Label for 'UNAVAILABLE' state. */
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
|
/** Label for transitioning state. */
|
||||||
STATE_TOGGLING,
|
STATE_TOGGLING,
|
||||||
|
/** Button text for Settings. */
|
||||||
BTN_SETTINGS,
|
BTN_SETTINGS,
|
||||||
|
/** Placeholder toast for adding widgets. */
|
||||||
TOAST_WIDGET_ADD,
|
TOAST_WIDGET_ADD,
|
||||||
|
/** Tab label: All. */
|
||||||
TAB_ALL,
|
TAB_ALL,
|
||||||
|
/** Tab label: Lighting. */
|
||||||
TAB_LIGHTING,
|
TAB_LIGHTING,
|
||||||
|
/** Tab label: Sockets. */
|
||||||
TAB_SOCKETS,
|
TAB_SOCKETS,
|
||||||
|
/** Tab label: Power. */
|
||||||
TAB_POWER,
|
TAB_POWER,
|
||||||
|
/** Tab label: Weather. */
|
||||||
TAB_WEATHER,
|
TAB_WEATHER,
|
||||||
|
/** Title for entity selection screen. */
|
||||||
TITLE_SETTINGS,
|
TITLE_SETTINGS,
|
||||||
|
/** Title for connection configuration screen. */
|
||||||
TITLE_CONNECTION,
|
TITLE_CONNECTION,
|
||||||
|
/** Title for user manual screen. */
|
||||||
TITLE_INSTRUCTIONS,
|
TITLE_INSTRUCTIONS,
|
||||||
|
/** Instruction line 1. */
|
||||||
INSTRUCTION_1,
|
INSTRUCTION_1,
|
||||||
|
/** Instruction line 2. */
|
||||||
INSTRUCTION_2,
|
INSTRUCTION_2,
|
||||||
|
/** Instruction line 3. */
|
||||||
INSTRUCTION_3,
|
INSTRUCTION_3,
|
||||||
|
/** Instruction line 4. */
|
||||||
INSTRUCTION_4,
|
INSTRUCTION_4,
|
||||||
|
/** Instruction line 5. */
|
||||||
INSTRUCTION_5,
|
INSTRUCTION_5,
|
||||||
|
/** Input label for URL. */
|
||||||
LABEL_URL,
|
LABEL_URL,
|
||||||
|
/** Input label for Access Token. */
|
||||||
LABEL_TOKEN,
|
LABEL_TOKEN,
|
||||||
|
/** Input label for refresh interval. */
|
||||||
LABEL_REFRESH,
|
LABEL_REFRESH,
|
||||||
|
/** Button text for testing and saving connection. */
|
||||||
BTN_TEST_SAVE,
|
BTN_TEST_SAVE,
|
||||||
|
/** Button text for deleting all widgets. */
|
||||||
BTN_DELETE_ALL,
|
BTN_DELETE_ALL,
|
||||||
|
/** Button text for saving selected entities. */
|
||||||
BTN_SAVE_SELECTED,
|
BTN_SAVE_SELECTED,
|
||||||
|
/** Button text for language change. */
|
||||||
BTN_CHANGE_LANG,
|
BTN_CHANGE_LANG,
|
||||||
|
/** Status: Connecting. */
|
||||||
STATUS_CONNECTING,
|
STATUS_CONNECTING,
|
||||||
|
/** Status: Successfully connected / Online. */
|
||||||
STATUS_CONNECTED,
|
STATUS_CONNECTED,
|
||||||
|
/** Status: Offline. */
|
||||||
STATUS_OFFLINE,
|
STATUS_OFFLINE,
|
||||||
|
/** Status error: Home Assistant reported an error. */
|
||||||
STATUS_ERROR_HA,
|
STATUS_ERROR_HA,
|
||||||
|
/** Status error: Authentication token missing. */
|
||||||
STATUS_NO_TOKEN,
|
STATUS_NO_TOKEN,
|
||||||
|
/** Title for language selection screen. */
|
||||||
TITLE_SELECT_LANGUAGE,
|
TITLE_SELECT_LANGUAGE,
|
||||||
|
/** Toast: Refreshing data. */
|
||||||
STATUS_REFRESHING,
|
STATUS_REFRESHING,
|
||||||
|
/** Status: Success message. */
|
||||||
STATUS_SUCCESS,
|
STATUS_SUCCESS,
|
||||||
|
/** Toast: Configuration saved and widgets cleared. */
|
||||||
TOAST_SAVED_CLEARED,
|
TOAST_SAVED_CLEARED,
|
||||||
|
/** General error status. */
|
||||||
STATUS_ERROR,
|
STATUS_ERROR,
|
||||||
|
/** Network connection error status. */
|
||||||
STATUS_ERROR_NETWORK,
|
STATUS_ERROR_NETWORK,
|
||||||
|
/** Confirmation message for mass deletion. */
|
||||||
CONFIRM_DELETE_ALL,
|
CONFIRM_DELETE_ALL,
|
||||||
|
/** Warning message for connection change. */
|
||||||
CONFIRM_CHANGE_CONN,
|
CONFIRM_CHANGE_CONN,
|
||||||
|
/** Label for brightness level. */
|
||||||
DIALOG_BRIGHTNESS,
|
DIALOG_BRIGHTNESS,
|
||||||
|
/** Dialog button: Set/Apply. */
|
||||||
DIALOG_SET,
|
DIALOG_SET,
|
||||||
|
/** Dialog button: Cancel. */
|
||||||
DIALOG_CANCEL,
|
DIALOG_CANCEL,
|
||||||
|
/** Dialog title: Warning. */
|
||||||
DIALOG_WARNING,
|
DIALOG_WARNING,
|
||||||
|
/** Dialog button: Confirm connection change. */
|
||||||
DIALOG_YES_CHANGE,
|
DIALOG_YES_CHANGE,
|
||||||
|
/** Dialog button: Confirm deletion. */
|
||||||
DIALOG_YES_DELETE,
|
DIALOG_YES_DELETE,
|
||||||
|
/** Toast: Cache/Dashboard cleared. */
|
||||||
TOAST_CLEARED,
|
TOAST_CLEARED,
|
||||||
|
/** Toast: Selected entities saved. */
|
||||||
TOAST_ENTITIES_SAVED
|
TOAST_ENTITIES_SAVED
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
package com.example.retroha.i18n
|
package com.example.retroha.i18n
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface defining the contract for retrieving localized strings.
|
||||||
|
* This allows the business logic to remain platform-independent.
|
||||||
|
*/
|
||||||
interface Strings {
|
interface Strings {
|
||||||
|
/**
|
||||||
|
* Retrieves a localized string for the given key.
|
||||||
|
* @param key The universal string identifier.
|
||||||
|
* @return The localized text for the current application language.
|
||||||
|
*/
|
||||||
operator fun get(key: StringKey): String
|
operator fun get(key: StringKey): String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,16 @@
|
|||||||
package com.example.retroha.model
|
package com.example.retroha.model
|
||||||
enum class EntityState { ON, OFF, UNAVAILABLE, TOGGLING }
|
|
||||||
|
/**
|
||||||
|
* Represents the fundamental visual and logical states of a Home Assistant entity.
|
||||||
|
* Used by the UI layer to determine colors and animations.
|
||||||
|
*/
|
||||||
|
enum class EntityState {
|
||||||
|
/** Entity is active (e.g., light is on, switch is closed). */
|
||||||
|
ON,
|
||||||
|
/** Entity is inactive (e.g., light is off, switch is open). */
|
||||||
|
OFF,
|
||||||
|
/** Entity is disconnected or its state cannot be determined. */
|
||||||
|
UNAVAILABLE,
|
||||||
|
/** Entity is currently transitioning between states (e.g., command sent but not confirmed). */
|
||||||
|
TOGGLING
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,18 @@
|
|||||||
package com.example.retroha.model
|
package com.example.retroha.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data configuration class for a UI widget.
|
||||||
|
* Holds all necessary information to render a Home Assistant entity on the dashboard.
|
||||||
|
*
|
||||||
|
* @property id Unique local identifier for the widget instance.
|
||||||
|
* @property entityId The full Home Assistant entity ID (e.g., "light.living_room").
|
||||||
|
* @property label The display name shown on the card top section.
|
||||||
|
* @property value The primary state value or measurement (e.g., "ON", "21.5°C").
|
||||||
|
* @property secondary Subtitle or additional info (e.g., battery level, wattage).
|
||||||
|
* @property domain The HA domain (light, switch, sensor, etc.) used for icon selection.
|
||||||
|
* @property state Current logical state for UI coloring and animations.
|
||||||
|
* @property brightness Optional brightness value (0-255) for light-domain entities.
|
||||||
|
*/
|
||||||
data class WidgetConfig(
|
data class WidgetConfig(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val entityId: String,
|
val entityId: String,
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
package com.example.retroha.model
|
package com.example.retroha.model
|
||||||
enum class WidgetInteraction { TOGGLE, EXECUTE, READ_ONLY }
|
|
||||||
|
/**
|
||||||
|
* Defines the type of user interaction supported by a widget.
|
||||||
|
*/
|
||||||
|
enum class WidgetInteraction {
|
||||||
|
/** Supports switching between ON and OFF states. */
|
||||||
|
TOGGLE,
|
||||||
|
/** Supports one-shot execution (e.g., triggering a script). */
|
||||||
|
EXECUTE,
|
||||||
|
/** Information only, no user interaction allowed. */
|
||||||
|
READ_ONLY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a Home Assistant domain string to its appropriate [WidgetInteraction] type.
|
||||||
|
*
|
||||||
|
* @return The interaction type (TOGGLE, EXECUTE, or READ_ONLY) based on the domain logic.
|
||||||
|
*/
|
||||||
fun String.toWidgetInteraction(): WidgetInteraction = when (this) {
|
fun String.toWidgetInteraction(): WidgetInteraction = when (this) {
|
||||||
"light",
|
"light",
|
||||||
"switch",
|
"switch",
|
||||||
|
|||||||
@@ -1,28 +1,65 @@
|
|||||||
package com.example.retroha.theme
|
package com.example.retroha.theme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bauhaus-inspired color palette for the application.
|
||||||
|
* All colors are defined as ARGB integers.
|
||||||
|
*/
|
||||||
object Colors {
|
object Colors {
|
||||||
|
/** Pure black for borders, shadows, and text. */
|
||||||
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()
|
||||||
|
|
||||||
|
// Semantic status colors
|
||||||
|
/** Card background color when entity is ON. */
|
||||||
const val STATUS_ON = YELLOW
|
const val STATUS_ON = YELLOW
|
||||||
|
/** Card background color when entity is OFF. */
|
||||||
const val STATUS_OFF = 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. */
|
||||||
const val STATUS_TOGGLING = WHITE
|
const val STATUS_TOGGLING = WHITE
|
||||||
|
|
||||||
|
// Semantic border colors
|
||||||
|
/** Default card border color. */
|
||||||
const val BORDER_DEFAULT = BLACK
|
const val BORDER_DEFAULT = BLACK
|
||||||
|
/** Border color during state transition. */
|
||||||
const val BORDER_TOGGLING = BLUE
|
const val BORDER_TOGGLING = BLUE
|
||||||
|
/** Border color when entity is unavailable. */
|
||||||
const val BORDER_UNAVAILABLE = GRAY_MID
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user