Initial commit
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)
|
||||
}
|
||||
}
|
||||