Skip to content
Open
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
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,12 @@
android:label="@string/diy_editor_new_title"
android:theme="@style/Theme.Essentials" />

<activity
android:name=".ui.activities.MacroEditorActivity"
android:exported="false"
android:label="@string/diy_new_macro"
android:theme="@style/Theme.Essentials" />


<service
android:name=".services.LocationReachedService"
Expand Down
13 changes: 12 additions & 1 deletion app/src/main/java/com/sameerasw/essentials/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ class MainActivity : AppCompatActivity() {

var showGitHubAuthSheet by remember { mutableStateOf(false) }
var showNewAutomationSheet by remember { mutableStateOf(false) }
var isDiyMacrosShown by remember { mutableStateOf(false) }
val gitHubToken by viewModel.gitHubToken
val gitHubUser by gitHubAuthViewModel.currentUser
val isOnboardingCompleted by viewModel.isOnboardingCompleted
Expand Down Expand Up @@ -631,7 +632,14 @@ class MainActivity : AppCompatActivity() {
DIYTabs.DIY -> {
Icon(
painter = painterResource(id = R.drawable.rounded_add_24),
contentDescription = stringResource(R.string.diy_editor_new_title),
contentDescription =
stringResource(
if (isDiyMacrosShown) {
R.string.diy_new_macro
} else {
R.string.diy_editor_new_title
},
),
)
}
}
Expand Down Expand Up @@ -908,6 +916,9 @@ class MainActivity : AppCompatActivity() {
onNewAutomationClick = {
showNewAutomationSheet = true
},
onShowMacrosChange = {
isDiyMacrosShown = it
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,12 @@ class SettingsRepository(
const val KEY_ISLAND_SHOW_CONSCIOUS_GATE = "island_show_conscious_gate"
const val KEY_ISLAND_SHOW_TIME_BATTERY = "island_show_time_battery"
const val KEY_ISLAND_TIME_SHOW_SECONDS = "island_time_show_seconds"
const val KEY_ISLAND_THEMED_TEXT = "island_themed_text"
const val KEY_ISLAND_ICON_COLOR_MODE = "island_icon_color_mode"
const val KEY_ISLAND_ICON_COLOR_CUSTOM = "island_icon_color_custom"
const val KEY_ISLAND_ICON_MATERIAL_YOU = "island_icon_material_you"
const val ISLAND_ICON_COLOR_DYNAMIC = "dynamic"
const val ISLAND_ICON_COLOR_CUSTOM = "custom"
const val KEY_ISLAND_TIME_FORMAT = "island_time_format"
const val KEY_ISLAND_TIME_PERIOD = "island_time_period"
const val KEY_ISLAND_TIME_LEADING_ZERO = "island_time_leading_zero"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ sealed interface Action {
override val isConfigurable: Boolean get() = true
}

@Keep
data class RunMacro(
@SerializedName("macroId") val macroId: String = "",
) : Action {
override val title: Int get() = R.string.diy_action_run_macro
override val icon: Int get() = R.drawable.rounded_bolt_24
}

@Keep
data object TurnOffLowPower : Action {
override val title: Int = R.string.diy_action_low_power_off
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,10 @@ object ActionRegistry {
),
),
) + getCategories(sdkInt, screenOnOnly = true)

// Saved macros as RunMacro actions, or null when there are none
fun getMacroCategory(): ActionCategory? =
DIYRepository.macros.value
.takeIf { it.isNotEmpty() }
?.let { macros -> ActionCategory(R.string.diy_category_macros, macros.map { Action.RunMacro(it.id) }) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,22 @@ import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlin.reflect.KClass

object DIYRepository {
private const val PREFS_NAME = "diy_automations_prefs"
private const val KEY_AUTOMATIONS = "saved_automations"
private const val KEY_MACROS = "saved_macros"

private val _automations = MutableStateFlow<List<Automation>>(emptyList())
val automations = _automations.asStateFlow()

private val _macros = MutableStateFlow<List<Macro>>(emptyList())
val macros = _macros.asStateFlow()

private var prefs: SharedPreferences? = null
private val gson =
GsonBuilder()
Expand Down Expand Up @@ -80,6 +85,7 @@ object DIYRepository {
if (prefs != null) return
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
reloadAutomations()
reloadMacros()
}

fun reloadAutomations() {
Expand Down Expand Up @@ -133,6 +139,56 @@ object DIYRepository {

fun getAutomation(id: String): Automation? = _automations.value.find { it.id == id }

fun reloadMacros() {
val json = prefs?.getString(KEY_MACROS, null)
_macros.value = if (json != null) macrosFromJson(json) else emptyList()
}

private fun saveMacrosToPrefs() {
val json = macrosToJson(_macros.value)
prefs?.edit()?.putString(KEY_MACROS, json)?.apply()
}

fun addMacro(macro: Macro) {
val current = _macros.value.toMutableList()
current.add(macro)
_macros.value = current
saveMacrosToPrefs()
}

fun updateMacro(macro: Macro) {
val current = _macros.value.toMutableList()
val index = current.indexOfFirst { it.id == macro.id }
if (index != -1) {
current[index] = macro
_macros.value = current
saveMacrosToPrefs()
}
}

fun removeMacro(id: String) {
val current = _macros.value.toMutableList()
current.removeAll { it.id == id }
_macros.value = current
saveMacrosToPrefs()
}

fun getMacro(id: String): Macro? = _macros.value.find { it.id == id }

internal fun macrosToJson(macros: List<Macro>): String = gson.toJson(macros)

internal fun macrosFromJson(json: String): List<Macro> =
try {
val type = object : TypeToken<List<Macro>>() {}.type
val list: List<Macro> = gson.fromJson(json, type) ?: emptyList()
// Gson puts nulls in the list for unknown types despite the non-null type
list.filterNotNull().map { macro ->
macro.copy(actions = macro.actionList.filterNotNull())
}
} catch (_: Exception) {
emptyList()
}

private fun updateActionShortcutLauncherState() {
val context = appContext ?: return
val showLauncher =
Expand Down
37 changes: 37 additions & 0 deletions app/src/main/java/com/sameerasw/essentials/domain/diy/Macro.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2026 sameerasw.com
* License: MIT License
*
* Feature Module: Domain Layer Models & Registries
* File: Macro.kt
* Description: Domain model and business logic entry for Macro.kt.
*/

package com.sameerasw.essentials.domain.diy

import android.content.Context
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import com.sameerasw.essentials.R

@Keep
data class Macro(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("actions") val actions: List<Action> = emptyList(),
) {
// Gson leaves missing lists null despite the default
@Suppress("USELESS_ELVIS")
val actionList: List<Action> get() = actions ?: emptyList()
}

// RunMacro shows the macro's name; every other action shows its title
fun Action.displayName(context: Context): String =
if (this is Action.RunMacro) {
DIYRepository.getMacro(macroId)?.name ?: context.getString(R.string.diy_macro_deleted_label)
} else {
context.getString(title)
}

// Macros match only the same macro; other actions match by type so their saved settings carry over
fun Action.matches(other: Action?): Boolean = if (this is Action.RunMacro) this == other else other != null && this::class == other::class
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import com.sameerasw.essentials.island.model.IslandItem
import com.sameerasw.essentials.island.model.IslandPriority
import com.sameerasw.essentials.island.plugins.BaseIslandPlugin
import com.sameerasw.essentials.island.ui.IslandTextStyles
import com.sameerasw.essentials.island.ui.islandTextColor
import com.sameerasw.essentials.island.ui.components.BatteryGlyph
import com.sameerasw.essentials.island.ui.components.BatteryRing
import com.sameerasw.essentials.island.ui.components.CameraRow
Expand Down Expand Up @@ -584,7 +585,7 @@ private fun BriefEventRow(event: UpcomingCalendarEvent, now: Long, accent: Color
Column(Modifier.weight(1f)) {
Text(
text = event.title,
style = IslandTextStyles.body.copy(color = Color.White, fontSize = 15.sp),
style = IslandTextStyles.body.copy(color = islandTextColor(), fontSize = 15.sp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Expand Down Expand Up @@ -628,7 +629,7 @@ private fun BriefPlayer(media: MediaSnapshot, artwork: ImageBitmap?, onClick: ()
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) {
MarqueeText(text = media.title, style = IslandTextStyles.body.copy(color = Color.White, fontSize = 15.sp))
MarqueeText(text = media.title, style = IslandTextStyles.body.copy(color = islandTextColor(), fontSize = 15.sp))
MarqueeText(text = media.artist, style = IslandTextStyles.body)
}
media.actions.like?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.sameerasw.essentials.R
import com.sameerasw.essentials.island.model.IslandExpandedScope
import com.sameerasw.essentials.island.plugins.soften
import com.sameerasw.essentials.island.ui.IslandTextStyles
import com.sameerasw.essentials.island.ui.islandTextColor
import com.sameerasw.essentials.island.ui.components.CameraRow
import com.sameerasw.essentials.island.ui.components.ConnectedButtonRow
import com.sameerasw.essentials.island.ui.components.ConnectedItem
Expand Down Expand Up @@ -68,7 +69,7 @@ fun CalendarExpanded(
"${timeFormat.format(Date(event.startTimeMillis))} – ${timeFormat.format(Date(event.endTimeMillis))}"
else -> timeFormat.format(Date(event.startTimeMillis))
}
val detailStyle = IslandTextStyles.body.copy(color = Color.White, fontSize = 15.sp, lineHeight = 20.sp)
val detailStyle = IslandTextStyles.body.copy(color = islandTextColor(), fontSize = 15.sp, lineHeight = 20.sp)

Box(propagateMinConstraints = true) {
if (drawBackground && !SurfaceBackdrop { Box(Modifier.fillMaxSize().accentGlow(calendarColor, showGlow, scope.cameraClearance)) }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.sameerasw.essentials.domain.model.NavigationNotificationData
import com.sameerasw.essentials.island.model.IslandExpandedScope
import com.sameerasw.essentials.island.ui.IslandLayoutSpec
import com.sameerasw.essentials.island.ui.IslandTextStyles
import com.sameerasw.essentials.island.ui.islandTextColor
import com.sameerasw.essentials.island.ui.components.CameraRow
import com.sameerasw.essentials.island.ui.components.ConnectedButtonRow
import com.sameerasw.essentials.island.ui.components.ConnectedItem
Expand Down Expand Up @@ -53,7 +54,7 @@ fun NavigationExpanded(
Spacer(Modifier.height(8.dp))
Text(
text = data.instruction,
style = IslandTextStyles.title.copy(color = Color.White, fontSize = 20.sp, lineHeight = 25.sp),
style = IslandTextStyles.title.copy(color = islandTextColor(), fontSize = 20.sp, lineHeight = 25.sp),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth().padding(horizontal = sidePadding),
Expand All @@ -63,7 +64,7 @@ fun NavigationExpanded(
Spacer(Modifier.height(2.dp))
Text(
text = data.eta,
style = IslandTextStyles.body.copy(color = Color.White.copy(alpha = 0.7f)),
style = IslandTextStyles.body.copy(color = islandTextColor(0.7f)),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth().padding(horizontal = sidePadding),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import com.sameerasw.essentials.domain.model.ActiveNotificationAlert
import com.sameerasw.essentials.domain.model.NotificationActionItem
import com.sameerasw.essentials.island.model.IslandExpandedScope
import com.sameerasw.essentials.island.ui.IslandTextStyles
import com.sameerasw.essentials.island.ui.islandTextColor
import com.sameerasw.essentials.island.ui.components.CameraRow
import com.sameerasw.essentials.island.ui.components.ConnectedButtonRow
import com.sameerasw.essentials.island.ui.components.ConnectedItem
Expand Down Expand Up @@ -111,7 +112,7 @@ fun NotificationExpanded(
Spacer(Modifier.height(4.dp))
Text(
text = message,
style = IslandTextStyles.body.copy(color = Color.White, fontSize = 15.sp, lineHeight = 20.sp),
style = IslandTextStyles.body.copy(color = islandTextColor(), fontSize = 15.sp, lineHeight = 20.sp),
maxLines = 7,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth().padding(horizontal = sidePadding),
Expand Down Expand Up @@ -205,7 +206,7 @@ private fun ReplyField(
scope.keepAlive()
},
singleLine = true,
textStyle = IslandTextStyles.body.copy(color = Color.White, fontSize = 15.sp),
textStyle = IslandTextStyles.body.copy(color = islandTextColor(), fontSize = 15.sp),
cursorBrush = SolidColor(accent),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send, capitalization = KeyboardCapitalization.Sentences),
keyboardActions = KeyboardActions(onSend = { send() }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.ui.unit.sp
import com.sameerasw.essentials.R
import com.sameerasw.essentials.island.model.IslandExpandedScope
import com.sameerasw.essentials.island.ui.IslandTextStyles
import com.sameerasw.essentials.island.ui.islandTextColor
import com.sameerasw.essentials.island.ui.components.CameraRow
import com.sameerasw.essentials.island.ui.components.ConnectedButtonRow
import com.sameerasw.essentials.island.ui.components.ConnectedItem
Expand Down Expand Up @@ -51,14 +52,14 @@ fun ProgressExpanded(
MarqueeText(text = title, style = IslandTextStyles.title, modifier = Modifier.weight(1f))
},
end = {
if (!indeterminate) Text("${(progress * 100).toInt()}%", style = IslandTextStyles.body.copy(color = Color.White))
if (!indeterminate) Text("${(progress * 100).toInt()}%", style = IslandTextStyles.body.copy(color = islandTextColor()))
},
)
if (!text.isNullOrBlank()) {
Spacer(Modifier.height(4.dp))
Text(
text = text,
style = IslandTextStyles.body.copy(color = Color.White, fontSize = 15.sp, lineHeight = 20.sp),
style = IslandTextStyles.body.copy(color = islandTextColor(), fontSize = 15.sp, lineHeight = 20.sp),
maxLines = 3,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth().padding(horizontal = sidePadding),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sameerasw.essentials.island.ui.IslandTextStyles
import com.sameerasw.essentials.island.ui.components.resolveIconTint

private const val LABEL_SHIFT_X = 0.15f
private const val LABEL_SHIFT_Y = 0.15f
Expand Down Expand Up @@ -64,7 +65,8 @@ fun SignalBars(level: Int, color: Color, label: String?, modifier: Modifier = Mo
}

@Composable
fun SignalIndicator(wifiLevel: Int?, mobileLevel: Int, color: Color, label: String?, modifier: Modifier = Modifier) {
fun SignalIndicator(wifiLevel: Int?, mobileLevel: Int, rawColor: Color, label: String?, modifier: Modifier = Modifier) {
val color = resolveIconTint(rawColor)
AnimatedContent(
targetState = wifiLevel,
transitionSpec = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.sameerasw.essentials.island.model.IslandPriority
import com.sameerasw.essentials.island.model.LineContent
import com.sameerasw.essentials.island.plugins.BaseIslandPlugin
import com.sameerasw.essentials.island.ui.IslandTextStyles
import com.sameerasw.essentials.island.ui.islandTextColor
import com.sameerasw.essentials.island.ui.components.CameraRow
import com.sameerasw.essentials.island.ui.components.ConnectedButtonRow
import com.sameerasw.essentials.island.ui.components.ConnectedItem
Expand Down Expand Up @@ -148,7 +149,7 @@ class TravelPlugin : BaseIslandPlugin() {
Spacer(Modifier.height(4.dp))
Text(
text = trip.time,
style = IslandTextStyles.body.copy(color = Color.White, fontSize = 15.sp, lineHeight = 20.sp),
style = IslandTextStyles.body.copy(color = islandTextColor(), fontSize = 15.sp, lineHeight = 20.sp),
modifier = Modifier.fillMaxWidth().padding(horizontal = sidePadding),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sameerasw.essentials.island.ui.IslandTextStyles
import com.sameerasw.essentials.island.ui.islandTextColor
import com.sameerasw.island.sdk.BriefCardPayload
import com.sameerasw.island.sdk.IslandSurfaceKey
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -66,7 +67,7 @@ fun RemoteBriefCardRow(card: RemoteBriefCard, onClick: () -> Unit, modifier: Mod
Spacer(Modifier.width(12.dp))
Text(
text = payload.subtitle.orEmpty(),
style = IslandTextStyles.body.copy(color = Color.White, fontSize = 14.sp),
style = IslandTextStyles.body.copy(color = islandTextColor(), fontSize = 14.sp),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
Expand Down
Loading
Loading