Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ out/

# Gradle files
.gradle/
.gradle_home/
build/

# Local configuration file (sdk path, etc)
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ Google and Google specifically disclaims all warranties as to its quality,
merchantability, or fitness for a particular purpose.

Google Play and the Google Play logo are trademarks of Google LLC.


I just added some home screen widgets so that i can control my wled light just from my home screen without even opening the app.
Comment thread
sohailxi marked this conversation as resolved.
40 changes: 40 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@
<data android:scheme="http" android:host="4.3.2.1" />
</intent-filter>
</activity>
<activity
android:name=".widget.PresetWidgetConfigureActivity"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>

<receiver
Comment thread
sohailxi marked this conversation as resolved.
android:name=".widget.WledWidgetReceiver"
Expand All @@ -71,6 +78,38 @@
</intent-filter>
</activity>

<receiver
android:name=".widget.PresetWidgetProvider"
android:exported="true"
android:label="@string/appwidget_text">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="ca.cgagnier.wlednativeandroid.widget.ACTION_TRIGGER_PRESET" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_preset_info" />
</receiver>

<receiver
android:name=".widget.SmallPresetWidgetProvider"
android:exported="true"
android:label="WLED Small Presets">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="ca.cgagnier.wlednativeandroid.widget.ACTION_TRIGGER_PRESET" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_preset_small_info" />
</receiver>

<service
android:name=".widget.PresetWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false" />

<!-- Device Controls (Quick Access) service for Android 11+ -->
<service
android:name=".service.controls.WledControlsService"
Expand All @@ -85,3 +124,4 @@
</application>

</manifest>

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ca.cgagnier.wlednativeandroid.model.wledapi

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class Preset(
@Json(name = "n") val name: String = "",
@Json(name = "on") val on: Boolean? = null,
@Json(name = "bri") val brightness: Int? = null,
@Json(name = "mainseg") val mainSegment: Int? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ca.cgagnier.wlednativeandroid.service.api
import ca.cgagnier.wlednativeandroid.model.Device
import ca.cgagnier.wlednativeandroid.model.wledapi.Info
import ca.cgagnier.wlednativeandroid.model.wledapi.JsonPost
import ca.cgagnier.wlednativeandroid.model.wledapi.Preset
import ca.cgagnier.wlednativeandroid.model.wledapi.State
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
Expand All @@ -21,8 +22,17 @@ interface DeviceApi {
@GET("json/info")
suspend fun getInfo(): Response<Info>

@GET("presets.json")
suspend fun getPresets(): Response<Map<String, Preset>>

@GET("json/state")
suspend fun getState(): Response<State>

@POST("json/state")
suspend fun postState(@Body state: State): Response<State>

@POST("json/state")
suspend fun postJson(@Body state: JsonPost): Response<State>
suspend fun postJson(@Body jsonPost: JsonPost): Response<State>

@Multipart
@POST("update")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package ca.cgagnier.wlednativeandroid.widget

import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ca.cgagnier.wlednativeandroid.model.Device
import ca.cgagnier.wlednativeandroid.repository.DeviceRepository
import ca.cgagnier.wlednativeandroid.ui.theme.WLEDNativeTheme
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import androidx.lifecycle.ViewModel

@AndroidEntryPoint
class PresetWidgetConfigureActivity : ComponentActivity() {

private val viewModel: PresetWidgetConfigureViewModel by viewModels()
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setResult(RESULT_CANCELED)

val intent = intent
val extras = intent.extras
if (extras != null) {
appWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
)
}

if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish()
return
}

setContent {
WLEDNativeTheme {
DeviceSelectionScreen(viewModel) { device ->
saveDevicePref(this, appWidgetId, device)

val resultValue = Intent()
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
setResult(RESULT_OK, resultValue)
finish()
}
}
}
}

companion object {
private const val PREFS_NAME = "ca.cgagnier.wlednativeandroid.widget.PresetWidget"
private const val PREF_PREFIX_KEY = "appwidget_"

internal fun saveDevicePref(context: Context, appWidgetId: Int, device: Device) {
val prefs = context.getSharedPreferences(PREFS_NAME, 0).edit()
prefs.putString(PREF_PREFIX_KEY + appWidgetId, device.address)
prefs.putString(PREF_PREFIX_KEY + appWidgetId + "_name", device.name)
prefs.apply()
}

internal fun loadDeviceAddress(context: Context, appWidgetId: Int): String? {
val prefs = context.getSharedPreferences(PREFS_NAME, 0)
return prefs.getString(PREF_PREFIX_KEY + appWidgetId, null)
}

internal fun loadDeviceName(context: Context, appWidgetId: Int): String {
val prefs = context.getSharedPreferences(PREFS_NAME, 0)
return prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_name", "WLED Device") ?: "WLED Device"
Comment thread
sohailxi marked this conversation as resolved.
}
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DeviceSelectionScreen(viewModel: PresetWidgetConfigureViewModel, onDeviceSelected: (Device) -> Unit) {
val devices by viewModel.allDevices.collectAsState(initial = emptyList())

Scaffold(
topBar = {
TopAppBar(title = { Text("Select WLED Device") })
}
) { padding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
items(devices) { device ->
DeviceItem(device, onDeviceSelected)
}
}
}
}

@Composable
private fun DeviceItem(device: Device, onClick: (Device) -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable { onClick(device) }
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = device.name, style = MaterialTheme.typography.titleMedium)
Text(text = device.address, style = MaterialTheme.typography.bodyMedium)
}
}
}

@HiltViewModel
class PresetWidgetConfigureViewModel @Inject constructor(
private val deviceRepository: DeviceRepository
) : ViewModel() {
val allDevices: Flow<List<Device>> = deviceRepository.allDevices
}

val Device.name: String
get() = if (customName.isNotEmpty()) customName else if (originalName.isNotEmpty()) originalName else address
Loading