Initial commit

This commit is contained in:
Krzysztof Cieślik
2026-06-13 21:43:53 +02:00
commit 22a3e0fe7e
80 changed files with 4175 additions and 0 deletions

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

72
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,72 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.dokka)
}
android {
namespace = "com.example.retroha"
compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig {
applicationId = "com.example.retroha"
minSdk = 14
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled = false
vectorDrawables.useSupportLibrary = true
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
splits {
abi {
isEnable = true
reset()
include("armeabi-v7a", "x86", "x86_64")
isUniversalApk = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
implementation(project(":shared"))
// OkHttp 3.12.x — ostatnia wersja wspierająca API 19
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
// Retrofit 2 + Gson converter
implementation(libs.retrofit)
implementation(libs.retrofit.gson)
implementation(libs.gson)
// MPAndroidChart
implementation(libs.mpandroidchart)
testImplementation(libs.junit)
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test:runner:1.5.2")
androidTestImplementation("androidx.test:rules:1.5.0")
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,50 @@
package com.example.retroha
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
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 AppNavigationTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testSettingsNavigationAndForms() {
Thread.sleep(1000)
onView(withId(R.id.btnSettings)).perform(click())
Thread.sleep(500)
onView(withId(R.id.btnEntitySelection)).check(matches(isDisplayed()))
onView(withId(R.id.btnInstructions)).check(matches(isDisplayed()))
onView(withId(R.id.btnGoToConnection)).check(matches(isDisplayed()))
onView(withId(R.id.btnEntitySelection)).perform(click())
Thread.sleep(500)
onView(withId(R.id.etSearch)).check(matches(isDisplayed()))
onView(withId(R.id.etSearch)).perform(replaceText("light"), closeSoftKeyboard())
Thread.sleep(500)
pressBack()
Thread.sleep(500)
onView(withId(R.id.btnInstructions)).perform(click())
Thread.sleep(500)
pressBack()
Thread.sleep(500)
onView(withId(R.id.btnGoToConnection)).perform(click())
Thread.sleep(500)
onView(withId(R.id.etUrl)).check(matches(isDisplayed()))
onView(withId(R.id.etToken)).check(matches(isDisplayed()))
onView(withId(R.id.etRefreshInterval)).perform(replaceText("60"), closeSoftKeyboard())
Thread.sleep(500)
pressBack()
Thread.sleep(500)
pressBack()
Thread.sleep(500)
onView(withId(R.id.gridView)).check(matches(isDisplayed()))
}
}

View File

@@ -0,0 +1,23 @@
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 tabs = listOf("OŚWIETLENIE", "GNIAZDKA", "MOC", "POGODA", "WSZYSTKO")
tabs.forEach { tabName ->
onView(withText(tabName)).perform(click())
Thread.sleep(500)
}
}
}

View File

@@ -0,0 +1,14 @@
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)
}
}

View File

@@ -0,0 +1,65 @@
package com.example.retroha
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.withId
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
import java.util.Random
import androidx.test.espresso.ViewInteraction
import android.view.View
import org.hamcrest.Matcher
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import android.view.ViewGroup
import android.widget.GridView
import org.hamcrest.Description
import org.hamcrest.TypeSafeMatcher
@RunWith(AndroidJUnit4::class)
class MonkeyStressTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
private val random = Random()
@Test
fun runMonkeyTest() {
val iterations = 50
val tabs = listOf("WSZYSTKO", "OŚWIETLENIE", "GNIAZDKA", "MOC", "POGODA")
for (i in 1..iterations) {
val actionType = random.nextInt(3)
try {
when (actionType) {
0 -> {
val tab = tabs[random.nextInt(tabs.size)]
onView(withText(tab)).perform(click())
}
1 -> {
onView(withId(R.id.gridView)).perform(clickRandomItem())
}
2 -> {
onView(withId(R.id.btnSettings)).perform(click())
Thread.sleep(500)
androidx.test.espresso.Espresso.pressBack()
}
}
} catch (e: Exception) {
}
Thread.sleep(200)
}
}
private fun clickRandomItem(): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> = withId(R.id.gridView)
override fun getDescription(): String = "Kliknięcie losowego elementu w GridView"
override fun perform(uiController: UiController, view: View) {
val gridView = view as GridView
if (gridView.childCount > 0) {
val randomIndex = random.nextInt(gridView.childCount)
gridView.getChildAt(randomIndex).performClick()
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/Theme.RetroHA">
<activity
android:name=".LanguageActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MainActivity" />
<activity android:name=".SettingsActivity" />
<activity android:name=".ConnectionSettingsActivity" />
<activity android:name=".InstructionsActivity" />
<activity android:name=".EntitySelectionActivity" />
</application>
</manifest>

View File

@@ -0,0 +1,10 @@
package com.example.retroha
import android.app.Activity
import android.os.Bundle
import com.example.retroha.i18n.LocaleHelper
abstract class BaseActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
LocaleHelper.setLocale(this)
super.onCreate(savedInstanceState)
}
}

View File

@@ -0,0 +1,72 @@
package com.example.retroha
import android.app.Activity
import android.os.Bundle
import android.widget.*
import com.example.retroha.data.Prefs
import com.example.retroha.network.HaClient
import com.example.retroha.network.HaState
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class ConnectionSettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_connection_settings)
val etUrl = findViewById<EditText>(R.id.etUrl)
val etToken = findViewById<EditText>(R.id.etToken)
val etRefreshInterval = findViewById<EditText>(R.id.etRefreshInterval)
val btnSave = findViewById<Button>(R.id.btnTestAndSave)
val tvStatus = findViewById<TextView>(R.id.tvStatus)
etUrl.setText(Prefs.getUrl(this))
etToken.setText(Prefs.getToken(this))
etRefreshInterval.setText((Prefs.getRefreshInterval(this) / 1000).toString())
btnSave.setOnClickListener {
val url = etUrl.text.toString()
val token = etToken.text.toString()
val intervalSec = etRefreshInterval.text.toString().toLongOrNull() ?: 30L
val strings = com.example.retroha.i18n.AndroidStrings(this)
android.app.AlertDialog.Builder(this)
.setTitle("UWAGA")
.setMessage(strings.get(com.example.retroha.i18n.StringKey.CONFIRM_CHANGE_CONN))
.setPositiveButton("TAK, ZMIEŃ") { _, _ ->
performTestAndSave(url, token, intervalSec, tvStatus)
}
.setNegativeButton(strings.get(com.example.retroha.i18n.StringKey.DIALOG_ANULUJ), null)
.show()
}
}
private fun performTestAndSave(url: String, token: String, intervalSec: Long, tvStatus: TextView) {
val strings = com.example.retroha.i18n.AndroidStrings(this)
tvStatus.text = strings.get(com.example.retroha.i18n.StringKey.STATUS_CONNECTING)
tvStatus.setTextColor(0xFF000000.toInt())
val testClient = HaClient.getServiceForTest(url, token)
testClient.getStates().enqueue(object : Callback<List<HaState>> {
override fun onResponse(call: Call<List<HaState>>, response: Response<List<HaState>>) {
if (response.isSuccessful) {
Prefs.setUrl(this@ConnectionSettingsActivity, url)
Prefs.setToken(this@ConnectionSettingsActivity, token)
Prefs.setRefreshInterval(this@ConnectionSettingsActivity, intervalSec * 1000L)
Prefs.setSelectedEntities(this@ConnectionSettingsActivity, emptySet())
HaClient.clearCache()
runOnUiThread {
tvStatus.text = "POŁĄCZONO POMYŚLNIE"
tvStatus.setTextColor(0xFF0056B3.toInt())
Toast.makeText(this@ConnectionSettingsActivity, "Zapisano i wyczyszczono widżety", Toast.LENGTH_SHORT).show()
finish()
}
} else {
runOnUiThread {
tvStatus.text = "BŁĄD: ${response.code()}"
tvStatus.setTextColor(0xFFE23A24.toInt())
}
}
}
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
runOnUiThread {
tvStatus.text = "BŁĄD SIECI: ${t.message}"
tvStatus.setTextColor(0xFFE23A24.toInt())
}
}
})
}
}

View File

@@ -0,0 +1,88 @@
package com.example.retroha
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.widget.*
import com.example.retroha.data.Prefs
import com.example.retroha.network.HaClient
import com.example.retroha.network.HaState
import com.example.retroha.ui.EntitySelectionAdapter
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class EntitySelectionActivity : BaseActivity() {
private lateinit var etSearch: EditText
private lateinit var lvEntities: ListView
private var allEntities = mutableListOf<HaState>()
private var filteredEntities = mutableListOf<HaState>()
private var selectedEntities = mutableSetOf<String>()
private var adapter: EntitySelectionAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entity_selection)
etSearch = findViewById(R.id.etSearch)
lvEntities = findViewById(R.id.lvEntities)
val btnSave = findViewById<Button>(R.id.btnSave)
selectedEntities.addAll(Prefs.getSelectedEntities(this))
btnSave.setOnClickListener {
Prefs.setSelectedEntities(this, selectedEntities)
Toast.makeText(this, "Wybrane encje zapisane", Toast.LENGTH_SHORT).show()
finish()
}
etSearch.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) { filterEntities(s.toString()) }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
lvEntities.onItemClickListener = AdapterView.OnItemClickListener { _, view, position, _ ->
val entityId = filteredEntities[position].entity_id
if (selectedEntities.contains(entityId)) {
selectedEntities.remove(entityId)
} else {
selectedEntities.add(entityId)
}
val layout = view as? LinearLayout
val checkbox = layout?.getChildAt(0) as? com.example.retroha.ui.BauhausCheckbox
checkbox?.isChecked = selectedEntities.contains(entityId)
}
fetchEntities()
}
private fun fetchEntities() {
val token = Prefs.getToken(this)
if (token.isEmpty()) return
HaClient.getService(this).getStates().enqueue(object : Callback<List<HaState>> {
override fun onResponse(call: Call<List<HaState>>, response: Response<List<HaState>>) {
if (response.isSuccessful) {
allEntities.clear()
allEntities.addAll(response.body() ?: emptyList())
allEntities.sortBy { it.entity_id }
runOnUiThread { filterEntities(etSearch.text.toString()) }
} else {
runOnUiThread { Toast.makeText(this@EntitySelectionActivity, "Błąd HA: ${response.code()}", Toast.LENGTH_SHORT).show() }
}
}
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
runOnUiThread {
Toast.makeText(this@EntitySelectionActivity, "Błąd: ${t.message}", Toast.LENGTH_LONG).show()
}
}
})
}
private fun filterEntities(query: String) {
filteredEntities.clear()
if (query.isEmpty()) {
filteredEntities.addAll(allEntities)
} else {
val q = query.lowercase()
allEntities.filterTo(filteredEntities) {
it.entity_id.lowercase().contains(q) ||
(it.attributes.friendly_name?.lowercase()?.contains(q) == true)
}
}
updateList()
}
private fun updateList() {
adapter = EntitySelectionAdapter(this, filteredEntities, selectedEntities)
lvEntities.adapter = adapter
}
}

View File

@@ -0,0 +1,8 @@
package com.example.retroha
import android.os.Bundle
class InstructionsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_instructions)
}
}

View File

@@ -0,0 +1,91 @@
package com.example.retroha
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import com.example.retroha.data.Prefs
import com.example.retroha.i18n.LocaleHelper
import com.example.retroha.theme.Colors
class LanguageActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val isFirstLaunch = Prefs.getLanguage(this) == null
val fromSettings = intent.getBooleanExtra("from_settings", false)
if (!isFirstLaunch && !fromSettings) {
LocaleHelper.setLocale(this)
startMainActivity()
return
}
val density = resources.displayMetrics.density
fun dp(v: Int) = (v * density + 0.5f).toInt()
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
setBackgroundColor(Colors.WHITE)
setPadding(dp(32), dp(32), dp(32), dp(32))
}
val title = TextView(this).apply {
text = "WYBIERZ JĘZYK\nSELECT LANGUAGE"
typeface = android.graphics.Typeface.MONOSPACE
textSize = 18f
setTextColor(Colors.BLACK)
gravity = Gravity.CENTER
setPadding(0, 0, 0, dp(64))
}
root.addView(title)
fun createBauhausButton(label: String, color: Int, onClick: () -> Unit): View {
val container = FrameLayout(this).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(60)).apply {
setMargins(0, 0, 0, dp(24))
}
}
val shadow = View(this).apply {
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(56)).apply {
gravity = Gravity.BOTTOM or Gravity.RIGHT
}
setBackgroundColor(Colors.BLACK)
}
val btn = Button(this).apply {
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(56)).apply {
gravity = Gravity.TOP or Gravity.LEFT
}
text = label
typeface = android.graphics.Typeface.MONOSPACE
setTextColor(Colors.WHITE)
val bgDrawable = android.graphics.drawable.GradientDrawable().apply {
shape = android.graphics.drawable.GradientDrawable.RECTANGLE
setColor(color)
setStroke(dp(2), Colors.BLACK)
}
background = bgDrawable
setOnClickListener { onClick() }
}
container.addView(shadow)
container.addView(btn)
return container
}
root.addView(createBauhausButton("POLSKI", Colors.BLUE) {
Prefs.setLanguage(this, "pl")
LocaleHelper.setLocale(this)
startMainActivity()
})
root.addView(createBauhausButton("ENGLISH", Colors.YELLOW) {
Prefs.setLanguage(this, "en")
LocaleHelper.setLocale(this)
startMainActivity()
})
setContentView(root)
}
private fun startMainActivity() {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
}
}

View File

@@ -0,0 +1,313 @@
package com.example.retroha
import android.app.Activity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.content.Intent
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.SeekBar
import android.widget.Toast
import android.widget.GridView
import com.example.retroha.i18n.AndroidStrings
import com.example.retroha.i18n.StringKey
import com.example.retroha.model.EntityState
import com.example.retroha.model.WidgetConfig
import com.example.retroha.model.WidgetInteraction
import com.example.retroha.model.toWidgetInteraction
import com.example.retroha.data.Prefs
import com.example.retroha.ui.WidgetAdapter
import com.example.retroha.network.HaClient
import com.example.retroha.network.HaState
import com.example.retroha.network.ToggleRequest
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.view.HapticFeedbackConstants
import android.view.WindowManager
import android.graphics.Color
import android.view.Gravity
class MainActivity : BaseActivity() {
private lateinit var strings: AndroidStrings
private lateinit var adapter: WidgetAdapter
private val allEntities = mutableListOf<WidgetConfig>()
private val displayedEntities = mutableListOf<WidgetConfig>()
private val mainHandler = Handler(Looper.getMainLooper())
private var currentCategory = "WSZYSTKO"
private lateinit var tvStatusIndicator: TextView
private lateinit var tabContainer: LinearLayout
private val refreshRunnable = object : Runnable {
override fun run() {
fetchHaStates()
val interval = Prefs.getRefreshInterval(this@MainActivity)
mainHandler.postDelayed(this, interval)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
strings = AndroidStrings(this)
tvStatusIndicator = findViewById(R.id.tvStatusIndicator)
tabContainer = findViewById(R.id.tabContainer)
val gridView = findViewById<GridView>(R.id.gridView)
gridView.numColumns = resolveColumns()
adapter = WidgetAdapter(this, displayedEntities)
adapter.onToggle = { cfg ->
findViewById<View>(android.R.id.content)?.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
handleToggle(cfg)
}
adapter.onLongToggle = { cfg ->
findViewById<View>(android.R.id.content)?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
handleLongToggle(cfg)
}
gridView.adapter = adapter
findViewById<android.view.View>(R.id.btnSettings).setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java))
}
findViewById<TextView>(R.id.btnSettings).apply {
val bg = android.graphics.drawable.GradientDrawable().apply {
shape = android.graphics.drawable.GradientDrawable.RECTANGLE
setColor(com.example.retroha.theme.Colors.BLUE)
setStroke(dp(2), com.example.retroha.theme.Colors.BLACK)
}
background = bg
}
findViewById<android.view.View>(R.id.tvTitle).setOnClickListener {
fetchHaStates()
Toast.makeText(this, "Odświeżanie...", Toast.LENGTH_SHORT).show()
}
setupTabs()
}
private fun setupTabs() {
val categories = listOf("WSZYSTKO", "OŚWIETLENIE", "GNIAZDKA", "MOC", "POGODA")
val mainView = findViewById<View>(android.R.id.content)
tabContainer.post {
tabContainer.removeAllViews()
categories.forEach { cat ->
val isSelected = cat == currentCategory
val tabFrame = FrameLayout(this).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, dp(34)).apply {
setMargins(dp(4), dp(4), dp(4), dp(4))
}
}
val shadow = View(this).apply {
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
setBackgroundColor(com.example.retroha.theme.Colors.BLACK)
translationX = dp(2).toFloat()
translationY = dp(2).toFloat()
}
val tabButton = TextView(this).apply {
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
text = cat
typeface = android.graphics.Typeface.MONOSPACE
textSize = 11f
setPadding(dp(12), 0, dp(12), 0)
gravity = Gravity.CENTER
val bgDrawable = android.graphics.drawable.GradientDrawable().apply {
shape = android.graphics.drawable.GradientDrawable.RECTANGLE
setColor(if (isSelected) com.example.retroha.theme.Colors.YELLOW else com.example.retroha.theme.Colors.WHITE)
setStroke(dp(2), com.example.retroha.theme.Colors.BLACK)
}
background = bgDrawable
setTextColor(com.example.retroha.theme.Colors.BLACK)
setOnClickListener {
if (currentCategory == cat) return@setOnClickListener
currentCategory = cat
setupTabs()
filterEntities()
mainView?.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
}
tabFrame.addView(shadow)
tabFrame.addView(tabButton)
tabContainer.addView(tabFrame)
}
}
}
private fun dp(v: Int) = (v * resources.displayMetrics.density + 0.5f).toInt()
private fun filterEntities() {
val filtered = when (currentCategory) {
"WSZYSTKO" -> allEntities
"OŚWIETLENIE" -> allEntities.filter { it.domain == "light" }
"GNIAZDKA" -> allEntities.filter {
it.domain == "switch" || it.domain == "outlet" ||
it.entityId.contains("socket") || it.entityId.contains("plug")
}
"MOC" -> allEntities.filter { it.secondary.contains("W") || it.entityId.contains("power") || it.entityId.contains("energy") || it.entityId.contains("current_consumption") }
"POGODA" -> allEntities.filter { it.domain == "weather" || it.domain == "sensor" && (it.entityId.contains("temp") || it.entityId.contains("hum")) }
else -> allEntities
}
runOnUiThread {
adapter.updateItems(filtered)
}
}
override fun onResume() {
super.onResume()
mainHandler.removeCallbacks(refreshRunnable)
mainHandler.post(refreshRunnable)
}
override fun onPause() {
super.onPause()
mainHandler.removeCallbacks(refreshRunnable)
}
private fun fetchHaStates() {
val token = Prefs.getToken(this)
if (token.isEmpty()) {
tvStatusIndicator.text = "BRAK TOKENU"
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.RED)
return
}
HaClient.getService(this).getStates().enqueue(object : Callback<List<HaState>> {
override fun onResponse(call: Call<List<HaState>>, response: Response<List<HaState>>) {
if (response.isSuccessful) {
val states = response.body() ?: return
updateEntities(states)
runOnUiThread {
tvStatusIndicator.text = "ONLINE"
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.BLUE)
}
} else {
runOnUiThread {
tvStatusIndicator.text = "BŁĄD HA: ${response.code()}"
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.RED)
}
}
}
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
runOnUiThread {
tvStatusIndicator.text = "OFFLINE"
tvStatusIndicator.setTextColor(com.example.retroha.theme.Colors.RED)
}
}
})
}
private fun updateEntities(haStates: List<HaState>) {
val selectedIds = Prefs.getSelectedEntities(this)
allEntities.clear()
haStates.filter { ha ->
selectedIds.contains(ha.entity_id)
}.forEachIndexed { index, ha ->
val domain = ha.entity_id.split(".")[0]
val state = when (ha.state) {
"on" -> EntityState.ON
"off" -> EntityState.OFF
"unavailable" -> EntityState.UNAVAILABLE
else -> EntityState.OFF
}
allEntities.add(WidgetConfig(
id = index.toLong(),
entityId = ha.entity_id,
label = ha.attributes.friendly_name ?: ha.entity_id,
value = ha.state.uppercase(),
secondary = ha.attributes.unit_of_measurement ?: "",
domain = domain,
state = state,
brightness = ha.attributes.brightness
))
}
runOnUiThread {
filterEntities()
}
}
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) {
HaClient.getService(this).setBrightness(com.example.retroha.network.BrightnessRequest(entityId, brightness))
.enqueue(object : Callback<List<HaState>> {
override fun onResponse(call: Call<List<HaState>>, response: Response<List<HaState>>) {
fetchHaStates()
}
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {}
})
}
private fun handleToggle(cfg: WidgetConfig) {
val idx = allEntities.indexOfFirst { it.entityId == cfg.entityId }
if (idx < 0) return
when (cfg.domain.toWidgetInteraction()) {
WidgetInteraction.TOGGLE -> doToggle(idx, cfg)
WidgetInteraction.EXECUTE -> doExecute(idx, cfg)
WidgetInteraction.READ_ONLY -> Unit
}
}
private fun doToggle(idx: Int, cfg: WidgetConfig) {
val domain = cfg.entityId.split(".")[0]
if (idx >= 0 && idx < allEntities.size) {
allEntities[idx] = allEntities[idx].copy(
state = EntityState.TOGGLING,
value = strings[StringKey.STATE_TOGGLING]
)
filterEntities()
}
HaClient.getService(this).toggle(domain, ToggleRequest(cfg.entityId)).enqueue(object : Callback<List<HaState>> {
override fun onResponse(call: Call<List<HaState>>, response: Response<List<HaState>>) {
if (!response.isSuccessful) {
runOnUiThread { Toast.makeText(this@MainActivity, "BŁĄD HA: ${response.code()}", Toast.LENGTH_SHORT).show() }
}
fetchHaStates()
}
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
runOnUiThread { Toast.makeText(this@MainActivity, "BŁĄD SIECI: ${t.message}", Toast.LENGTH_SHORT).show() }
fetchHaStates()
}
})
}
private fun doExecute(idx: Int, cfg: WidgetConfig) {
val domain = cfg.entityId.split(".")[0]
if (idx >= 0 && idx < allEntities.size) {
allEntities[idx] = allEntities[idx].copy(
state = EntityState.TOGGLING,
value = strings[StringKey.STATE_TOGGLING]
)
filterEntities()
}
HaClient.getService(this).toggle(domain, ToggleRequest(cfg.entityId)).enqueue(object : Callback<List<HaState>> {
override fun onResponse(call: Call<List<HaState>>, response: Response<List<HaState>>) {
if (!response.isSuccessful) {
runOnUiThread { Toast.makeText(this@MainActivity, "BŁĄD HA: ${response.code()}", Toast.LENGTH_SHORT).show() }
}
fetchHaStates()
}
override fun onFailure(call: Call<List<HaState>>, t: Throwable) {
runOnUiThread { Toast.makeText(this@MainActivity, "BŁĄD SIECI: ${t.message}", Toast.LENGTH_SHORT).show() }
fetchHaStates()
}
})
}
override fun onDestroy() {
super.onDestroy()
mainHandler.removeCallbacks(refreshRunnable)
mainHandler.removeCallbacksAndMessages(null)
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
mainHandler.removeCallbacks(refreshRunnable)
allEntities.clear()
displayedEntities.clear()
adapter.notifyDataSetChanged()
}
}
private fun resolveColumns(): Int {
val config = resources.configuration
val sw = config.smallestScreenWidthDp
val isLandscape = config.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
return when {
sw >= 720 -> if (isLandscape) 5 else 4
sw >= 600 -> if (isLandscape) 4 else 3
else -> if (isLandscape) 3 else 2
}
}
}

View File

@@ -0,0 +1,38 @@
package com.example.retroha
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import com.example.retroha.data.Prefs
class SettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
findViewById<Button>(R.id.btnEntitySelection).setOnClickListener {
startActivity(Intent(this, EntitySelectionActivity::class.java))
}
findViewById<Button>(R.id.btnGoToConnection).setOnClickListener {
startActivity(Intent(this, ConnectionSettingsActivity::class.java))
}
findViewById<Button>(R.id.btnInstructions).setOnClickListener {
startActivity(Intent(this, InstructionsActivity::class.java))
}
findViewById<Button>(R.id.btnChangeLang).setOnClickListener {
val intent = Intent(this, LanguageActivity::class.java)
intent.putExtra("from_settings", true)
startActivity(intent)
}
findViewById<Button>(R.id.btnDeleteAll).setOnClickListener {
val strings = com.example.retroha.i18n.AndroidStrings(this)
android.app.AlertDialog.Builder(this)
.setTitle("UWAGA")
.setMessage(strings.get(com.example.retroha.i18n.StringKey.CONFIRM_DELETE_ALL))
.setPositiveButton("TAK, USUŃ") { _, _ ->
Prefs.setSelectedEntities(this, emptySet())
Toast.makeText(this, "Wyczyszczono pulpit", Toast.LENGTH_SHORT).show()
}
.setNegativeButton(strings.get(com.example.retroha.i18n.StringKey.DIALOG_ANULUJ), null)
.show()
}
}
}

View File

@@ -0,0 +1,25 @@
package com.example.retroha.data
import android.content.Context
import android.content.SharedPreferences
object Prefs {
private const val PREFS_NAME = "retroha_prefs"
private const val KEY_URL = "ha_url"
private const val KEY_TOKEN = "ha_token"
private const val KEY_SELECTED_ENTITIES = "selected_entities"
private const val KEY_REFRESH_INTERVAL = "refresh_interval"
private const val KEY_LANGUAGE = "app_language"
private fun getPrefs(context: Context): SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
fun getLanguage(context: Context): String? = getPrefs(context).getString(KEY_LANGUAGE, null)
fun setLanguage(context: Context, lang: String) = getPrefs(context).edit().putString(KEY_LANGUAGE, lang).apply()
fun getUrl(context: Context): String = getPrefs(context).getString(KEY_URL, "http:
fun setUrl(context: Context, url: String) = getPrefs(context).edit().putString(KEY_URL, url).apply()
fun getToken(context: Context): String = getPrefs(context).getString(KEY_TOKEN, "") ?: ""
fun setToken(context: Context, token: String) = getPrefs(context).edit().putString(KEY_TOKEN, token).apply()
fun getRefreshInterval(context: Context): Long = getPrefs(context).getLong(KEY_REFRESH_INTERVAL, 30000L)
fun setRefreshInterval(context: Context, intervalMs: Long) = getPrefs(context).edit().putLong(KEY_REFRESH_INTERVAL, intervalMs).apply()
fun getSelectedEntities(context: Context): Set<String> =
getPrefs(context).getStringSet(KEY_SELECTED_ENTITIES, emptySet()) ?: emptySet()
fun setSelectedEntities(context: Context, entities: Set<String>) =
getPrefs(context).edit().putStringSet(KEY_SELECTED_ENTITIES, entities).apply()
}

View File

@@ -0,0 +1,45 @@
package com.example.retroha.i18n
import android.content.Context
import com.example.retroha.R
class AndroidStrings(private val context: Context) : Strings {
override fun get(key: StringKey): String = context.getString(
when (key) {
StringKey.STATE_ON -> R.string.state_on
StringKey.STATE_OFF -> R.string.state_off
StringKey.STATE_UNAVAILABLE -> R.string.state_unavailable
StringKey.STATE_TOGGLING -> R.string.state_toggling
StringKey.BTN_SETTINGS -> R.string.btn_settings
StringKey.TOAST_WIDGET_ADD -> R.string.toast_widget_add
StringKey.TAB_ALL -> R.string.tab_all
StringKey.TAB_LIGHTING -> R.string.tab_lighting
StringKey.TAB_SOCKETS -> R.string.tab_sockets
StringKey.TAB_POWER -> R.string.tab_power
StringKey.TAB_WEATHER -> R.string.tab_weather
StringKey.TITLE_SETTINGS -> R.string.title_settings
StringKey.TITLE_CONNECTION -> R.string.title_connection
StringKey.TITLE_INSTRUCTIONS -> R.string.title_instructions
StringKey.INSTRUCTION_1 -> R.string.instruction_1
StringKey.INSTRUCTION_2 -> R.string.instruction_2
StringKey.INSTRUCTION_3 -> R.string.instruction_3
StringKey.INSTRUCTION_4 -> R.string.instruction_4
StringKey.INSTRUCTION_5 -> R.string.instruction_5
StringKey.LABEL_URL -> R.string.label_url
StringKey.LABEL_TOKEN -> R.string.label_token
StringKey.LABEL_REFRESH -> R.string.label_refresh
StringKey.BTN_TEST_SAVE -> R.string.btn_test_save
StringKey.BTN_DELETE_ALL -> R.string.btn_delete_all
StringKey.BTN_SAVE_SELECTED -> R.string.btn_save_selected
StringKey.BTN_CHANGE_LANG -> R.string.btn_change_lang
StringKey.STATUS_CONNECTING -> R.string.status_connecting
StringKey.STATUS_CONNECTED -> R.string.status_connected
StringKey.STATUS_OFFLINE -> R.string.status_offline
StringKey.STATUS_ERROR_HA -> R.string.status_error_ha
StringKey.STATUS_NO_TOKEN -> R.string.status_no_token
StringKey.CONFIRM_DELETE_ALL -> R.string.confirm_delete_all
StringKey.CONFIRM_CHANGE_CONN -> R.string.confirm_change_conn
StringKey.DIALOG_BRIGHTNESS -> R.string.dialog_brightness
StringKey.DIALOG_USTAW -> R.string.dialog_ustaw
StringKey.DIALOG_ANULUJ -> R.string.dialog_anuluj
}
)
}

View File

@@ -0,0 +1,16 @@
package com.example.retroha.i18n
import android.content.Context
import android.content.res.Configuration
import com.example.retroha.data.Prefs
import java.util.Locale
object LocaleHelper {
fun setLocale(context: Context) {
val lang = Prefs.getLanguage(context) ?: return
val locale = Locale(lang)
Locale.setDefault(locale)
val resources = context.resources
val config = Configuration(resources.configuration)
config.locale = locale
resources.updateConfiguration(config, resources.displayMetrics)
}
}

View File

@@ -0,0 +1,28 @@
package com.example.retroha.network
import retrofit2.Call
import retrofit2.http.*
interface HaApiService {
@GET("api/states")
fun getStates(): Call<List<HaState>>
@POST("api/services/{domain}/toggle")
fun toggle(@Path("domain") domain: String, @Body body: ToggleRequest): Call<List<HaState>>
@POST("api/services/light/turn_on")
fun setBrightness(@Body body: BrightnessRequest): Call<List<HaState>>
}
data class HaState(
val entity_id: String,
val state: String,
val attributes: HaAttributes
)
data class HaAttributes(
val friendly_name: String?,
val unit_of_measurement: String?,
val brightness: Int? = null
)
data class ToggleRequest(
val entity_id: String
)
data class BrightnessRequest(
val entity_id: String,
val brightness: Int
)

View File

@@ -0,0 +1,42 @@
package com.example.retroha.network
import android.content.Context
import com.example.retroha.data.Prefs
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object HaClient {
private var serviceInstance: HaApiService? = null
private val gson = GsonBuilder()
.registerTypeAdapter(HaState::class.java, HaStateAdapter())
.create()
fun getService(context: Context): HaApiService {
val url = Prefs.getUrl(context)
val token = Prefs.getToken(context)
return serviceInstance ?: buildService(url, token).also { serviceInstance = it }
}
fun clearCache() {
serviceInstance = null
}
fun getServiceForTest(url: String, token: String): HaApiService {
return buildService(url, token)
}
private fun buildService(url: String, token: String): HaApiService {
val baseUrl = if (url.endsWith("/")) url else "$url/"
val okHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.addHeader("Content-Type", "application/json")
.build()
chain.proceed(request)
}
.build()
return Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(HaApiService::class.java)
}
}

View File

@@ -0,0 +1,38 @@
package com.example.retroha.network
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
class HaStateAdapter : TypeAdapter<HaState>() {
override fun write(out: JsonWriter, value: HaState?) {
}
override fun read(reader: JsonReader): HaState {
var entityId = ""
var state = ""
var friendlyName: String? = null
var unit: String? = null
var brightness: Int? = null
reader.beginObject()
while (reader.hasNext()) {
when (reader.nextName()) {
"entity_id" -> entityId = reader.nextString()
"state" -> state = reader.nextString()
"attributes" -> {
reader.beginObject()
while (reader.hasNext()) {
when (reader.nextName()) {
"friendly_name" -> friendlyName = if (reader.peek() == JsonToken.NULL) { reader.nextNull(); null } else reader.nextString()
"unit_of_measurement" -> unit = if (reader.peek() == JsonToken.NULL) { reader.nextNull(); null } else reader.nextString()
"brightness" -> brightness = if (reader.peek() == JsonToken.NULL) { reader.nextNull(); null } else reader.nextInt()
else -> reader.skipValue()
}
}
reader.endObject()
}
else -> reader.skipValue()
}
}
reader.endObject()
return HaState(entityId, state, HaAttributes(friendlyName, unit, brightness))
}
}

View File

@@ -0,0 +1,32 @@
package com.example.retroha.ui
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.view.View
import com.example.retroha.theme.Colors
class BauhausCheckbox(context: Context) : View(context) {
private val density = resources.displayMetrics.density
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
var isChecked: Boolean = false
set(value) {
field = value
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val size = (24 * density).toInt()
setMeasuredDimension(size, size)
}
override fun onDraw(canvas: Canvas) {
val size = width.toFloat()
val b = 2 * density
paint.color = Colors.BLACK
paint.style = Paint.Style.STROKE
paint.strokeWidth = b
canvas.drawRect(b/2, b/2, size - b/2, size - b/2, paint)
if (isChecked) {
paint.style = Paint.Style.FILL
paint.color = Colors.YELLOW
canvas.drawRect(b * 1.5f, b * 1.5f, size - b * 1.5f, size - b * 1.5f, paint)
}
}
}

View File

@@ -0,0 +1,56 @@
package com.example.retroha.ui
import android.content.Context
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.LinearLayout
import android.widget.TextView
import com.example.retroha.network.HaState
import com.example.retroha.theme.Colors
class EntitySelectionAdapter(
private val context: Context,
private val items: List<HaState>,
private val selectedEntities: Set<String>
) : BaseAdapter() {
override fun getCount() = items.size
override fun getItem(position: Int) = items[position]
override fun getItemId(position: Int) = position.toLong()
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val layout = (convertView as? LinearLayout) ?: createLayout()
val checkbox = layout.getChildAt(0) as BauhausCheckbox
val textContainer = layout.getChildAt(1) as LinearLayout
val tvName = textContainer.getChildAt(0) as TextView
val tvId = textContainer.getChildAt(1) as TextView
val item = items[position]
checkbox.isChecked = selectedEntities.contains(item.entity_id)
tvName.text = item.attributes.friendly_name ?: item.entity_id
tvId.text = item.entity_id
return layout
}
private fun createLayout(): LinearLayout {
return LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
setPadding(dp(12), dp(12), dp(12), dp(12))
gravity = Gravity.CENTER_VERTICAL
addView(BauhausCheckbox(context))
addView(LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(12), 0, 0, 0)
addView(TextView(context).apply {
typeface = android.graphics.Typeface.MONOSPACE
setTextColor(Colors.BLACK)
textSize = 14f
setSingleLine()
})
addView(TextView(context).apply {
typeface = android.graphics.Typeface.MONOSPACE
setTextColor(Colors.GRAY_MID)
textSize = 10f
setSingleLine()
})
})
}
}
private fun dp(v: Int) = (v * context.resources.displayMetrics.density + 0.5f).toInt()
}

View File

@@ -0,0 +1,6 @@
package com.example.retroha.ui
import android.graphics.Typeface
object Fonts {
val REGULAR: Typeface by lazy { Typeface.create("monospace", Typeface.NORMAL) }
val BOLD: Typeface by lazy { Typeface.create("monospace", Typeface.BOLD) }
}

View File

@@ -0,0 +1,74 @@
package com.example.retroha.ui
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import com.example.retroha.theme.Colors
object HaIcons {
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
}
private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 2f
}
private val tmpRect = RectF()
private val tmpPath = Path()
fun draw(canvas: Canvas, domain: String, x: Float, y: Float, size: Float, color: Int) {
fillPaint.color = color
strokePaint.color = color
canvas.save()
canvas.translate(x, y)
when (domain) {
"light" -> drawLight(canvas, size)
"switch" -> drawSwitch(canvas, size)
"weather" -> drawWeather(canvas, size)
"power" -> drawPower(canvas, size)
"sensor" -> drawSensor(canvas, size)
else -> drawGeneric(canvas, size)
}
canvas.restore()
}
private fun drawLight(canvas: Canvas, s: Float) {
canvas.drawCircle(s / 2f, s / 2f, s * 0.2f, fillPaint)
val rayStart = s * 0.25f
val rayEnd = s * 0.5f
for (i in 0 until 8) {
val angle = i * 45.0
val startX = (s / 2f + rayStart * Math.cos(Math.toRadians(angle))).toFloat()
val startY = (s / 2f + rayStart * Math.sin(Math.toRadians(angle))).toFloat()
val endX = (s / 2f + rayEnd * Math.cos(Math.toRadians(angle))).toFloat()
val endY = (s / 2f + rayEnd * Math.sin(Math.toRadians(angle))).toFloat()
canvas.drawLine(startX, startY, endX, endY, strokePaint)
}
}
private fun drawSwitch(canvas: Canvas, s: Float) {
tmpPath.reset()
tmpPath.moveTo(s / 2f, s * 0.15f)
tmpPath.lineTo(s * 0.85f, s * 0.85f)
tmpPath.lineTo(s * 0.15f, s * 0.85f)
tmpPath.close()
canvas.drawPath(tmpPath, fillPaint)
}
private fun drawPower(canvas: Canvas, s: Float) {
tmpPath.reset()
tmpPath.moveTo(s * 0.15f, s * 0.15f)
tmpPath.lineTo(s * 0.85f, s / 2f)
tmpPath.lineTo(s * 0.15f, s * 0.85f)
tmpPath.close()
canvas.drawPath(tmpPath, fillPaint)
}
private fun drawWeather(canvas: Canvas, s: Float) {
canvas.drawCircle(s / 2f, s * 0.45f, s * 0.25f, fillPaint)
canvas.drawLine(s * 0.15f, s * 0.75f, s * 0.85f, s * 0.75f, strokePaint)
}
private fun drawSensor(canvas: Canvas, s: Float) {
canvas.drawCircle(s / 2f, s * 0.25f, s * 0.08f, fillPaint)
canvas.drawCircle(s / 2f, s * 0.5f, s * 0.12f, fillPaint)
canvas.drawCircle(s / 2f, s * 0.75f, s * 0.08f, fillPaint)
}
private fun drawGeneric(canvas: Canvas, s: Float) {
tmpRect.set(s * 0.25f, s * 0.25f, s * 0.75f, s * 0.75f)
canvas.drawRect(tmpRect, fillPaint)
}
}

View File

@@ -0,0 +1,28 @@
package com.example.retroha.ui
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import com.example.retroha.theme.Colors
class LanguageIconDrawable(private val sizePx: Int) : Drawable() {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 3f
color = Colors.BLACK
}
override fun draw(canvas: Canvas) {
val cx = bounds.exactCenterX()
val cy = bounds.exactCenterY()
val r = sizePx / 2f - 2f
canvas.drawCircle(cx, cy, r, paint)
canvas.drawLine(cx - r, cy, cx + r, cy, paint)
canvas.drawLine(cx, cy - r, cx, cy + r, paint)
}
override fun setAlpha(alpha: Int) {}
override fun setColorFilter(colorFilter: ColorFilter?) {}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
override fun getIntrinsicWidth() = sizePx
override fun getIntrinsicHeight() = sizePx
}

View File

@@ -0,0 +1,75 @@
package com.example.retroha.ui
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.view.Window
import android.view.ViewGroup
import android.widget.Button
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.SeekBar
import android.widget.TextView
import com.example.retroha.theme.Colors
class LightControlDialog(
context: Context,
private val entityName: String,
private val initialBrightness: Int,
private val onBrightnessChanged: (Int) -> Unit
) : Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
val density = context.resources.displayMetrics.density
fun dp(v: Int) = (v * density + 0.5f).toInt()
val root = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(Colors.WHITE)
setPadding(dp(24), dp(24), dp(24), dp(24))
layoutParams = LinearLayout.LayoutParams(dp(320), ViewGroup.LayoutParams.WRAP_CONTENT)
}
root.addView(TextView(context).apply {
text = entityName.uppercase()
typeface = android.graphics.Typeface.MONOSPACE
textSize = 16f
setTextColor(Colors.BLACK)
setPadding(0, 0, 0, dp(16))
})
root.addView(View(context).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(2))
setBackgroundColor(Colors.BLACK)
})
val tvBrightness = TextView(context).apply {
text = "JASNOŚĆ: ${(initialBrightness * 100 / 255)}%"
typeface = android.graphics.Typeface.MONOSPACE
textSize = 14f
setTextColor(Colors.BLACK)
setPadding(0, dp(24), 0, dp(8))
}
root.addView(tvBrightness)
val seekBar = SeekBar(context).apply {
max = 255
progress = initialBrightness
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(s: SeekBar?, p: Int, fromUser: Boolean) {
tvBrightness.text = "JASNOŚĆ: ${(p * 100 / 255)}%"
}
override fun onStartTrackingTouch(s: SeekBar?) {}
override fun onStopTrackingTouch(s: SeekBar?) {
onBrightnessChanged(progress)
}
})
}
root.addView(seekBar)
root.addView(View(context).apply {
layoutParams = LinearLayout.LayoutParams(1, dp(16))
})
setContentView(root)
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
setCanceledOnTouchOutside(true)
}
}

View File

@@ -0,0 +1,33 @@
package com.example.retroha.ui
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import com.example.retroha.model.WidgetConfig
class WidgetAdapter(
private val context: Context,
initialItems: List<WidgetConfig>
) : BaseAdapter() {
private val items = mutableListOf<WidgetConfig>().apply { addAll(initialItems) }
var onToggle: ((WidgetConfig) -> Unit)? = null
var onLongToggle: ((WidgetConfig) -> Unit)? = null
fun updateItems(newItems: List<WidgetConfig>) {
items.clear()
items.addAll(newItems)
notifyDataSetChanged()
}
override fun getCount() = items.size
override fun getItem(position: Int) = items[position]
override fun getItemId(position: Int) = items[position].id
override fun hasStableIds() = true
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val card = convertView as? WidgetCardView ?: WidgetCardView(context).also {
it.onToggle = onToggle
it.onLongToggle = onLongToggle
}
if (position < items.size) {
card.bind(items[position])
}
return card
}
}

View File

@@ -0,0 +1,208 @@
package com.example.retroha.ui
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.os.Build
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextUtils
import android.view.View
import android.util.TypedValue
import android.view.animation.DecelerateInterpolator
import com.example.retroha.model.EntityState
import com.example.retroha.model.WidgetConfig
import com.example.retroha.model.WidgetInteraction
import com.example.retroha.model.toWidgetInteraction
import com.example.retroha.theme.Colors
class WidgetCardView(context: Context) : View(context) {
private val density = resources.displayMetrics.density
private val borderPx = dp(2)
private val stripePx = dp(4)
private val shadowPx = dp(3)
private val cardHeightPx = dp(88)
private val textLeft = borderPx + stripePx + dp(6)
private val textRight = dp(8)
private val textTop = dp(12)
private val lineGap1 = dp(3)
private val lineGap2 = dp(2)
private val execIconSize = dp(7).toFloat()
private val execIconPath = Path()
private val paintFill = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
private val tpLabel = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = Colors.GRAY_MID; textSize = sp(11); typeface = Fonts.REGULAR
}
private val tpValue = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = Colors.BLACK; textSize = sp(18); typeface = Fonts.BOLD
}
private val tpSecondary = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = Colors.GRAY_MID; textSize = sp(10); typeface = Fonts.REGULAR
}
private val pulseInterpolator = DecelerateInterpolator()
var onToggle: ((WidgetConfig) -> Unit)? = null
var onLongToggle: ((WidgetConfig) -> Unit)? = null
private var config: WidgetConfig? = null
private var currentInteraction: WidgetInteraction = WidgetInteraction.READ_ONLY
private var lLabel: StaticLayout? = null
private var lValue: StaticLayout? = null
private var lSecondary: StaticLayout? = null
private var pulseAnim: ObjectAnimator? = null
private var cachedLabel = ""
private var cachedValue = ""
private var cachedSecondary = ""
private var cachedTextW = 0
init {
isFocusable = true
setOnClickListener {
val cfg = config ?: return@setOnClickListener
onToggle?.invoke(cfg)
}
setOnLongClickListener {
val cfg = config ?: return@setOnLongClickListener false
onLongToggle?.invoke(cfg)
true
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val w = MeasureSpec.getSize(widthMeasureSpec)
setMeasuredDimension(w, cardHeightPx + shadowPx)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
updateExecIconPath(w - shadowPx)
rebuildLayoutsIfNeeded(w)
}
fun bind(cfg: WidgetConfig) {
config = cfg
currentInteraction = cfg.domain.toWidgetInteraction()
isClickable = currentInteraction != WidgetInteraction.READ_ONLY
&& cfg.state != EntityState.UNAVAILABLE
&& cfg.state != EntityState.TOGGLING
tpValue.color = if (cfg.state == EntityState.UNAVAILABLE) Colors.GRAY_MID else Colors.BLACK
if (cfg.state == EntityState.TOGGLING) startPulse() else stopPulse()
if (width > 0) rebuildLayoutsIfNeeded(width)
invalidate()
}
private fun updateExecIconPath(cardW: Int) {
val sz = execIconSize
val right = cardW.toFloat() - dp(8)
val top = dp(9).toFloat()
execIconPath.reset()
execIconPath.moveTo(right - sz, top)
execIconPath.lineTo(right, top + sz / 2f)
execIconPath.lineTo(right - sz, top + sz)
execIconPath.close()
}
private fun rebuildLayoutsIfNeeded(viewW: Int) {
val cfg = config ?: return
val cardW = viewW - shadowPx
val textW = (cardW - textLeft - textRight).coerceAtLeast(1)
val labelChanged = cfg.label != cachedLabel
val textChanged = labelChanged || cfg.value != cachedValue || cfg.secondary != cachedSecondary
val widthChanged = textW != cachedTextW
if (!textChanged && !widthChanged && lLabel != null) return
val labelUp = if (labelChanged) cfg.label.uppercase() else cachedLabel
lLabel = makeLayout(labelUp, tpLabel, textW)
lValue = makeLayout(cfg.value, tpValue, textW)
lSecondary = makeLayout(cfg.secondary, tpSecondary, textW)
cachedLabel = cfg.label
cachedValue = cfg.value
cachedSecondary = cfg.secondary
cachedTextW = textW
}
override fun onDraw(canvas: Canvas) {
val cfg = config ?: return
val cardW = (width - shadowPx).toFloat()
val cardH = cardHeightPx.toFloat()
val b = borderPx.toFloat()
val s = shadowPx.toFloat()
paintFill.color = Colors.BLACK
canvas.drawRect(s, s, width.toFloat(), cardH + s, paintFill)
paintFill.color = when (cfg.state) {
EntityState.TOGGLING -> Colors.BORDER_TOGGLING
EntityState.UNAVAILABLE -> Colors.BORDER_UNAVAILABLE
else -> Colors.BORDER_DEFAULT
}
canvas.drawRect(0f, 0f, cardW, cardH, paintFill)
paintFill.color = when (cfg.state) {
EntityState.ON -> Colors.STATUS_ON
EntityState.OFF -> Colors.STATUS_OFF
EntityState.UNAVAILABLE -> Colors.STATUS_UNAVAILABLE
EntityState.TOGGLING -> Colors.STATUS_TOGGLING
}
canvas.drawRect(b, b, cardW - b, cardH - b, paintFill)
paintFill.color = stripeColor(cfg.domain)
canvas.drawRect(b, b, b + stripePx, cardH - b, paintFill)
val iconSize = dp(16).toFloat()
val iconX = cardW - iconSize - dp(8)
val iconY = dp(8).toFloat()
HaIcons.draw(canvas, cfg.domain, iconX, iconY, iconSize, paintFill.color)
if (currentInteraction == WidgetInteraction.EXECUTE && cfg.state != EntityState.UNAVAILABLE) {
paintFill.color = Colors.GRAY_MID
canvas.drawPath(execIconPath, paintFill)
}
val tl = lLabel; val tv = lValue; val ts = lSecondary
if (tl != null && tv != null && ts != null) {
canvas.save()
canvas.translate(textLeft.toFloat(), textTop.toFloat())
tl.draw(canvas)
canvas.translate(0f, (tl.height + lineGap1).toFloat())
tv.draw(canvas)
canvas.translate(0f, (tv.height + lineGap2).toFloat())
ts.draw(canvas)
canvas.restore()
}
if (isPressed) {
paintFill.color = 0x22000000
canvas.drawRect(0f, 0f, cardW, cardH, paintFill)
}
}
override fun drawableStateChanged() {
super.drawableStateChanged()
invalidate()
}
private fun startPulse() {
if (pulseAnim?.isRunning == true) return
setLayerType(LAYER_TYPE_HARDWARE, null)
pulseAnim = ObjectAnimator.ofFloat(this, ALPHA, 1f, 0.35f).apply {
duration = 550
repeatMode = ObjectAnimator.REVERSE
repeatCount = ObjectAnimator.INFINITE
interpolator = pulseInterpolator
start()
}
}
private fun stopPulse() {
pulseAnim?.cancel()
pulseAnim = null
setLayerType(LAYER_TYPE_NONE, null)
alpha = 1f
}
private fun stripeColor(domain: String): Int = when (domain) {
"light" -> Colors.STRIPE_LIGHT
"switch" -> Colors.STRIPE_SWITCH
"sensor" -> Colors.STRIPE_SENSOR
"binary_sensor" -> Colors.STRIPE_BINARY_SENSOR
"script" -> Colors.STRIPE_SCRIPT
"automation" -> Colors.STRIPE_AUTOMATION
else -> Colors.STRIPE_DEFAULT
}
private fun dp(v: Int) = (v * density + 0.5f).toInt()
private fun sp(v: Int) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, v.toFloat(), resources.displayMetrics)
@Suppress("DEPRECATION")
private fun makeLayout(text: String, paint: TextPaint, width: Int): StaticLayout =
if (Build.VERSION.SDK_INT >= 23)
StaticLayout.Builder
.obtain(text, 0, text.length, paint, width)
.setMaxLines(1)
.setEllipsize(TextUtils.TruncateAt.END)
.build()
else
StaticLayout(
text, 0, text.length, paint, width,
Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false,
TextUtils.TruncateAt.END, width
)
}

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FF000000" />
<size android:width="2dp" />
</shape>

View File

@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/ha_white"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_connection"
android:textStyle="bold"
android:typeface="monospace"
android:textSize="18sp"
android:textColor="@color/ha_black" />
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/ha_black"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_url"
android:typeface="monospace"
android:textSize="12sp"
android:textColor="@color/ha_black" />
<EditText
android:id="@+id/etUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:hint="http://10.0.2.2:8123"
android:inputType="textUri"
android:typeface="monospace"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/ha_black"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_token"
android:typeface="monospace"
android:textSize="12sp"
android:textColor="@color/ha_black" />
<EditText
android:id="@+id/etToken"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:hint="••••••••••••"
android:inputType="textPassword"
android:typeface="monospace"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/ha_black"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_refresh"
android:typeface="monospace"
android:textSize="12sp"
android:textColor="@color/ha_black" />
<EditText
android:id="@+id/etRefreshInterval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:hint="30"
android:inputType="number"
android:typeface="monospace"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/ha_black"
android:layout_marginBottom="24dp" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/ha_black"
android:layout_marginLeft="4dp"
android:layout_marginTop="4dp" />
<Button
android:id="@+id/btnTestAndSave"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@string/btn_test_save"
android:background="@color/ha_blue"
android:textColor="@color/ha_white"
android:typeface="monospace"
android:textStyle="bold" />
</FrameLayout>
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:typeface="monospace"
android:textSize="12sp"
android:gravity="center"
android:textColor="@color/ha_black" />
</LinearLayout>

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/ha_white"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_entity_selection"
android:textStyle="bold"
android:typeface="monospace"
android:textSize="18sp"
android:textColor="@color/ha_black" />
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/ha_black"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp" />
<EditText
android:id="@+id/etSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Szukaj encji..."
android:inputType="text"
android:typeface="monospace"
android:textSize="14sp"
android:drawableLeft="@android:drawable/ic_menu_search" />
<ListView
android:id="@+id/lvEntities"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="8dp"
android:divider="@color/ha_black"
android:dividerHeight="1dp" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<View
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/ha_black"
android:layout_marginLeft="4dp"
android:layout_marginTop="4dp" />
<Button
android:id="@+id/btnSave"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@string/btn_save_selected"
android:background="@color/ha_blue"
android:textColor="@color/ha_white"
android:typeface="monospace"
android:textStyle="bold" />
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/ha_white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_instructions"
android:textStyle="bold"
android:typeface="monospace"
android:textSize="18sp"
android:textColor="@color/ha_black"
android:layout_marginBottom="16dp" />
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/ha_black"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inst_header_1"
android:textStyle="bold"
android:typeface="monospace"
android:textSize="14sp"
android:textColor="@color/ha_black"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inst_body_1"
android:typeface="monospace"
android:textSize="12sp"
android:textColor="@color/ha_black"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="24dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inst_header_2"
android:textStyle="bold"
android:typeface="monospace"
android:textSize="14sp"
android:textColor="@color/ha_black"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inst_body_2"
android:typeface="monospace"
android:textSize="12sp"
android:textColor="@color/ha_black"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="24dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inst_header_3"
android:textStyle="bold"
android:typeface="monospace"
android:textSize="14sp"
android:textColor="@color/ha_black"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inst_body_3"
android:typeface="monospace"
android:textSize="12sp"
android:textColor="@color/ha_black"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="24dp" />
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/ha_white">
<!-- TOP BAR -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/ha_white">
<RelativeLayout
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:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="USTAWIENIA"
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="USTAWIENIA"
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_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: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_height="0dp"
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>

View File

@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/ha_white"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_settings"
android:textStyle="bold"
android:typeface="monospace"
android:textSize="18sp"
android:textColor="@color/ha_black"
android:layout_marginBottom="16dp" />
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/ha_black"
android:layout_marginBottom="16dp" />
<!-- ENTITY SELECTION BUTTON -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/ha_black"
android:layout_marginLeft="4dp"
android:layout_marginTop="4dp" />
<Button
android:id="@+id/btnEntitySelection"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@string/btn_entity_selection"
android:background="@color/ha_yellow"
android:textColor="@color/ha_black"
android:typeface="monospace"
android:textStyle="bold" />
</FrameLayout>
<!-- CONNECTION BUTTON -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<View
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/ha_black"
android:layout_marginLeft="4dp"
android:layout_marginTop="4dp" />
<Button
android:id="@+id/btnGoToConnection"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@string/title_connection"
android:background="@color/ha_blue"
android:textColor="@color/ha_white"
android:typeface="monospace"
android:textStyle="bold" />
</FrameLayout>
<!-- INSTRUCTIONS BUTTON -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<View
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/ha_black"
android:layout_marginLeft="4dp"
android:layout_marginTop="4dp" />
<Button
android:id="@+id/btnInstructions"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@string/btn_instructions"
android:background="@color/ha_gray_light"
android:textColor="@color/ha_black"
android:typeface="monospace"
android:textStyle="bold" />
</FrameLayout>
<!-- CHANGE LANGUAGE BUTTON -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<View
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/ha_black"
android:layout_marginLeft="4dp"
android:layout_marginTop="4dp" />
<Button
android:id="@+id/btnChangeLang"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@string/btn_change_lang"
android:background="@color/ha_orange"
android:textColor="@color/ha_black"
android:typeface="monospace"
android:textStyle="bold" />
</FrameLayout>
<!-- DELETE ALL BUTTON -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<View
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/ha_black"
android:layout_marginLeft="4dp"
android:layout_marginTop="4dp" />
<Button
android:id="@+id/btnDeleteAll"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@string/btn_delete_all"
android:background="@color/ha_red"
android:textColor="@color/ha_white"
android:typeface="monospace"
android:textStyle="bold" />
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">RetroHA</string>
<string name="app_title">HA_PANEL</string>
<string name="state_on">ON</string>
<string name="state_off">OFF</string>
<string name="state_unavailable">UNAVAILABLE</string>
<string name="state_toggling"></string>
<string name="btn_settings">SETTINGS</string>
<string name="toast_widget_add">Add widget — TODO</string>
<string name="tab_all">ALL</string>
<string name="tab_lighting">LIGHTING</string>
<string name="tab_sockets">SOCKETS</string>
<string name="tab_power">POWER</string>
<string name="tab_weather">WEATHER</string>
<string name="title_settings">ENTITY SELECTION</string>
<string name="title_connection">SERVER CONFIGURATION</string>
<string name="title_instructions">USER MANUAL</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_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_5">5. CONNECTION CHANGE CLEARS WIDGET LIST.</string>
<string name="label_url">URL ADDRESS</string>
<string name="label_token">ACCESS TOKEN</string>
<string name="label_refresh">REFRESH (SECONDS)</string>
<string name="btn_test_save">TEST AND SAVE</string>
<string name="btn_delete_all">DELETE ALL WIDGETS</string>
<string name="btn_save_selected">SAVE SELECTED</string>
<string name="btn_change_lang">JĘZYK / LANGUAGE</string>
<string name="btn_entity_selection">WIDGET SELECTION</string>
<string name="btn_instructions">USER MANUAL</string>
<string name="status_connecting">CONNECTING...</string>
<string name="status_connected">CONNECTED SUCCESSFULLY</string>
<string name="status_offline">OFFLINE</string>
<string name="status_error_ha">HA ERROR</string>
<string name="status_no_token">NO TOKEN</string>
<string name="confirm_delete_all">ARE YOU SURE YOU WANT TO DELETE ALL WIDGETS FROM HOME?</string>
<string name="confirm_change_conn">CHANGING ADDRESS OR TOKEN WILL DELETE ALL SELECTED WIDGETS. CONTINUE?</string>
<string name="dialog_brightness">BRIGHTNESS</string>
<string name="dialog_ustaw">SET</string>
<string name="dialog_anuluj">CANCEL</string>
<string name="inst_header_1">BASIC USAGE</string>
<string name="inst_body_1">- Short click on a tile toggles the device (ON/OFF).\n- Long press on a light tile opens a dedicated brightness slider.\n- Use the tabs at the top (e.g., Lighting, Power) to quickly filter displayed devices.</string>
<string name="inst_header_2">SORTING &amp; VISIBILITY</string>
<string name="inst_body_2">- Widgets are displayed and sorted based on their \'entity_id\' (identifiers from Home Assistant).\n- Currently supported domains: light, switch, power, sensor, weather.</string>
<string name="inst_header_3">OPTIMIZATION</string>
<string name="inst_body_3">- The app is built for older tablets. It uses a custom, lightweight graphics engine (Bauhaus Canvas).\n- KIOSK mode is enabled by default (the screen will never turn off while the app is visible).\n- Background refresh rate can be adjusted in \'Server Configuration\'.</string>
</resources>

View File

@@ -0,0 +1,11 @@
<resources>
<!-- Force light Bauhaus theme even in system dark mode -->
<style name="Theme.RetroHA" parent="@android:style/Theme.Holo.Light.NoActionBar">
<item name="android:windowBackground">@color/ha_white</item>
<item name="android:colorBackground">@color/ha_white</item>
<item name="android:textColor">@color/ha_black</item>
<item name="android:listDivider">@null</item>
<item name="android:listSelector">@android:color/transparent</item>
<item name="android:scrollbarThumbVertical">@drawable/scrollbar_thumb</item>
</style>
</resources>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Bauhaus primaries -->
<color name="ha_black">#FF000000</color>
<color name="ha_white">#FFFFFFFF</color>
<color name="ha_red">#FFE23A24</color>
<color name="ha_yellow">#FFFAD02C</color>
<color name="ha_blue">#FF0056B3</color>
<!-- Bauhaus secondaries -->
<color name="ha_orange">#FFF4801A</color>
<color name="ha_green">#FF2D7D46</color>
<color name="ha_violet">#FF6B3FA0</color>
<!-- Neutrals -->
<color name="ha_gray_light">#FFCCCCCC</color>
<color name="ha_gray_mid">#FF888888</color>
<color name="ha_gray_dark">#FF444444</color>
</resources>

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">RetroHA</string>
<string name="app_title">HA_PANEL</string>
<string name="state_on">WŁ.</string>
<string name="state_off">WYŁ.</string>
<string name="state_unavailable">NIEOSIĄGALNE</string>
<string name="state_toggling"></string>
<string name="btn_settings">USTAWIENIA</string>
<string name="toast_widget_add">Dodaj widget — TODO</string>
<string name="tab_all">WSZYSTKO</string>
<string name="tab_lighting">OŚWIETLENIE</string>
<string name="tab_sockets">GNIAZDKA</string>
<string name="tab_power">MOC</string>
<string name="tab_weather">POGODA</string>
<string name="title_settings">WYBÓR ENCJI</string>
<string name="title_connection">KONFIGURACJA SERWERA</string>
<string name="title_instructions">INSTRUKCJA OBSŁUGI</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_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_5">5. ZMIANA POŁĄCZENIA CZYŚCI LISTĘ WIDŻETÓW.</string>
<string name="label_url">ADRES URL</string>
<string name="label_token">TOKEN DOSTĘPU</string>
<string name="label_refresh">ODŚWIEŻANIE (SEKUNDY)</string>
<string name="btn_test_save">TESTUJ I ZAPISZ</string>
<string name="btn_delete_all">USUŃ WSZYSTKIE WIDŻETY</string>
<string name="btn_save_selected">ZAPISZ WYBRANE</string>
<string name="btn_change_lang">JĘZYK / LANGUAGE</string>
<string name="btn_entity_selection">WYBÓR WIDŻETÓW</string>
<string name="btn_instructions">INSTRUKCJA OBSŁUGI</string>
<string name="status_connecting">ŁĄCZENIE...</string>
<string name="status_connected">POŁĄCZONO POMYŚLNIE</string>
<string name="status_offline">OFFLINE</string>
<string name="status_error_ha">BŁĄD HA</string>
<string name="status_no_token">BRAK TOKENU</string>
<string name="confirm_delete_all">CZY NA PEWNO CHCESZ USUNĄĆ WSZYSTKIE WIDŻETY Z PULPITU?</string>
<string name="confirm_change_conn">ZMIANA ADRESU LUB TOKENU SPOWODUJE USUNIĘCIE WSZYSTKICH WYBRANYCH WIDŻETÓW. KONTYNUOWAĆ?</string>
<string name="dialog_brightness">JASNOŚĆ</string>
<string name="dialog_ustaw">USTAW</string>
<string name="dialog_anuluj">ANULUJ</string>
<string name="inst_header_1">PODSTAWY OBSŁUGI</string>
<string name="inst_body_1">- Krótkie kliknięcie kafelka przełącza urządzenie (ON/OFF).\n- Długie przytrzymanie kafelka światła otwiera dedykowany suwak jasności.\n- Zakładki na górze ekranu (np. Oświetlenie, Moc) służą do szybkiego filtrowania wyświetlanych urządzeń.</string>
<string name="inst_header_2">SORTOWANIE I WIDOCZNOŚĆ</string>
<string name="inst_body_2">- Widżety są wyświetlane i sortowane w oparciu o ich \'entity_id\' (identyfikatory z Home Assistanta).\n- Aktualnie obsługiwane domeny: light, switch, power, sensor, weather.</string>
<string name="inst_header_3">OPTYMALIZACJA</string>
<string name="inst_body_3">- Aplikacja jest zbudowana z myślą o starych tabletach. Używa autorskiego, lekkiego silnika graficznego (Bauhaus Canvas).\n- Tryb KIOSK jest włączony domyślnie (ekran nigdy nie zgaśnie, gdy aplikacja jest widoczna).\n- Odświeżanie w tle można dostosować w \'Konfiguracji Połączenia\'.</string>
</resources>

View File

@@ -0,0 +1,10 @@
<resources>
<style name="Theme.RetroHA" parent="@android:style/Theme.Holo.Light.NoActionBar">
<item name="android:windowBackground">@color/ha_white</item>
<item name="android:colorBackground">@color/ha_white</item>
<item name="android:textColor">@color/ha_black</item>
<item name="android:listDivider">@null</item>
<item name="android:listSelector">@android:color/transparent</item>
<item name="android:scrollbarThumbVertical">@drawable/scrollbar_thumb</item>
</style>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>

View File

@@ -0,0 +1,9 @@
package com.example.retroha
import org.junit.Test
import org.junit.Assert.*
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}