Initial commit
22
.gitattributes
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Handle line endings automatically
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Standard binary files
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.webp binary
|
||||||
|
*.jar binary
|
||||||
|
*.zip binary
|
||||||
|
*.aar binary
|
||||||
|
*.apk binary
|
||||||
|
|
||||||
|
# Android specific
|
||||||
|
*.flat binary
|
||||||
|
*.webp binary
|
||||||
|
|
||||||
|
# Ensure gradlew has LF line endings and is executable
|
||||||
|
gradlew text eol=lf
|
||||||
|
gradlew.bat text eol=crlf
|
||||||
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
/.idea/deploymentTargetSelector.xml
|
||||||
|
/.idea/androidTestResultsUserPreferences.xml
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/app/build
|
||||||
|
/shared/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*.class
|
||||||
|
.kotlin
|
||||||
833
OPTIMIZATION_PLAN.md
Normal file
@@ -0,0 +1,833 @@
|
|||||||
|
# Plan optymalizacji — MyApplication (HA Widget Panel)
|
||||||
|
|
||||||
|
Cel: maksymalna wydajność na AllWinner A13 (single-core 1.2GHz, 512MB RAM, Android 4.0, API 14).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spis treści
|
||||||
|
|
||||||
|
1. [Stack technologiczny](#1-stack-technologiczny)
|
||||||
|
2. [System designu — Bauhaus / Swiss Grid](#2-system-designu--bauhaus--swiss-grid)
|
||||||
|
3. [Konfiguracja buildu](#3-konfiguracja-buildu)
|
||||||
|
4. [Rendering — Custom View](#4-rendering--custom-view)
|
||||||
|
5. [GridView — lista widgetów](#5-gridview--lista-widgetów)
|
||||||
|
6. [Ikony — Canvas drawing](#6-ikony--canvas-drawing)
|
||||||
|
7. [StaticLayout — pre-obliczony tekst](#7-staticlayout--pre-obliczony-tekst)
|
||||||
|
8. [Animacje — GPU only](#8-animacje--gpu-only)
|
||||||
|
9. [Sieć — OkHttp + Retrofit](#9-sieć--okhttp--retrofit)
|
||||||
|
10. [JSON — TypeAdaptery zamiast refleksji](#10-json--typeadaptery-zamiast-refleksji)
|
||||||
|
11. [Wątki — HandlerThread](#11-wątki--handlerthread)
|
||||||
|
12. [Pamięć — LruCache + SparseArray](#12-pamięć--lrucache--sparsearray)
|
||||||
|
13. [Inicjalizacja przy starcie](#13-inicjalizacja-przy-starcie)
|
||||||
|
14. [Języki](#14-języki)
|
||||||
|
15. [Priorytety implementacji](#15-priorytety-implementacji)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Stack technologiczny
|
||||||
|
|
||||||
|
| Element | Wybór | Powód |
|
||||||
|
|---|---|---|
|
||||||
|
| Język | **Kotlin** | Identyczny bytecode co Java — zero różnicy runtime |
|
||||||
|
| UI framework | **Natywne Android API 14** | Zero overhead bibliotek, pełna kontrola |
|
||||||
|
| Tema | **Theme.Holo** | Natywna, zero zależności, dostępna od API 11 |
|
||||||
|
| Lista widgetów | **android.widget.GridView** | Natywna, wbudowane recycling widoków, brak Jetpack |
|
||||||
|
| Sieć | **OkHttp 3.12.x + Retrofit 2.6.4** | Ostatnie wersje wspierające API 14 |
|
||||||
|
| JSON | **Gson + TypeAdaptery** | Streaming parsing bez refleksji |
|
||||||
|
| **ZABRONIONE** | AppCompat, Material Design, RecyclerView, Jetpack, Compose, CardView | Zbędny overhead / wymaga API 21+ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. System designu — Bauhaus / Swiss Grid
|
||||||
|
|
||||||
|
### Paleta kolorów
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
object Colors {
|
||||||
|
const val WHITE = 0xFFFFFFFF.toInt() // tło, tekst na ciemnym
|
||||||
|
const val BLACK = 0xFF000000.toInt() // tekst, obramowania, cień
|
||||||
|
const val RED = 0xFFE23A24.toInt() // błędy, alarmy, akcent krytyczny
|
||||||
|
const val YELLOW = 0xFFFAD02C.toInt() // aktywny stan, akcent główny
|
||||||
|
const val BLUE = 0xFF0056B3.toInt() // info, przyciski, akcent drugorzędny
|
||||||
|
const val GRAY = 0xFF888888.toInt() // wyłączone, disabled
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Żadnych innych kolorów. Żadnych gradientów. Żadnego alpha blendingu poza animacjami opacity.
|
||||||
|
|
||||||
|
### Typografia — system monospace
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
object Fonts {
|
||||||
|
// Wbudowany w każde urządzenie z API 14 — zero overhead, zero KB w APK
|
||||||
|
val REGULAR: Typeface = Typeface.create("monospace", Typeface.NORMAL)
|
||||||
|
val BOLD: Typeface = Typeface.create("monospace", Typeface.BOLD)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Każdy `TextPaint` używa `Fonts.REGULAR` lub `Fonts.BOLD`. Żadnych innych krojów.
|
||||||
|
|
||||||
|
### Skala typograficzna
|
||||||
|
|
||||||
|
```
|
||||||
|
Rola Rozmiar Waga Zastosowanie
|
||||||
|
────────────────────────────────────────────────────
|
||||||
|
value 20sp BOLD główna wartość encji (21.5°C, ON, 58%)
|
||||||
|
label 12sp NORMAL nazwa encji
|
||||||
|
secondary 10sp NORMAL jednostka, czas ostatniej aktualizacji
|
||||||
|
```
|
||||||
|
|
||||||
|
### Siatka spacingu — tylko wielokrotności 4dp
|
||||||
|
|
||||||
|
```
|
||||||
|
Padding wewnętrzny karty: 8dp
|
||||||
|
Odstęp między kartami: 4dp
|
||||||
|
Padding ekranu: 8dp
|
||||||
|
Grubość borderu: 1dp lub 2dp
|
||||||
|
Szerokość lewego paska: 4dp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Geometria — ZERO zaokrągleń
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// ŹLE — zaokrąglone rogi są ZABRONIONE
|
||||||
|
canvas.drawRoundRect(rect, 8f, 8f, paint)
|
||||||
|
|
||||||
|
// DOBRZE — ostre rogi wszędzie
|
||||||
|
canvas.drawRect(rect, paint)
|
||||||
|
```
|
||||||
|
|
||||||
|
`android:radius` i `corners` w drawable — ZABRONIONE.
|
||||||
|
|
||||||
|
### Hard shadow — technika
|
||||||
|
|
||||||
|
Zamiast `elevation` — przesunięty czarny `View`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<FrameLayout android:padding="4dp">
|
||||||
|
|
||||||
|
<!-- Cień: ten sam rozmiar co karta, przesunięty o 4dp -->
|
||||||
|
<View
|
||||||
|
android:background="#000000"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginLeft="4dp"
|
||||||
|
android:layout_marginTop="4dp"/>
|
||||||
|
|
||||||
|
<!-- Karta na wierzchu -->
|
||||||
|
<com.example.myapplication.ui.WidgetCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Konfiguracja buildu
|
||||||
|
|
||||||
|
### `app/build.gradle.kts`
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 14
|
||||||
|
// Multidex WYŁĄCZONY — bez AppCompat/Material szacowane ~25K metod, poniżej limitu 65K
|
||||||
|
resourceConfigurations += listOf("pl", "en")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllWinner A13 = ARM Cortex-A8 = armeabi-v7a
|
||||||
|
// Wyklucza x86/arm64 → mniejszy APK, szybsza instalacja
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
isEnable = true
|
||||||
|
reset()
|
||||||
|
include("armeabi-v7a")
|
||||||
|
isUniversalApk = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Sieć — OK, nie są to UI wrappery
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
implementation(libs.okhttp.logging)
|
||||||
|
implementation(libs.retrofit)
|
||||||
|
implementation(libs.retrofit.gson)
|
||||||
|
implementation(libs.gson)
|
||||||
|
implementation(libs.mpandroidchart)
|
||||||
|
|
||||||
|
// USUNIĘTE: AppCompat, Material, core-ktx, multidex, RecyclerView
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `res/values/themes.xml`
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<style name="AppTheme" parent="@android:style/Theme.Holo">
|
||||||
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
|
<item name="android:colorBackground">@android:color/black</item>
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dlaczego odpada multidex
|
||||||
|
|
||||||
|
```
|
||||||
|
AppCompat usunięty: -30 000 metod
|
||||||
|
Material usunięty: -15 000 metod
|
||||||
|
core-ktx usunięte: - 5 000 metod
|
||||||
|
──────────────────────────────────────
|
||||||
|
Szacowany total: ~25 000 metod ← poniżej limitu 65K
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Rendering — Custom View
|
||||||
|
|
||||||
|
**Zasady:**
|
||||||
|
- `Paint` / `TextPaint` / `Rect` / `RectF` — pola klasy, nigdy zmienne lokalne w `onDraw`
|
||||||
|
- `Fonts.REGULAR` / `Fonts.BOLD` przypisane raz przy inicjalizacji obiektu Paint
|
||||||
|
- `drawRect` zamiast `drawRoundRect` — ostre rogi, szybsze
|
||||||
|
- Stała wysokość karty w `onMeasure` — eliminuje kosztowne obliczenia `wrap_content`
|
||||||
|
- Settery z `if (field == value) return` — bez zbędnych `invalidate()`
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class WidgetCardView(context: Context) : View(context) {
|
||||||
|
|
||||||
|
private val dp = resources.displayMetrics.density
|
||||||
|
|
||||||
|
private val paintBg = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
private val paintBorder = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = 2f * dp
|
||||||
|
color = Colors.BLACK
|
||||||
|
}
|
||||||
|
private val paintLabel = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
typeface = Fonts.REGULAR
|
||||||
|
textSize = 12f * dp
|
||||||
|
color = Colors.WHITE
|
||||||
|
}
|
||||||
|
private val paintValue = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
typeface = Fonts.BOLD
|
||||||
|
textSize = 20f * dp
|
||||||
|
color = Colors.WHITE
|
||||||
|
}
|
||||||
|
private val paintSecondary = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
typeface = Fonts.REGULAR
|
||||||
|
textSize = 10f * dp
|
||||||
|
color = Colors.GRAY
|
||||||
|
}
|
||||||
|
private val paintStripe = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
|
||||||
|
private val rectBg = Rect()
|
||||||
|
private val rectStripe = Rect()
|
||||||
|
|
||||||
|
private var labelLayout: StaticLayout? = null
|
||||||
|
private var valueLayout: StaticLayout? = null
|
||||||
|
private var secondaryLayout: StaticLayout? = null
|
||||||
|
|
||||||
|
var label: String = ""
|
||||||
|
set(v) { if (field == v) return; field = v; rebuildLayouts(); invalidate() }
|
||||||
|
var value: String = ""
|
||||||
|
set(v) { if (field == v) return; field = v; rebuildLayouts(); invalidate() }
|
||||||
|
var secondaryText: String = ""
|
||||||
|
set(v) { if (field == v) return; field = v; rebuildLayouts(); invalidate() }
|
||||||
|
var isActive: Boolean = false
|
||||||
|
set(v) { if (field == v) return; field = v; invalidate() }
|
||||||
|
var stripeColor: Int = Colors.BLUE
|
||||||
|
set(v) { if (field == v) return; field = v; invalidate() }
|
||||||
|
|
||||||
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
|
val stripeW = (4f * dp).toInt()
|
||||||
|
rectBg.set(0, 0, w, h)
|
||||||
|
rectStripe.set(0, 0, stripeW, h)
|
||||||
|
rebuildLayouts()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rebuildLayouts() {
|
||||||
|
if (width == 0) return
|
||||||
|
val contentW = width - rectStripe.width() - (8 * dp).toInt() - (4 * dp).toInt()
|
||||||
|
labelLayout = buildLayout(label, paintLabel, contentW)
|
||||||
|
valueLayout = buildLayout(value, paintValue, contentW)
|
||||||
|
secondaryLayout = buildLayout(secondaryText, paintSecondary, contentW)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildLayout(text: String, paint: TextPaint, w: Int): StaticLayout =
|
||||||
|
StaticLayout(text, paint, w, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false)
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
val dp8 = 8f * dp
|
||||||
|
val dp4 = 4f * dp
|
||||||
|
|
||||||
|
// Tło karty
|
||||||
|
paintBg.color = if (isActive) Colors.YELLOW else Colors.BLACK
|
||||||
|
canvas.drawRect(rectBg, paintBg)
|
||||||
|
|
||||||
|
// Pionowy pasek koloru (domena encji)
|
||||||
|
paintStripe.color = stripeColor
|
||||||
|
canvas.drawRect(rectStripe, paintStripe)
|
||||||
|
|
||||||
|
// Obramowanie
|
||||||
|
canvas.drawRect(rectBg, paintBorder)
|
||||||
|
|
||||||
|
// Kolor tekstu zależy od tła
|
||||||
|
val textColor = if (isActive) Colors.BLACK else Colors.WHITE
|
||||||
|
paintLabel.color = textColor
|
||||||
|
paintValue.color = textColor
|
||||||
|
|
||||||
|
val textX = rectStripe.width() + dp8
|
||||||
|
|
||||||
|
canvas.save()
|
||||||
|
canvas.translate(textX, dp8)
|
||||||
|
labelLayout?.draw(canvas)
|
||||||
|
canvas.restore()
|
||||||
|
|
||||||
|
val labelH = labelLayout?.height?.toFloat() ?: 0f
|
||||||
|
canvas.save()
|
||||||
|
canvas.translate(textX, dp8 + labelH + dp4)
|
||||||
|
valueLayout?.draw(canvas)
|
||||||
|
canvas.restore()
|
||||||
|
|
||||||
|
val valueH = valueLayout?.height?.toFloat() ?: 0f
|
||||||
|
canvas.save()
|
||||||
|
canvas.translate(textX, dp8 + labelH + dp4 + valueH + dp4)
|
||||||
|
secondaryLayout?.draw(canvas)
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
val w = MeasureSpec.getSize(widthMeasureSpec)
|
||||||
|
val h = when {
|
||||||
|
resources.configuration.screenHeightDp < 480 -> (72 * dp).toInt()
|
||||||
|
else -> (88 * dp).toInt()
|
||||||
|
}
|
||||||
|
setMeasuredDimension(w, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Czego unikać:**
|
||||||
|
- `ConstraintLayout` w itemach gridu — skomplikowany algorytm pomiaru
|
||||||
|
- `CardView` — wymaga AppCompat, ma elevation
|
||||||
|
- Zagnieżdżenie ViewGroup > 2 poziomów
|
||||||
|
- `wrap_content` na wysokości itemów gridu
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. GridView — lista widgetów
|
||||||
|
|
||||||
|
`android.widget.GridView` — natywny od API 1, zero zależności, wbudowane recycling widoków.
|
||||||
|
Zastępuje `RecyclerView` (Jetpack — zabroniony).
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val gridView = GridView(this).apply {
|
||||||
|
numColumns = when {
|
||||||
|
resources.configuration.screenWidthDp >= 840 -> 4 // tablet landscape
|
||||||
|
resources.configuration.screenWidthDp >= 600 -> 3 // tablet portrait
|
||||||
|
else -> 2 // telefon
|
||||||
|
}
|
||||||
|
val dp4 = (4 * resources.displayMetrics.density).toInt()
|
||||||
|
val dp8 = (8 * resources.displayMetrics.density).toInt()
|
||||||
|
horizontalSpacing = dp4
|
||||||
|
verticalSpacing = dp4
|
||||||
|
setPadding(dp8, dp8, dp8, dp8)
|
||||||
|
stretchMode = GridView.STRETCH_COLUMN_WIDTH
|
||||||
|
adapter = widgetAdapter
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### BaseAdapter
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class WidgetAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private var items: List<WidgetConfig>
|
||||||
|
) : BaseAdapter() {
|
||||||
|
|
||||||
|
override fun getCount() = items.size
|
||||||
|
override fun getItem(pos: Int) = items[pos]
|
||||||
|
override fun getItemId(pos: Int) = items[pos].entityId.hashCode().toLong()
|
||||||
|
override fun hasStableIds() = true // GridView może reużywać widoki
|
||||||
|
|
||||||
|
override fun getView(pos: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
// convertView = recycled view — ZAWSZE reużywaj jeśli istnieje
|
||||||
|
val card = (convertView as? WidgetCardView) ?: WidgetCardView(context)
|
||||||
|
val item = items[pos]
|
||||||
|
card.label = item.friendlyName
|
||||||
|
card.value = item.state
|
||||||
|
card.secondaryText = item.lastUpdated
|
||||||
|
card.isActive = item.isOn
|
||||||
|
card.stripeColor = domainColor(item.domain)
|
||||||
|
return card
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(newItems: List<WidgetConfig>) {
|
||||||
|
items = newItems
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bezpośrednia aktualizacja stanu encji bez przebudowy adaptera
|
||||||
|
fun updateState(entityId: String, newState: String, isOn: Boolean) {
|
||||||
|
val idx = items.indexOfFirst { it.entityId == entityId }
|
||||||
|
if (idx == -1) return
|
||||||
|
items = items.toMutableList().also {
|
||||||
|
it[idx] = it[idx].copy(state = newState, isOn = isOn)
|
||||||
|
}
|
||||||
|
// Aktualizuj tylko widoczną kartę jeśli jest na ekranie
|
||||||
|
val card = gridView?.getChildAt(idx) as? WidgetCardView
|
||||||
|
if (card != null) {
|
||||||
|
card.value = newState
|
||||||
|
card.isActive = isOn
|
||||||
|
} else {
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun domainColor(domain: String) = when (domain) {
|
||||||
|
"light" -> Colors.YELLOW
|
||||||
|
"switch" -> Colors.BLUE
|
||||||
|
"sensor" -> Colors.BLUE
|
||||||
|
"binary_sensor" -> Colors.BLUE
|
||||||
|
"script", "scene" -> Colors.RED
|
||||||
|
else -> Colors.GRAY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Uwaga:** `GridView` nie ma `DiffUtil` (Jetpack). Dla małej listy widgetów (< 30 pozycji)
|
||||||
|
`notifyDataSetChanged()` przy zmianie konfiguracji jest akceptowalne. Przy aktualizacji
|
||||||
|
samego stanu encji — bezpośrednia modyfikacja `WidgetCardView` bez przebudowy adaptera.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Ikony — Canvas drawing
|
||||||
|
|
||||||
|
Zakaz bitmap (PNG/JPG). Ikony rysowane bezpośrednio na `Canvas` jako kształty geometryczne.
|
||||||
|
Styl Bauhaus: proste figury, jeden kolor, zero dekoracji.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
object HaIcons {
|
||||||
|
|
||||||
|
// Paint jako pole — zero alokacji przy każdym draw()
|
||||||
|
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()
|
||||||
|
|
||||||
|
fun draw(canvas: Canvas, domain: String, x: Float, y: Float, size: Float, color: Int) {
|
||||||
|
fillPaint.color = color
|
||||||
|
strokePaint.color = color
|
||||||
|
when (domain) {
|
||||||
|
"light" -> drawLight(canvas, x, y, size)
|
||||||
|
"switch" -> drawSwitch(canvas, x, y, size)
|
||||||
|
"sensor" -> drawSensor(canvas, x, y, size)
|
||||||
|
"binary_sensor" -> drawBinarySensor(canvas, x, y, size)
|
||||||
|
"script", "automation" -> drawScript(canvas, x, y, size)
|
||||||
|
else -> drawGeneric(canvas, x, y, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawLight(canvas: Canvas, x: Float, y: Float, s: Float) {
|
||||||
|
// Kwadrat + promienie — geometryczna "lampa"
|
||||||
|
tmpRect.set(x + s*0.25f, y + s*0.25f, x + s*0.75f, y + s*0.75f)
|
||||||
|
canvas.drawRect(tmpRect, fillPaint)
|
||||||
|
canvas.drawLine(x + s*0.5f, y, x + s*0.5f, y + s*0.2f, strokePaint)
|
||||||
|
canvas.drawLine(x + s*0.5f, y + s*0.8f, x + s*0.5f, y + s, strokePaint)
|
||||||
|
canvas.drawLine(x, y + s*0.5f, x + s*0.2f, y + s*0.5f, strokePaint)
|
||||||
|
canvas.drawLine(x + s*0.8f, y + s*0.5f, x + s, y + s*0.5f, strokePaint)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawSwitch(canvas: Canvas, x: Float, y: Float, s: Float) {
|
||||||
|
// Prostokąt poziomy + blok po prawej
|
||||||
|
tmpRect.set(x, y + s*0.35f, x + s, y + s*0.65f)
|
||||||
|
canvas.drawRect(tmpRect, fillPaint)
|
||||||
|
tmpRect.set(x + s*0.55f, y + s*0.15f, x + s*0.95f, y + s*0.85f)
|
||||||
|
canvas.drawRect(tmpRect, fillPaint)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawSensor(canvas: Canvas, x: Float, y: Float, s: Float) {
|
||||||
|
// Krzyż — pomiar
|
||||||
|
tmpRect.set(x + s*0.4f, y, x + s*0.6f, y + s)
|
||||||
|
canvas.drawRect(tmpRect, fillPaint)
|
||||||
|
tmpRect.set(x, y + s*0.4f, x + s, y + s*0.6f)
|
||||||
|
canvas.drawRect(tmpRect, fillPaint)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawBinarySensor(canvas: Canvas, x: Float, y: Float, s: Float) {
|
||||||
|
// Kwadrat w kwadracie
|
||||||
|
tmpRect.set(x + s*0.1f, y + s*0.1f, x + s*0.9f, y + s*0.9f)
|
||||||
|
canvas.drawRect(tmpRect, strokePaint)
|
||||||
|
tmpRect.set(x + s*0.3f, y + s*0.3f, x + s*0.7f, y + s*0.7f)
|
||||||
|
canvas.drawRect(tmpRect, fillPaint)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawScript(canvas: Canvas, x: Float, y: Float, s: Float) {
|
||||||
|
// Trzy poziome paski — lista komend
|
||||||
|
val h = s * 0.12f
|
||||||
|
val gap = s * 0.25f
|
||||||
|
for (i in 0..2) {
|
||||||
|
tmpRect.set(x, y + gap * i + gap * 0.5f, x + s, y + gap * i + gap * 0.5f + h)
|
||||||
|
canvas.drawRect(tmpRect, fillPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawGeneric(canvas: Canvas, x: Float, y: Float, s: Float) {
|
||||||
|
tmpRect.set(x + s*0.15f, y + s*0.15f, x + s*0.85f, y + s*0.85f)
|
||||||
|
canvas.drawRect(tmpRect, fillPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ponieważ ikony są prostymi operacjami Canvas (kilka `drawRect` / `drawLine`), rasteryzacja
|
||||||
|
jest natychmiastowa — nie potrzeba cache'owania bitmap.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. StaticLayout — pre-obliczony tekst
|
||||||
|
|
||||||
|
`canvas.drawText()` mierzy tekst przy każdym `onDraw`. `StaticLayout` robi to raz.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun buildLayout(text: String, paint: TextPaint, widthPx: Int): StaticLayout =
|
||||||
|
StaticLayout(text, paint, widthPx, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false)
|
||||||
|
|
||||||
|
// W onDraw — zero mierzenia:
|
||||||
|
canvas.save()
|
||||||
|
canvas.translate(x, y)
|
||||||
|
layout.draw(canvas)
|
||||||
|
canvas.restore()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reguły:**
|
||||||
|
- Przebuduj tylko gdy zmienia się tekst **lub** `onSizeChanged`
|
||||||
|
- `if (field == value) return` w każdym setterze przed `invalidate()`
|
||||||
|
- Guard `if (width == 0) return` w `rebuildLayouts()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Animacje — GPU only
|
||||||
|
|
||||||
|
Tylko właściwości animowane przez GPU via `ViewPropertyAnimator`.
|
||||||
|
Zero animacji zmieniających layout — `width`/`height`/`margin`/`padding` wywołują
|
||||||
|
`requestLayout()` co zabija wydajność.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Wysunięcie panelu konfiguracji z dołu
|
||||||
|
fun revealPanel(panel: View) {
|
||||||
|
panel.visibility = View.VISIBLE
|
||||||
|
panel.translationY = panel.height.toFloat()
|
||||||
|
panel.animate()
|
||||||
|
.translationY(0f)
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(200)
|
||||||
|
.setInterpolator(DecelerateInterpolator())
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chowanie panelu
|
||||||
|
fun hidePanel(panel: View) {
|
||||||
|
panel.animate()
|
||||||
|
.translationY(panel.height.toFloat())
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(150)
|
||||||
|
.setInterpolator(LinearInterpolator())
|
||||||
|
.withEndAction { panel.visibility = View.GONE }
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feedback dotknięcia karty
|
||||||
|
fun tapFeedback(card: View) {
|
||||||
|
card.animate()
|
||||||
|
.alpha(0.4f)
|
||||||
|
.setDuration(80)
|
||||||
|
.setInterpolator(LinearInterpolator())
|
||||||
|
.withEndAction {
|
||||||
|
card.animate().alpha(1f).setDuration(120).start()
|
||||||
|
}
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tabela dozwolonych właściwości
|
||||||
|
|
||||||
|
```
|
||||||
|
Właściwość GPU Zastosowanie
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
translationX/Y ✓ przesuwanie paneli, drawer
|
||||||
|
scaleX/Y ✓ powiększenie aktywnego widgetu
|
||||||
|
alpha ✓ fade, feedback dotknięcia
|
||||||
|
rotation ✓ obrót ikony ładowania
|
||||||
|
|
||||||
|
width/height ✗ requestLayout() — ZABRONIONE
|
||||||
|
margin/padding ✗ requestLayout() — ZABRONIONE
|
||||||
|
layout_weight ✗ requestLayout() — ZABRONIONE
|
||||||
|
backgroundColor ✗ software render — zmień kolor przez Paint w onDraw
|
||||||
|
```
|
||||||
|
|
||||||
|
**Interpolatory:** tylko `LinearInterpolator` lub `DecelerateInterpolator`.
|
||||||
|
**Czas trwania:** 80ms–250ms. Żadnych sprężyn, odbić, elastyczności.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Sieć — OkHttp + Retrofit
|
||||||
|
|
||||||
|
Jeden singleton na całą aplikację — `ConnectionPool` jest drogi w tworzeniu.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
object NetworkModule {
|
||||||
|
|
||||||
|
val okHttpClient: OkHttpClient by lazy {
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.connectionPool(ConnectionPool(3, 30, TimeUnit.SECONDS))
|
||||||
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(15, TimeUnit.SECONDS)
|
||||||
|
.cache(Cache(File(context.cacheDir, "ha_http"), 2L * 1024 * 1024))
|
||||||
|
.addInterceptor(AuthInterceptor(token))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val retrofit: Retrofit by lazy {
|
||||||
|
Retrofit.Builder()
|
||||||
|
.baseUrl(baseUrl)
|
||||||
|
.client(okHttpClient)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create(buildGson()))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. JSON — TypeAdaptery zamiast refleksji
|
||||||
|
|
||||||
|
Gson domyślnie używa refleksji — na A13 odczuwalne przy każdym parsowaniu.
|
||||||
|
TypeAdapter = ręczne parsowanie strumieniowe, 3-5x szybsze.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class HaEntityTypeAdapter : TypeAdapter<HaEntity>() {
|
||||||
|
override fun read(reader: JsonReader): HaEntity {
|
||||||
|
var entityId = ""; var state = ""; var friendlyName = ""
|
||||||
|
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 = reader.nextString()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
}
|
||||||
|
else -> reader.skipValue() // nie parsuj niepotrzebnych pól
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
return HaEntity(entityId, state, friendlyName)
|
||||||
|
}
|
||||||
|
override fun write(out: JsonWriter, value: HaEntity?) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Wątki — HandlerThread
|
||||||
|
|
||||||
|
Na single-core nie twórz wielu wątków — context switching jest kosztowny.
|
||||||
|
Jeden `HandlerThread` na całą komunikację z HA.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
object HaWorker {
|
||||||
|
private val thread = HandlerThread("ha-worker").also { it.start() }
|
||||||
|
val handler = Handler(thread.looper)
|
||||||
|
val mainThread = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
fun post(block: () -> Unit) = handler.post(block)
|
||||||
|
fun postDelayed(ms: Long, block: () -> Unit) = handler.postDelayed(block, ms)
|
||||||
|
fun cancelAll() = handler.removeCallbacksAndMessages(null)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dlaczego `HandlerThread` zamiast `Executors.newSingleThreadExecutor()`:**
|
||||||
|
- `postDelayed` dla retry z exponential backoff
|
||||||
|
- `removeCallbacks` do anulowania zakolejkowanych zadań bez dodatkowej synchronizacji
|
||||||
|
- Wbudowana kolejka z priorytetem
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Pamięć — LruCache + SparseArray
|
||||||
|
|
||||||
|
### Cache stanów encji
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
object EntityStateCache {
|
||||||
|
private val cache = LruCache<String, EntityState>(50)
|
||||||
|
fun get(id: String) = cache.get(id)
|
||||||
|
fun put(id: String, s: EntityState) = cache.put(id, s)
|
||||||
|
fun evictAll() = cache.evictAll()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SparseArray zamiast HashMap dla kluczy Int
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// ŹLE — autoboxing Int→Integer przy każdym get/put
|
||||||
|
val viewTypes = HashMap<Int, Int>()
|
||||||
|
|
||||||
|
// DOBRZE — zero alokacji, zoptymalizowane dla kluczy int
|
||||||
|
val viewTypes = SparseArray<Int>()
|
||||||
|
```
|
||||||
|
|
||||||
|
### onTrimMemory — oddaj pamięć gdy system prosi
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MainActivity : Activity() {
|
||||||
|
override fun onTrimMemory(level: Int) {
|
||||||
|
super.onTrimMemory(level)
|
||||||
|
if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
|
||||||
|
EntityStateCache.evictAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WeakReference w callbackach sieciowych
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// ŹLE — anonimowa klasa trzyma referencję do Activity
|
||||||
|
client.newCall(req).enqueue(object : Callback {
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
textView.text = "..." // wyciek Activity po rotacji!
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// DOBRZE
|
||||||
|
class StateCallback(activity: MainActivity) : Callback {
|
||||||
|
private val ref = WeakReference(activity)
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
ref.get()?.runOnUiThread { /* aktualizuj UI */ }
|
||||||
|
}
|
||||||
|
override fun onFailure(call: Call, e: IOException) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Inicjalizacja przy starcie
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class App : Application() {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
// Pierwsze połączenie w tle — tworzy connection pool przed pierwszym UI request
|
||||||
|
HaWorker.post { warmupNetwork() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun warmupNetwork() {
|
||||||
|
try {
|
||||||
|
NetworkModule.okHttpClient
|
||||||
|
.newCall(pingRequest())
|
||||||
|
.execute()
|
||||||
|
.close()
|
||||||
|
} catch (_: IOException) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Co kiedy jest przeliczane
|
||||||
|
|
||||||
|
```
|
||||||
|
Zdarzenie Co się dzieje
|
||||||
|
──────────────────────────────────────────────────────────────────
|
||||||
|
Start aplikacji warmup connection pool w tle
|
||||||
|
onSizeChanged karty rebuild StaticLayout (1x na widget)
|
||||||
|
onDraw drawRect + HaIcons.draw + layout.draw
|
||||||
|
→ ZERO alokacji, ZERO parsowania
|
||||||
|
Zmiana stanu encji bezpośredni setter na WidgetCardView
|
||||||
|
→ tylko ta karta wywołuje invalidate()
|
||||||
|
onTrimMemory (MODERATE+) EntityStateCache.evictAll() → RAM zwolniony
|
||||||
|
Obrócenie ekranu stany encji z EntityStateCache (bez sieci)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Języki
|
||||||
|
|
||||||
|
```
|
||||||
|
res/values/strings.xml angielski (default)
|
||||||
|
res/values-pl/strings.xml polski
|
||||||
|
```
|
||||||
|
|
||||||
|
`resourceConfigurations += listOf("pl", "en")` wycina pozostałe tłumaczenia z bibliotek
|
||||||
|
narzędziowych (~200KB oszczędności w APK).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Priorytety implementacji
|
||||||
|
|
||||||
|
```
|
||||||
|
Priorytet Optymalizacja Wpływ
|
||||||
|
──────────────────────────────────────────────────────────────
|
||||||
|
KRYTYCZNY Brak AppCompat/Material → Theme.Holo RAM / metody
|
||||||
|
Custom View — drawRect, brak zaokrągleń rendering
|
||||||
|
Ikony jako Canvas draw (brak bitmap) rendering / RAM
|
||||||
|
Jeden OkHttpClient singleton RAM
|
||||||
|
|
||||||
|
DUŻY StaticLayout (pre-obliczony tekst) rendering
|
||||||
|
TypeAdaptery Gson CPU / parsing
|
||||||
|
HandlerThread (1 wątek tła) CPU
|
||||||
|
GridView z BaseAdapter + hasStableIds rendering
|
||||||
|
|
||||||
|
ŚREDNI EntityStateCache (LruCache) sieć / RAM
|
||||||
|
SparseArray zamiast HashMap RAM
|
||||||
|
onTrimMemory → czyszczenie cache RAM
|
||||||
|
Animacje tylko GPU (ViewPropertyAnimator) jank
|
||||||
|
Bezpośredni setter na kartę (bez adapter) rendering
|
||||||
|
|
||||||
|
MAŁY ABI splits (armeabi-v7a only) APK size
|
||||||
|
isShrinkResources = true APK size
|
||||||
|
resourceConfigurations (pl + en) APK size
|
||||||
|
WeakReference w callbackach memory leaks
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zależności po zmianach
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# gradle/libs.versions.toml
|
||||||
|
# USUNIĘTO: AppCompat, Material, core-ktx, multidex
|
||||||
|
|
||||||
|
[versions]
|
||||||
|
okhttp = "3.12.13"
|
||||||
|
retrofit = "2.6.4"
|
||||||
|
gson = "2.10.1"
|
||||||
|
mpandroidchart = "v3.1.0"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||||
|
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
||||||
|
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||||
|
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
|
||||||
|
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
|
||||||
|
mpandroidchart = { group = "com.github.PhilJay", name = "MPAndroidChart", version.ref = "mpandroidchart" }
|
||||||
|
```
|
||||||
36
README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# RetroHA
|
||||||
|
|
||||||
|
**RetroHA** is a lightweight and efficient dashboard for Home Assistant, specifically designed for legacy Android tablets intended to serve as wall-mounted home control panels.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
* **Bauhaus Canvas Engine**: A custom graphics engine. The interface does not use standard Android XML views but is manually rendered on a `Canvas`. This ensures extreme fluidity even on decade-old devices.
|
||||||
|
* **Low-End Hardware Optimization**: Minimal RAM usage, no unnecessary libraries, and aggressive caching of graphical layouts.
|
||||||
|
* **Bauhaus Design**: A minimalist aesthetic inspired by the Bauhaus school—high contrast, geometric shapes, and maximum legibility.
|
||||||
|
* **Kiosk Mode**: Built-in "Wake Lock" support to prevent the screen from turning off while the panel is active.
|
||||||
|
* **Rapid Control**:
|
||||||
|
* Tile Click: Toggle or Execute.
|
||||||
|
* Light Long Press: Brightness menu with "slide and release" gesture (auto-save).
|
||||||
|
|
||||||
|
## Libraries & Acknowledgments
|
||||||
|
|
||||||
|
This project utilizes the following Open Source libraries (compliant with F-Droid requirements):
|
||||||
|
|
||||||
|
* **Retrofit** (Square Inc.) — Apache License 2.0: Type-safe HTTP client for Android and Java.
|
||||||
|
* **OkHttp** (Square Inc.) — Apache License 2.0: An efficient HTTP client for Android.
|
||||||
|
* **Gson** (Google) — Apache License 2.0: A Java library that can be used to convert Java Objects into their JSON representation.
|
||||||
|
* **MPAndroidChart** (PhilJay) — Apache License 2.0: A powerful Android chart view / graph view library.
|
||||||
|
* **AndroidX Libraries** (Google) — Apache License 2.0: Component libraries for Android.
|
||||||
|
|
||||||
|
## Technical Requirements
|
||||||
|
|
||||||
|
* **Android**: minSdk 14 (Support back to Android 4.0 ICS), optimized for Android 4.4+.
|
||||||
|
* **Home Assistant**: Requires Server URL and a Long-Lived Access Token.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
The current project status and planned features are detailed in [TASKS.md](./TASKS.md).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the **GNU GPL v3**. See the repository for details.
|
||||||
64
ROADMAP.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# RetroHA — Roadmap
|
||||||
|
|
||||||
|
## Sieć i integracja z HA
|
||||||
|
|
||||||
|
- [x] Ekran konfiguracji: adres serwera HA + Long-Lived Access Token
|
||||||
|
- [x] Klient REST (Retrofit) — pobieranie stanu encji (`/api/states`)
|
||||||
|
- [x] Wywoływanie serwisów HA (`/api/services/<domain>/<service>`) — toggle, execute
|
||||||
|
- [ ] WebSocket (`/api/websocket`) — subskrypcja zdarzeń `state_changed` dla aktualizacji w czasie rzeczywistym
|
||||||
|
- [ ] Logika ponownego łączenia po utracie połączenia (exponential backoff)
|
||||||
|
- [x] Wskaźnik stanu połączenia w górnym pasku (połączono / rozłączono / łączenie)
|
||||||
|
- [x] Obsługa błędów: timeout, 401 Unauthorized, 404 Not Found
|
||||||
|
|
||||||
|
## Architektura — warstwa sieciowa (moduł `shared`)
|
||||||
|
|
||||||
|
- [x] `HaApiService` — interfejs Retrofit z endpointami REST
|
||||||
|
- [x] `HaEntity` — DTO mapujący JSON z `/api/states`
|
||||||
|
- [ ] `HaWebSocketClient` — klient WebSocket z obsługą `auth`, `subscribe_events`
|
||||||
|
- [x] `EntityStateCache` — mapujący `entity_id → EntityState` (w `Prefs` i `MainActivity`)
|
||||||
|
- [ ] `HaWorker` — `HandlerThread` jako jeden wątek sieciowy (single-core device)
|
||||||
|
- [x] `WeakReference` / `runOnUiThread` w callbackach sieciowych — unikanie wycieków `Activity`
|
||||||
|
|
||||||
|
## Konfiguracja widgetów
|
||||||
|
|
||||||
|
- [x] Ekran dodawania widgetu — przeglądarka encji HA (lista z wyszukiwarką)
|
||||||
|
- [x] Trwałe zapisywanie listy widgetów (JSON w `SharedPreferences`)
|
||||||
|
- [x] Usuwanie widgetu (przez ekran wyboru encji)
|
||||||
|
- [ ] Zmiana kolejności widgetów (drag & drop lub przyciski ↑↓)
|
||||||
|
- [x] Filtrowanie widgetów przez zakładki (Oświetlenie, Gniazdka, Moc, itp.)
|
||||||
|
|
||||||
|
## Obsługa typów encji
|
||||||
|
|
||||||
|
- [x] `light` — sterowanie jasnością (slider, auto-zapis po puszczeniu)
|
||||||
|
- [ ] `light` — sterowanie kolorem (jeśli obsługuje)
|
||||||
|
- [ ] `climate` — wyświetlanie temperatury docelowej, trybu HVAC; widok szczegółów
|
||||||
|
- [x] `sensor` / `binary_sensor` — wyświetlanie wartości i stanów
|
||||||
|
- [x] `switch` — przełączanie ON/OFF
|
||||||
|
- [x] `automation` / `script` — wywoływanie akcji (EXECUTE)
|
||||||
|
|
||||||
|
## Wydajność i stabilność
|
||||||
|
|
||||||
|
- [x] **Bauhaus Canvas** — autorski, lekki silnik graficzny (ręczne rysowanie na `Canvas`)
|
||||||
|
- [x] Optymalizacja `onDraw` — cachowanie `StaticLayout`, unikanie alokacji w pętli rysowania
|
||||||
|
- [x] Obsługa bardzo starych urządzeń (optymalizacja pod słabe procesory)
|
||||||
|
- [ ] ProGuard / R8 dla release buildu
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
- [x] Zakładki (Tabs) do szybkiego filtrowania domen
|
||||||
|
- [x] Obsługa języków (PL/EN)
|
||||||
|
- [x] Tryb Kiosk (Wake Lock) — ekran nie gaśnie przy widocznej aplikacji
|
||||||
|
- [x] Responsywność — automatyczna liczba kolumn (Grid)
|
||||||
|
|
||||||
|
## i18n
|
||||||
|
|
||||||
|
- [x] Pełne wsparcie dla języka polskiego (PL)
|
||||||
|
- [x] Pełne wsparcie dla języka angielskiego (EN)
|
||||||
|
- [ ] Tłumaczenia na języki: DE, FR, UK
|
||||||
|
|
||||||
|
## Testy
|
||||||
|
|
||||||
|
- [x] Testy instrumentalne — `AppNavigationTest`, `AutomatedClickTest`
|
||||||
|
- [x] Monkey Stress Test — testowanie stabilności pod dużym obciążeniem
|
||||||
|
- [x] Unit testy — `ExampleUnitTest`
|
||||||
|
|
||||||
47
TASKS.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# RetroHA — Task Tracker
|
||||||
|
|
||||||
|
Methodology: Incremental Kanban (Simple .md file)
|
||||||
|
|
||||||
|
## [PHASE 1] Foundations (Proof of Concept)
|
||||||
|
- [x] Project configuration (minSdk 19, Kotlin, Retrofit)
|
||||||
|
- [x] Login screen (URL + Token)
|
||||||
|
- [x] Configuration persistence in SharedPreferences
|
||||||
|
- [x] Basic REST client (fetch `/api/states`)
|
||||||
|
- [x] Connection verification (Online/Offline status)
|
||||||
|
|
||||||
|
## [PHASE 2] Custom Graphics Engine (Bauhaus Canvas)
|
||||||
|
- [x] `WidgetCardView` implementation (Custom View)
|
||||||
|
- [x] Manual tile rendering (background, border, shadow)
|
||||||
|
- [x] Manual text rendering with `StaticLayout` optimization
|
||||||
|
- [x] Icon system (`HaIcons.kt`) rendered on Canvas
|
||||||
|
- [x] Widget grid (GridView) with responsive column count
|
||||||
|
|
||||||
|
## [PHASE 3] Data Integration & Entity Selection
|
||||||
|
- [x] Mapping Home Assistant states to visual styles
|
||||||
|
- [x] Entity browser and selection screen (Searchable)
|
||||||
|
- [x] Persistent storage of selected widget list
|
||||||
|
- [x] Domain filtering via Tabs (Lighting, Sockets, Power, etc.)
|
||||||
|
- [x] Automatic background data refresh
|
||||||
|
|
||||||
|
## [PHASE 4] Interactions & Control
|
||||||
|
- [x] Short click handling (Toggle / Execute)
|
||||||
|
- [x] Pulsing animation during state transitions (Toggling)
|
||||||
|
- [x] Brightness control menu (LightControlDialog)
|
||||||
|
- [x] Button removal from brightness menu (Auto-save on release)
|
||||||
|
- [x] Dismiss menu on outside touch
|
||||||
|
|
||||||
|
## [PHASE 5] UX Polish & Stability
|
||||||
|
- [x] Internationalization (PL/EN support)
|
||||||
|
- [x] Kiosk Mode (Wake Lock - prevent screen sleep)
|
||||||
|
- [x] `onDraw` performance optimization (zero allocation in draw loop)
|
||||||
|
- [x] Stability testing (Monkey Stress Test)
|
||||||
|
|
||||||
|
## Future Development
|
||||||
|
- [ ] WebSocket implementation for real-time updates
|
||||||
|
- [ ] Support for `climate` and `media_player` domains
|
||||||
|
- [ ] RGB color support for lighting
|
||||||
|
|
||||||
|
---
|
||||||
|
*Legend:*
|
||||||
|
- [x] - Done
|
||||||
|
- [ ] - To Do
|
||||||
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
72
app/build.gradle.kts
Normal 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
@@ -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
|
||||||
@@ -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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/src/main/AndroidManifest.xml
Normal 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>
|
||||||
10
app/src/main/java/com/example/retroha/BaseActivity.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
91
app/src/main/java/com/example/retroha/LanguageActivity.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
313
app/src/main/java/com/example/retroha/MainActivity.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/src/main/java/com/example/retroha/SettingsActivity.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/src/main/java/com/example/retroha/data/Prefs.kt
Normal 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()
|
||||||
|
}
|
||||||
45
app/src/main/java/com/example/retroha/i18n/AndroidStrings.kt
Normal 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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
16
app/src/main/java/com/example/retroha/i18n/LocaleHelper.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
42
app/src/main/java/com/example/retroha/network/HaClient.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/src/main/java/com/example/retroha/ui/BauhausCheckbox.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
6
app/src/main/java/com/example/retroha/ui/Fonts.kt
Normal 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) }
|
||||||
|
}
|
||||||
74
app/src/main/java/com/example/retroha/ui/HaIcons.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/src/main/java/com/example/retroha/ui/WidgetAdapter.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
208
app/src/main/java/com/example/retroha/ui/WidgetCardView.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
30
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal 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>
|
||||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal 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>
|
||||||
5
app/src/main/res/drawable/scrollbar_thumb.xml
Normal 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>
|
||||||
133
app/src/main/res/layout/activity_connection_settings.xml
Normal 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>
|
||||||
65
app/src/main/res/layout/activity_entity_selection.xml
Normal 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>
|
||||||
90
app/src/main/res/layout/activity_instructions.xml
Normal 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>
|
||||||
131
app/src/main/res/layout/activity_main.xml
Normal 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>
|
||||||
134
app/src/main/res/layout/activity_settings.xml
Normal 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>
|
||||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
||||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal 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>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
60
app/src/main/res/values-en/strings.xml
Normal 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 & 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>
|
||||||
11
app/src/main/res/values-night/themes.xml
Normal 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>
|
||||||
19
app/src/main/res/values/colors.xml
Normal 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>
|
||||||
60
app/src/main/res/values/strings.xml
Normal 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>
|
||||||
10
app/src/main/res/values/themes.xml
Normal 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>
|
||||||
13
app/src/main/res/xml/backup_rules.xml
Normal 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>
|
||||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal 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>
|
||||||
4
app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<base-config cleartextTrafficPermitted="true" />
|
||||||
|
</network-security-config>
|
||||||
9
app/src/test/java/com/example/retroha/ExampleUnitTest.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
build.gradle.kts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application) apply false
|
||||||
|
alias(libs.plugins.kotlin.jvm) apply false
|
||||||
|
alias(libs.plugins.dokka)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global Dokka configuration for Wiki
|
||||||
|
subprojects {
|
||||||
|
plugins.withType<org.jetbrains.kotlin.dokka.gradle.DokkaPlugin> {
|
||||||
|
tasks.withType<org.jetbrains.kotlin.dokka.gradle.AbstractDokkaTask>().configureEach {
|
||||||
|
val format = name.substringAfter("dokka").substringBefore("MultiModule").lowercase()
|
||||||
|
outputDirectory.set(file("${project.rootDir}/docs/wiki/$format/${project.name}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
gradle.properties
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. For more details, visit
|
||||||
|
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
12
gradle/gradle-daemon-jvm.properties
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#This file is generated by updateDaemonJvm
|
||||||
|
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
|
||||||
|
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
|
||||||
|
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
|
||||||
|
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
|
||||||
|
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
|
||||||
|
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
|
||||||
|
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
|
||||||
|
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
|
||||||
|
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
|
||||||
|
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
|
||||||
|
toolchainVersion=21
|
||||||
36
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[versions]
|
||||||
|
agp = "9.2.1"
|
||||||
|
kotlin = "2.0.21"
|
||||||
|
dokka = "2.0.0"
|
||||||
|
multidex = "2.0.1"
|
||||||
|
coreKtx = "1.10.1"
|
||||||
|
junit = "4.13.2"
|
||||||
|
junitVersion = "1.1.5"
|
||||||
|
espressoCore = "3.5.1"
|
||||||
|
appcompat = "1.6.1"
|
||||||
|
material = "1.10.0"
|
||||||
|
okhttp = "3.12.13"
|
||||||
|
retrofit = "2.6.4"
|
||||||
|
gson = "2.10.1"
|
||||||
|
mpandroidchart = "v3.1.0"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
|
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||||
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
|
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||||
|
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
||||||
|
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||||
|
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
|
||||||
|
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
|
||||||
|
mpandroidchart = { group = "com.github.PhilJay", name = "MPAndroidChart", version.ref = "mpandroidchart" }
|
||||||
|
multidex = { group = "androidx.multidex", name = "multidex", version.ref = "multidex" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
|
dokka = { id = "org.jetbrains.kotlin.dokka", version.ref = "dokka" }
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
9
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#Sun May 17 20:54:36 CEST 2026
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
251
gradlew
vendored
Executable file
@@ -0,0 +1,251 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH="\\\"\\\""
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
94
gradlew.bat
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
28
settings.gradle.kts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google {
|
||||||
|
content {
|
||||||
|
includeGroupByRegex("com\\.android.*")
|
||||||
|
includeGroupByRegex("com\\.google.*")
|
||||||
|
includeGroupByRegex("androidx.*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url = uri("https://jitpack.io") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "RetroHA"
|
||||||
|
include(":app")
|
||||||
|
include(":shared")
|
||||||
8
shared/build.gradle.kts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
alias(libs.plugins.dokka)
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(11)
|
||||||
|
}
|
||||||
39
shared/src/main/kotlin/com/example/retroha/i18n/StringKey.kt
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package com.example.retroha.i18n
|
||||||
|
enum class StringKey {
|
||||||
|
STATE_ON,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
STATE_TOGGLING,
|
||||||
|
BTN_SETTINGS,
|
||||||
|
TOAST_WIDGET_ADD,
|
||||||
|
TAB_ALL,
|
||||||
|
TAB_LIGHTING,
|
||||||
|
TAB_SOCKETS,
|
||||||
|
TAB_POWER,
|
||||||
|
TAB_WEATHER,
|
||||||
|
TITLE_SETTINGS,
|
||||||
|
TITLE_CONNECTION,
|
||||||
|
TITLE_INSTRUCTIONS,
|
||||||
|
INSTRUCTION_1,
|
||||||
|
INSTRUCTION_2,
|
||||||
|
INSTRUCTION_3,
|
||||||
|
INSTRUCTION_4,
|
||||||
|
INSTRUCTION_5,
|
||||||
|
LABEL_URL,
|
||||||
|
LABEL_TOKEN,
|
||||||
|
LABEL_REFRESH,
|
||||||
|
BTN_TEST_SAVE,
|
||||||
|
BTN_DELETE_ALL,
|
||||||
|
BTN_SAVE_SELECTED,
|
||||||
|
BTN_CHANGE_LANG,
|
||||||
|
STATUS_CONNECTING,
|
||||||
|
STATUS_CONNECTED,
|
||||||
|
STATUS_OFFLINE,
|
||||||
|
STATUS_ERROR_HA,
|
||||||
|
STATUS_NO_TOKEN,
|
||||||
|
CONFIRM_DELETE_ALL,
|
||||||
|
CONFIRM_CHANGE_CONN,
|
||||||
|
DIALOG_BRIGHTNESS,
|
||||||
|
DIALOG_USTAW,
|
||||||
|
DIALOG_ANULUJ
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.example.retroha.i18n
|
||||||
|
interface Strings {
|
||||||
|
operator fun get(key: StringKey): String
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
package com.example.retroha.model
|
||||||
|
enum class EntityState { ON, OFF, UNAVAILABLE, TOGGLING }
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.retroha.model
|
||||||
|
data class WidgetConfig(
|
||||||
|
val id: Long,
|
||||||
|
val entityId: String,
|
||||||
|
val label: String,
|
||||||
|
val value: String,
|
||||||
|
val secondary: String,
|
||||||
|
val domain: String,
|
||||||
|
val state: EntityState,
|
||||||
|
val brightness: Int? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.example.retroha.model
|
||||||
|
enum class WidgetInteraction { TOGGLE, EXECUTE, READ_ONLY }
|
||||||
|
fun String.toWidgetInteraction(): WidgetInteraction = when (this) {
|
||||||
|
"light",
|
||||||
|
"switch",
|
||||||
|
"fan",
|
||||||
|
"input_boolean",
|
||||||
|
"automation",
|
||||||
|
"lock" -> WidgetInteraction.TOGGLE
|
||||||
|
"script",
|
||||||
|
"scene",
|
||||||
|
"button",
|
||||||
|
"input_button" -> WidgetInteraction.EXECUTE
|
||||||
|
else -> WidgetInteraction.READ_ONLY
|
||||||
|
}
|
||||||
28
shared/src/main/kotlin/com/example/retroha/theme/Colors.kt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.example.retroha.theme
|
||||||
|
object Colors {
|
||||||
|
const val BLACK = 0xFF000000.toInt()
|
||||||
|
const val WHITE = 0xFFFFFFFF.toInt()
|
||||||
|
const val RED = 0xFFE23A24.toInt()
|
||||||
|
const val YELLOW = 0xFFFAD02C.toInt()
|
||||||
|
const val BLUE = 0xFF0056B3.toInt()
|
||||||
|
const val ORANGE = 0xFFF4801A.toInt()
|
||||||
|
const val GREEN = 0xFF2D7D46.toInt()
|
||||||
|
const val VIOLET = 0xFF6B3FA0.toInt()
|
||||||
|
const val GRAY_LIGHT = 0xFFCCCCCC.toInt()
|
||||||
|
const val GRAY_MID = 0xFF888888.toInt()
|
||||||
|
const val GRAY_DARK = 0xFF444444.toInt()
|
||||||
|
const val STATUS_ON = YELLOW
|
||||||
|
const val STATUS_OFF = WHITE
|
||||||
|
const val STATUS_UNAVAILABLE = GRAY_LIGHT
|
||||||
|
const val STATUS_TOGGLING = WHITE
|
||||||
|
const val BORDER_DEFAULT = BLACK
|
||||||
|
const val BORDER_TOGGLING = BLUE
|
||||||
|
const val BORDER_UNAVAILABLE = GRAY_MID
|
||||||
|
const val STRIPE_LIGHT = ORANGE
|
||||||
|
const val STRIPE_SWITCH = BLUE
|
||||||
|
const val STRIPE_SENSOR = VIOLET
|
||||||
|
const val STRIPE_BINARY_SENSOR = VIOLET
|
||||||
|
const val STRIPE_SCRIPT = RED
|
||||||
|
const val STRIPE_AUTOMATION = RED
|
||||||
|
const val STRIPE_DEFAULT = GRAY_DARK
|
||||||
|
}
|
||||||