diff --git a/.gitignore b/.gitignore index a05276e7..fb8e9138 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ out/ # Gradle files .gradle/ +.gradle_home/ build/ # Local configuration file (sdk path, etc) diff --git a/README.md b/README.md index 126fdaf2..1d73c9df 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8f257f5e..4cb363a6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,13 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Preset.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Preset.kt new file mode 100644 index 00000000..fbba691a --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/model/wledapi/Preset.kt @@ -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 +) diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt index a88dbc23..794f0c4a 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/service/api/DeviceApi.kt @@ -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 @@ -21,8 +22,17 @@ interface DeviceApi { @GET("json/info") suspend fun getInfo(): Response + @GET("presets.json") + suspend fun getPresets(): Response> + + @GET("json/state") + suspend fun getState(): Response + + @POST("json/state") + suspend fun postState(@Body state: State): Response + @POST("json/state") - suspend fun postJson(@Body state: JsonPost): Response + suspend fun postJson(@Body jsonPost: JsonPost): Response @Multipart @POST("update") diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt new file mode 100644 index 00000000..e3dd236e --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetConfigureActivity.kt @@ -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" + } + } +} + +@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> = deviceRepository.allDevices +} + +val Device.name: String + get() = if (customName.isNotEmpty()) customName else if (originalName.isNotEmpty()) originalName else address diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetProvider.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetProvider.kt new file mode 100644 index 00000000..38f0019d --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetProvider.kt @@ -0,0 +1,149 @@ +package ca.cgagnier.wlednativeandroid.widget + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.util.Log +import android.widget.RemoteViews +import ca.cgagnier.wlednativeandroid.R +import ca.cgagnier.wlednativeandroid.model.wledapi.State +import ca.cgagnier.wlednativeandroid.service.api.DeviceApiFactory +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class PresetWidgetProvider : AppWidgetProvider() { + + @Inject + lateinit var deviceApiFactory: DeviceApiFactory + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + for (appWidgetId in appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId) + } + } + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + if (intent.action == ACTION_TRIGGER_PRESET) { + val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + val presetId = intent.getIntExtra(EXTRA_PRESET_ID, -1) + val deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS) + val listId = intent.getIntExtra(EXTRA_LIST_ID, R.id.preset_list) + + if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID && presetId != -1 && deviceAddress != null) { + triggerPreset(context, deviceAddress, presetId, appWidgetId, listId) + } + } + + if (intent.action == ACTION_REFRESH) { + val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + val listId = intent.getIntExtra(EXTRA_LIST_ID, R.id.preset_list) + if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + val appWidgetManager = AppWidgetManager.getInstance(context) + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, listId) + } + } + } + + private fun triggerPreset(context: Context, deviceAddress: String, presetId: Int, appWidgetId: Int, listId: Int) { + CoroutineScope(Dispatchers.IO).launch { + try { + val api = deviceApiFactory.create(deviceAddress) + val response = api.postState(State(selectedPresetId = presetId)) + + if (response.isSuccessful) { + // Add a small delay to allow device state to update internally if needed + // kotlinx.coroutines.delay(200) + // Wait, I need to import delay if I use it. + // Or just trigger update immediately. + val appWidgetManager = AppWidgetManager.getInstance(context) + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, listId) + Log.d(TAG, "Triggered widget update for $appWidgetId") + } + } catch (e: Exception) { + Log.e(TAG, "Error triggering preset", e) + } + } + } + + companion object { + const val ACTION_TRIGGER_PRESET = "ca.cgagnier.wlednativeandroid.widget.ACTION_TRIGGER_PRESET" + const val ACTION_REFRESH = "ca.cgagnier.wlednativeandroid.widget.ACTION_REFRESH" + const val EXTRA_PRESET_ID = "ca.cgagnier.wlednativeandroid.widget.EXTRA_PRESET_ID" + const val EXTRA_DEVICE_ADDRESS = "ca.cgagnier.wlednativeandroid.widget.EXTRA_DEVICE_ADDRESS" + const val EXTRA_LIST_LIMIT = "ca.cgagnier.wlednativeandroid.widget.EXTRA_LIST_LIMIT" + const val EXTRA_LAYOUT_ID = "ca.cgagnier.wlednativeandroid.widget.EXTRA_LAYOUT_ID" + const val EXTRA_LIST_ID = "ca.cgagnier.wlednativeandroid.widget.EXTRA_LIST_ID" + private const val TAG = "PresetWidgetProvider" + + fun updateAppWidget( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + limit: Int = 0, + layoutId: Int = R.layout.widget_preset, + listId: Int = R.id.preset_list, + itemLayoutId: Int = R.layout.widget_preset_item + ) { + val deviceName = PresetWidgetConfigureActivity.loadDeviceName(context, appWidgetId) + val deviceAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + + if (deviceAddress == null) { + return + } + + // Construct the RemoteViews object + val views = RemoteViews(context.packageName, layoutId) + views.setTextViewText(R.id.appwidget_text, deviceName) + + // Set up the collection + val intent = Intent(context, PresetWidgetService::class.java).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + putExtra(EXTRA_LIST_LIMIT, limit) + putExtra(EXTRA_LAYOUT_ID, itemLayoutId) + data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) + } + views.setRemoteAdapter(listId, intent) + views.setEmptyView(listId, R.id.empty_view) + + val refreshIntent = Intent(context, PresetWidgetProvider::class.java).apply { + action = ACTION_REFRESH + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + putExtra(EXTRA_LIST_ID, listId) + data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) + } + val refreshPendingIntent = PendingIntent.getBroadcast( + context, appWidgetId, refreshIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + views.setOnClickPendingIntent(R.id.empty_view, refreshPendingIntent) + + // Set up pending intent template for items + val toastIntent = Intent(context, PresetWidgetProvider::class.java).apply { + action = ACTION_TRIGGER_PRESET + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + putExtra(EXTRA_LIST_ID, listId) + data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) + } + val toastPendingIntent = PendingIntent.getBroadcast( + context, 0, toastIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + views.setPendingIntentTemplate(listId, toastPendingIntent) + + appWidgetManager.updateAppWidget(appWidgetId, views) + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, listId) + } + } +} diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt new file mode 100644 index 00000000..2455456c --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/PresetWidgetService.kt @@ -0,0 +1,223 @@ +package ca.cgagnier.wlednativeandroid.widget + +import android.content.Context +import android.content.Intent +import android.util.Log +import android.widget.RemoteViews +import android.widget.RemoteViewsService +import ca.cgagnier.wlednativeandroid.R +import ca.cgagnier.wlednativeandroid.model.wledapi.Preset +import ca.cgagnier.wlednativeandroid.service.api.DeviceApiFactory +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import kotlinx.coroutines.runBlocking + +class PresetWidgetService : RemoteViewsService() { + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { + return PresetWidgetRemoteViewsFactory(this.applicationContext, intent) + } +} + +class PresetWidgetRemoteViewsFactory( + private val context: Context, + intent: Intent +) : RemoteViewsService.RemoteViewsFactory { + + private val appWidgetId: Int = intent.getIntExtra( + android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID, + android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID + ) + private val limit: Int = intent.getIntExtra(PresetWidgetProvider.EXTRA_LIST_LIMIT, 0) + private val itemLayoutId: Int = intent.getIntExtra(PresetWidgetProvider.EXTRA_LAYOUT_ID, R.layout.widget_preset_item) + private var presets: List> = emptyList() + private var deviceAddress: String? = null + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface PresetWidgetServiceEntryPoint { + fun deviceApiFactory(): DeviceApiFactory + fun moshi(): Moshi + } + + override fun onCreate() { + // Data loading should ideally happen in onDataSetChanged + } + + private var selectedPresetId: Int = -1 + + override fun onDataSetChanged() { + deviceAddress = PresetWidgetConfigureActivity.loadDeviceAddress(context, appWidgetId) + if (deviceAddress == null) { + Log.e(TAG, "Device address is null") + return + } + + val entryPoint = EntryPointAccessors.fromApplication( + context, + PresetWidgetServiceEntryPoint::class.java + ) + val deviceApiFactory = entryPoint.deviceApiFactory() + val moshi = entryPoint.moshi() + + runBlocking { + try { + Log.d(TAG, "Fetching presets for $deviceAddress") + val api = deviceApiFactory.create(deviceAddress!!) + + // Fetch state to know selected preset + val stateResponse = api.getState() + if (stateResponse.isSuccessful) { + val state = stateResponse.body() + if (state != null) { + selectedPresetId = state.selectedPresetId ?: -1 + if (selectedPresetId == -1 && state.selectedPlaylistId != null && state.selectedPlaylistId > 0) { + selectedPresetId = state.selectedPlaylistId + } + } + } + + // Fetch Presets + val response = api.getPresets() + if (response.isSuccessful) { + val presetsMap = response.body() + Log.d(TAG, "Presets fetched: ${presetsMap?.size}") + if (presetsMap != null) { + savePresetsToCache(presetsMap, moshi) + processPresets(presetsMap) + } + } else { + Log.e(TAG, "Error fetching presets: ${response.code()}") + loadPresetsFromCache(moshi) + } + } catch (e: Exception) { + Log.e(TAG, "Error fetching presets", e) + loadPresetsFromCache(moshi) + } + } + } + + private fun processPresets(presetsMap: Map) { + var sortedPresets = presetsMap.entries + .filter { it.key != "0" } + .map { it.key to it.value } + .sortedBy { it.first.toIntOrNull() ?: Int.MAX_VALUE } + + if (limit > 0) { + sortedPresets = sortedPresets.take(limit) + } + presets = sortedPresets + } + + private fun savePresetsToCache(presetsMap: Map, moshi: Moshi) { + try { + val type = Types.newParameterizedType(Map::class.java, String::class.java, Preset::class.java) + val adapter = moshi.adapter>(type) + val json = adapter.toJson(presetsMap) + + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit() + prefs.putString(PREF_CACHE_PRESETS + appWidgetId, json) + prefs.putInt(PREF_CACHE_SELECTED_ID + appWidgetId, selectedPresetId) + prefs.apply() + } catch (e: Exception) { + Log.e(TAG, "Failed to save presets to cache", e) + } + } + + private fun loadPresetsFromCache(moshi: Moshi) { + try { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val json = prefs.getString(PREF_CACHE_PRESETS + appWidgetId, null) + selectedPresetId = prefs.getInt(PREF_CACHE_SELECTED_ID + appWidgetId, -1) + + if (json != null) { + val type = Types.newParameterizedType(Map::class.java, String::class.java, Preset::class.java) + val adapter = moshi.adapter>(type) + val presetsMap = adapter.fromJson(json) + if (presetsMap != null) { + Log.d(TAG, "Loaded presets from cache for $appWidgetId") + processPresets(presetsMap) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to load presets from cache", e) + } + } + + // ... (rest of the file) + + + override fun onDestroy() { + presets = emptyList() + } + + override fun getCount(): Int { + return presets.size + } + + + + // ... (rest of class) + + override fun getViewAt(position: Int): RemoteViews { + if (position >= presets.size) return RemoteViews(context.packageName, itemLayoutId) + + val (id, preset) = presets[position] + val views = RemoteViews(context.packageName, itemLayoutId) + + var name = preset.name + if (name.isEmpty()) { + name = "Preset $id" + } + + views.setTextViewText(R.id.preset_name, name) + + if (id.toIntOrNull() == selectedPresetId) { + if (itemLayoutId == R.layout.widget_preset_button_item) { + views.setInt(R.id.widget_item, "setBackgroundResource", R.drawable.widget_button_selected) + } else { + views.setViewVisibility(R.id.preset_indicator, android.view.View.VISIBLE) + } + } else { + if (itemLayoutId == R.layout.widget_preset_button_item) { + views.setInt(R.id.widget_item, "setBackgroundResource", R.drawable.widget_background) + } else { + views.setViewVisibility(R.id.preset_indicator, android.view.View.GONE) + } + } + + val fillInIntent = Intent().apply { + putExtra(PresetWidgetProvider.EXTRA_PRESET_ID, id.toIntOrNull() ?: -1) + putExtra(PresetWidgetProvider.EXTRA_DEVICE_ADDRESS, deviceAddress) + } + views.setOnClickFillInIntent(R.id.widget_item, fillInIntent) + + return views + } + + override fun getLoadingView(): RemoteViews? { + return null + } + + override fun getViewTypeCount(): Int { + return 1 + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun hasStableIds(): Boolean { + return true + } + + companion object { + private const val TAG = "PresetWidgetService" + private const val PREFS_NAME = "ca.cgagnier.wlednativeandroid.widget.PresetWidgetCache" + private const val PREF_CACHE_PRESETS = "cache_presets_" + private const val PREF_CACHE_SELECTED_ID = "cache_selected_id_" + } +} diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt new file mode 100644 index 00000000..c95979cb --- /dev/null +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/SmallPresetWidgetProvider.kt @@ -0,0 +1,27 @@ +package ca.cgagnier.wlednativeandroid.widget + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import ca.cgagnier.wlednativeandroid.R + +class SmallPresetWidgetProvider : AppWidgetProvider() { + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + for (appWidgetId in appWidgetIds) { + PresetWidgetProvider.updateAppWidget( + context, + appWidgetManager, + appWidgetId, + limit = 3, + layoutId = R.layout.widget_preset_horizontal, + listId = R.id.preset_grid, + itemLayoutId = R.layout.widget_preset_button_item + ) + } + } +} diff --git a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetConfigureActivity.kt b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetConfigureActivity.kt index 6374a0ea..2b495586 100644 --- a/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetConfigureActivity.kt +++ b/app/src/main/java/ca/cgagnier/wlednativeandroid/widget/WledWidgetConfigureActivity.kt @@ -114,7 +114,7 @@ class WledWidgetConfigureActivity : ComponentActivity() { // TODO: Style the widget selection screen better so it looks more like the rest of the WLED app. @Composable -fun ConfigurationScreen(devices: List, onDeviceSelected: (Device) -> Unit) { +private fun ConfigurationScreen(devices: List, onDeviceSelected: (Device) -> Unit) { Scaffold( topBar = { TopAppBar( @@ -138,7 +138,7 @@ fun ConfigurationScreen(devices: List, onDeviceSelected: (Device) -> Uni } @Composable -fun DeviceItem(device: Device, onClick: () -> Unit) { +private fun DeviceItem(device: Device, onClick: () -> Unit) { Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/res/drawable/ic_dot_indicator.xml b/app/src/main/res/drawable/ic_dot_indicator.xml new file mode 100644 index 00000000..136435fa --- /dev/null +++ b/app/src/main/res/drawable/ic_dot_indicator.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/drawable/widget_background.xml b/app/src/main/res/drawable/widget_background.xml new file mode 100644 index 00000000..8fce07ac --- /dev/null +++ b/app/src/main/res/drawable/widget_background.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/drawable/widget_button_selected.xml b/app/src/main/res/drawable/widget_button_selected.xml new file mode 100644 index 00000000..2c223c3a --- /dev/null +++ b/app/src/main/res/drawable/widget_button_selected.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/drawable/widget_preset_small_preview.xml b/app/src/main/res/drawable/widget_preset_small_preview.xml new file mode 100644 index 00000000..cfb04639 --- /dev/null +++ b/app/src/main/res/drawable/widget_preset_small_preview.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/widget_preview.xml b/app/src/main/res/drawable/widget_preview.xml new file mode 100644 index 00000000..ed550e79 --- /dev/null +++ b/app/src/main/res/drawable/widget_preview.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/layout/widget_preset.xml b/app/src/main/res/layout/widget_preset.xml new file mode 100644 index 00000000..34f20e11 --- /dev/null +++ b/app/src/main/res/layout/widget_preset.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/widget_preset_button_item.xml b/app/src/main/res/layout/widget_preset_button_item.xml new file mode 100644 index 00000000..8520bca2 --- /dev/null +++ b/app/src/main/res/layout/widget_preset_button_item.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/layout/widget_preset_horizontal.xml b/app/src/main/res/layout/widget_preset_horizontal.xml new file mode 100644 index 00000000..75247a7e --- /dev/null +++ b/app/src/main/res/layout/widget_preset_horizontal.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/app/src/main/res/layout/widget_preset_item.xml b/app/src/main/res/layout/widget_preset_item.xml new file mode 100644 index 00000000..6e253da8 --- /dev/null +++ b/app/src/main/res/layout/widget_preset_item.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..1aad025b --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ + + 8dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 779e2c79..12ead593 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -116,6 +116,8 @@ Open Dismiss Some of your devices are hidden + Presets + No presets found Single Device Control a single WLED device diff --git a/app/src/main/res/xml/widget_preset_info.xml b/app/src/main/res/xml/widget_preset_info.xml new file mode 100644 index 00000000..17fbfa41 --- /dev/null +++ b/app/src/main/res/xml/widget_preset_info.xml @@ -0,0 +1,10 @@ + + diff --git a/app/src/main/res/xml/widget_preset_small_info.xml b/app/src/main/res/xml/widget_preset_small_info.xml new file mode 100644 index 00000000..718522f4 --- /dev/null +++ b/app/src/main/res/xml/widget_preset_small_info.xml @@ -0,0 +1,12 @@ + +