mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
feat(widget): Add ANC control widget
New home-screen widget that toggles AirPods ANC modes (Off / ANC / Transparency / Adaptive) directly, with six adaptive layouts that pick based on widget size (QUAD_CORNERS, ROW_ICONS, COLUMN_ICONS, GRID_2X2, ROW, COLUMN). Consolidated battery+ANC configuration into a single ViewModel that detects widget type from AppWidgetManager. ACTIVE state uses Material3 secondaryContainer/onSecondaryContainer for guaranteed contrast. Includes live config preview, preview subtitle, stale-selection guard, device-label toggle, and aligned icons with the app's AncModeSelector.
This commit is contained in:
@@ -101,6 +101,17 @@
|
|||||||
android:resource="@xml/battery_widget_info" />
|
android:resource="@xml/battery_widget_info" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".main.ui.widget.AncWidgetProvider"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/anc_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package eu.darken.capod.main.ui.widget
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.glance.GlanceId
|
||||||
|
import androidx.glance.LocalSize
|
||||||
|
import androidx.glance.appwidget.GlanceAppWidget
|
||||||
|
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||||
|
import androidx.glance.appwidget.SizeMode
|
||||||
|
import androidx.glance.appwidget.provideContent
|
||||||
|
import dagger.hilt.EntryPoint
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
|
import eu.darken.capod.common.debug.logging.asLog
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||||
|
import eu.darken.capod.common.upgrade.isPro
|
||||||
|
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||||
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
|
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||||
|
|
||||||
|
class AncGlanceWidget : GlanceAppWidget() {
|
||||||
|
|
||||||
|
override val sizeMode = SizeMode.Exact
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@EntryPoint
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface AncWidgetEntryPoint {
|
||||||
|
fun deviceMonitor(): DeviceMonitor
|
||||||
|
fun upgradeRepo(): UpgradeRepo
|
||||||
|
fun widgetSettings(): WidgetSettings
|
||||||
|
fun deviceProfilesRepo(): DeviceProfilesRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||||
|
val ep: AncWidgetEntryPoint
|
||||||
|
val appWidgetId: Int
|
||||||
|
val initialIsPro: Boolean
|
||||||
|
val initialProfileId: String?
|
||||||
|
val cachedDevice: PodDevice?
|
||||||
|
|
||||||
|
try {
|
||||||
|
ep = EntryPointAccessors.fromApplication(context, AncWidgetEntryPoint::class.java)
|
||||||
|
appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id)
|
||||||
|
log(TAG, VERBOSE) { "provideGlance(appWidgetId=$appWidgetId)" }
|
||||||
|
initialIsPro = ep.upgradeRepo().isPro()
|
||||||
|
initialProfileId = ep.widgetSettings().getWidgetProfile(appWidgetId)
|
||||||
|
cachedDevice = initialProfileId?.let { ep.deviceMonitor().getDeviceForProfile(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "provideGlance setup failed: ${e.asLog()}" }
|
||||||
|
provideContent {
|
||||||
|
GlanceAncWidgetContent(
|
||||||
|
state = AncWidgetRenderState.Message(
|
||||||
|
theme = WidgetTheme.DEFAULT,
|
||||||
|
resolvedBgColor = WidgetRenderStateMapper.resolvedBgColor(context, WidgetTheme.DEFAULT),
|
||||||
|
resolvedTextColor = WidgetRenderStateMapper.resolvedTextColor(context, WidgetTheme.DEFAULT),
|
||||||
|
resolvedIconColor = WidgetRenderStateMapper.resolvedIconColor(context, WidgetTheme.DEFAULT),
|
||||||
|
primaryText = context.getString(eu.darken.capod.R.string.widget_error_loading_label),
|
||||||
|
),
|
||||||
|
context = context,
|
||||||
|
appWidgetId = appWidgetId(context, id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
provideContent {
|
||||||
|
val devices by ep.deviceMonitor().devices.collectAsState(initial = emptyList())
|
||||||
|
val profiles by ep.deviceProfilesRepo().profiles.collectAsState(initial = emptyList())
|
||||||
|
val upgradeInfo by ep.upgradeRepo().upgradeInfo.collectAsState(initial = null)
|
||||||
|
val widthDp = LocalSize.current.width
|
||||||
|
val heightDp = LocalSize.current.height
|
||||||
|
|
||||||
|
val state = try {
|
||||||
|
val profileId = ep.widgetSettings().getWidgetProfile(appWidgetId)
|
||||||
|
val theme = WidgetTheme.fromBundle(
|
||||||
|
AppWidgetManager.getInstance(context).getAppWidgetOptions(appWidgetId)
|
||||||
|
)
|
||||||
|
|
||||||
|
val isPro = upgradeInfo?.isPro ?: initialIsPro
|
||||||
|
|
||||||
|
val liveDevice = devices.firstOrNull { it.profileId == profileId }
|
||||||
|
val device = liveDevice ?: cachedDevice?.takeIf { it.profileId == profileId }
|
||||||
|
|
||||||
|
val profileLabel = profileId?.let { pid ->
|
||||||
|
profiles.firstOrNull { it.id == pid }?.label
|
||||||
|
}
|
||||||
|
|
||||||
|
val widthCells = getCellsForSize(widthDp.value.toInt())
|
||||||
|
val heightCells = getCellsForSize(heightDp.value.toInt())
|
||||||
|
val layout = when {
|
||||||
|
widthCells <= 1 && heightCells <= 1 -> AncLayout.QUAD_CORNERS
|
||||||
|
heightCells <= 1 -> AncLayout.ROW_ICONS
|
||||||
|
widthCells <= 1 -> AncLayout.COLUMN_ICONS
|
||||||
|
widthCells > 2 -> AncLayout.ROW
|
||||||
|
heightCells > 3 -> AncLayout.COLUMN
|
||||||
|
else -> AncLayout.GRID_2X2
|
||||||
|
}
|
||||||
|
|
||||||
|
AncWidgetRenderStateMapper.map(
|
||||||
|
context = context,
|
||||||
|
device = device,
|
||||||
|
theme = theme,
|
||||||
|
isPro = isPro,
|
||||||
|
hasConfiguredProfile = profileId != null,
|
||||||
|
profileLabel = profileLabel,
|
||||||
|
layout = layout,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "provideGlance failed: ${e.asLog()}" }
|
||||||
|
AncWidgetRenderState.Message(
|
||||||
|
theme = WidgetTheme.DEFAULT,
|
||||||
|
resolvedBgColor = WidgetRenderStateMapper.resolvedBgColor(context, WidgetTheme.DEFAULT),
|
||||||
|
resolvedTextColor = WidgetRenderStateMapper.resolvedTextColor(context, WidgetTheme.DEFAULT),
|
||||||
|
resolvedIconColor = WidgetRenderStateMapper.resolvedIconColor(context, WidgetTheme.DEFAULT),
|
||||||
|
primaryText = context.getString(eu.darken.capod.R.string.widget_error_loading_label),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GlanceAncWidgetContent(state = state, context = context, appWidgetId = appWidgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun providePreview(context: Context, widgetCategory: Int) {
|
||||||
|
val previewState = AncWidgetRenderState.previewActive(
|
||||||
|
context = context,
|
||||||
|
bgColor = WidgetRenderStateMapper.resolvedBgColor(context, WidgetTheme.DEFAULT),
|
||||||
|
textColor = WidgetRenderStateMapper.resolvedTextColor(context, WidgetTheme.DEFAULT),
|
||||||
|
iconColor = WidgetRenderStateMapper.resolvedIconColor(context, WidgetTheme.DEFAULT),
|
||||||
|
activeColor = WidgetRenderStateMapper.resolveThemeColor(context, com.google.android.material.R.attr.colorSecondaryContainer),
|
||||||
|
onActiveColor = WidgetRenderStateMapper.resolveThemeColor(context, com.google.android.material.R.attr.colorOnSecondaryContainer),
|
||||||
|
)
|
||||||
|
|
||||||
|
provideContent {
|
||||||
|
GlanceAncWidgetContent(
|
||||||
|
state = previewState,
|
||||||
|
context = context,
|
||||||
|
appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appWidgetId(context: Context, id: GlanceId): Int {
|
||||||
|
return try {
|
||||||
|
GlanceAppWidgetManager(context).getAppWidgetId(id)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
AppWidgetManager.INVALID_APPWIDGET_ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCellsForSize(size: Int): Int {
|
||||||
|
var n = 2
|
||||||
|
while (70 * n - 30 < size) {
|
||||||
|
++n
|
||||||
|
}
|
||||||
|
return n - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = logTag("Widget", "ANC", "Glance")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package eu.darken.capod.main.ui.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.glance.GlanceId
|
||||||
|
import androidx.glance.action.ActionParameters
|
||||||
|
import androidx.glance.appwidget.action.ActionCallback
|
||||||
|
import androidx.glance.appwidget.updateAll
|
||||||
|
import dagger.hilt.EntryPoint
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
|
import eu.darken.capod.common.debug.logging.asLog
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||||
|
import eu.darken.capod.pods.core.apple.aap.AapConnectionManager
|
||||||
|
import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
|
||||||
|
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
class AncModeActionCallback : ActionCallback {
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@EntryPoint
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface AncModeCallbackEntryPoint {
|
||||||
|
fun aapConnectionManager(): AapConnectionManager
|
||||||
|
fun widgetSettings(): WidgetSettings
|
||||||
|
fun deviceMonitor(): DeviceMonitor
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||||
|
val modeName = parameters[ANC_MODE_KEY] ?: run {
|
||||||
|
log(TAG, ERROR) { "onAction: missing ANC_MODE_KEY" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val widgetId = parameters[WIDGET_ID_KEY] ?: run {
|
||||||
|
log(TAG, ERROR) { "onAction: missing WIDGET_ID_KEY" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log(TAG, VERBOSE) { "onAction(mode=$modeName, widgetId=$widgetId)" }
|
||||||
|
|
||||||
|
val mode = try {
|
||||||
|
enumValueOf<AapSetting.AncMode.Value>(modeName)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
log(TAG, ERROR) { "onAction: invalid mode name: $modeName" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val ep = EntryPointAccessors.fromApplication(context, AncModeCallbackEntryPoint::class.java)
|
||||||
|
|
||||||
|
val profileId = ep.widgetSettings().getWidgetProfile(widgetId)
|
||||||
|
if (profileId == null) {
|
||||||
|
log(TAG, ERROR) { "onAction: no profile for widgetId=$widgetId" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val device = ep.deviceMonitor().getDeviceForProfile(profileId)
|
||||||
|
if (device == null) {
|
||||||
|
log(TAG, ERROR) { "onAction: no device for profileId=$profileId" }
|
||||||
|
AncGlanceWidget().updateAll(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val address = device.address
|
||||||
|
if (address == null) {
|
||||||
|
log(TAG, ERROR) { "onAction: device has no address" }
|
||||||
|
AncGlanceWidget().updateAll(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ep.aapConnectionManager().sendCommand(address, AapCommand.SetAncMode(mode))
|
||||||
|
log(TAG, VERBOSE) { "onAction: sent SetAncMode($mode) to $address" }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "onAction: sendCommand failed: ${e.asLog()}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
AncGlanceWidget().updateAll(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val ANC_MODE_KEY = ActionParameters.Key<String>("anc_mode")
|
||||||
|
val WIDGET_ID_KEY = ActionParameters.Key<Int>("widget_id")
|
||||||
|
private val TAG = logTag("Widget", "ANC", "Callback")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package eu.darken.capod.main.ui.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.glance.appwidget.GlanceAppWidget
|
||||||
|
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||||
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
class AncWidgetProvider : GlanceAppWidgetReceiver() {
|
||||||
|
|
||||||
|
override val glanceAppWidget: GlanceAppWidget = AncGlanceWidget()
|
||||||
|
|
||||||
|
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||||
|
log(TAG) { "onDeleted(appWidgetIds=${appWidgetIds.toList()})" }
|
||||||
|
super.onDeleted(context, appWidgetIds)
|
||||||
|
val ep = EntryPointAccessors.fromApplication(
|
||||||
|
context.applicationContext,
|
||||||
|
AncGlanceWidget.AncWidgetEntryPoint::class.java,
|
||||||
|
)
|
||||||
|
appWidgetIds.forEach { widgetId ->
|
||||||
|
ep.widgetSettings().removeWidget(widgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = logTag("Widget", "ANC", "Provider")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package eu.darken.capod.main.ui.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.R
|
||||||
|
import eu.darken.capod.main.ui.components.shortLabel
|
||||||
|
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||||
|
|
||||||
|
sealed class AncWidgetRenderState {
|
||||||
|
abstract val theme: WidgetTheme
|
||||||
|
@get:ColorInt abstract val resolvedBgColor: Int
|
||||||
|
@get:ColorInt abstract val resolvedTextColor: Int
|
||||||
|
@get:ColorInt abstract val resolvedIconColor: Int
|
||||||
|
|
||||||
|
data class Active(
|
||||||
|
override val theme: WidgetTheme,
|
||||||
|
@ColorInt override val resolvedBgColor: Int,
|
||||||
|
@ColorInt override val resolvedTextColor: Int,
|
||||||
|
@ColorInt override val resolvedIconColor: Int,
|
||||||
|
@ColorInt val resolvedActiveColor: Int,
|
||||||
|
@ColorInt val resolvedOnActiveColor: Int,
|
||||||
|
val modes: List<ModeItem>,
|
||||||
|
val deviceLabel: String?,
|
||||||
|
val layout: AncLayout,
|
||||||
|
) : AncWidgetRenderState()
|
||||||
|
|
||||||
|
data class Message(
|
||||||
|
override val theme: WidgetTheme,
|
||||||
|
@ColorInt override val resolvedBgColor: Int,
|
||||||
|
@ColorInt override val resolvedTextColor: Int,
|
||||||
|
@ColorInt override val resolvedIconColor: Int,
|
||||||
|
val primaryText: String,
|
||||||
|
val secondaryText: String? = null,
|
||||||
|
) : AncWidgetRenderState()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun previewActive(
|
||||||
|
context: Context,
|
||||||
|
theme: WidgetTheme = WidgetTheme.DEFAULT,
|
||||||
|
@ColorInt bgColor: Int = 0xFFFFFFFF.toInt(),
|
||||||
|
@ColorInt textColor: Int = 0xFF1E1E1E.toInt(),
|
||||||
|
@ColorInt iconColor: Int = 0xFF1E1E1E.toInt(),
|
||||||
|
@ColorInt activeColor: Int = 0xFFCCE5FF.toInt(),
|
||||||
|
@ColorInt onActiveColor: Int = 0xFF001E30.toInt(),
|
||||||
|
layout: AncLayout = AncLayout.GRID_2X2,
|
||||||
|
modes: List<AapSetting.AncMode.Value> = AapSetting.AncMode.Value.entries,
|
||||||
|
activeMode: AapSetting.AncMode.Value = AapSetting.AncMode.Value.ON,
|
||||||
|
deviceLabel: String? = null,
|
||||||
|
): Active = Active(
|
||||||
|
theme = theme,
|
||||||
|
resolvedBgColor = bgColor,
|
||||||
|
resolvedTextColor = textColor,
|
||||||
|
resolvedIconColor = iconColor,
|
||||||
|
resolvedActiveColor = activeColor,
|
||||||
|
resolvedOnActiveColor = onActiveColor,
|
||||||
|
modes = modes.map { mode ->
|
||||||
|
ModeItem(
|
||||||
|
mode = mode,
|
||||||
|
label = mode.shortLabel(context),
|
||||||
|
iconRes = mode.previewIconRes(),
|
||||||
|
state = if (mode == activeMode) ButtonState.ACTIVE else ButtonState.INACTIVE,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
deviceLabel = deviceLabel,
|
||||||
|
layout = layout,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ModeItem(
|
||||||
|
val mode: AapSetting.AncMode.Value,
|
||||||
|
val label: String,
|
||||||
|
@DrawableRes val iconRes: Int,
|
||||||
|
val state: ButtonState,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class ButtonState {
|
||||||
|
ACTIVE, PENDING, INACTIVE,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class AncLayout {
|
||||||
|
QUAD_CORNERS, ROW_ICONS, COLUMN_ICONS, GRID_2X2, ROW, COLUMN,
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AapSetting.AncMode.Value.previewIconRes(): Int = when (this) {
|
||||||
|
AapSetting.AncMode.Value.OFF -> R.drawable.ic_anc_off
|
||||||
|
AapSetting.AncMode.Value.ON -> R.drawable.ic_anc_on
|
||||||
|
AapSetting.AncMode.Value.TRANSPARENCY -> R.drawable.ic_anc_transparency
|
||||||
|
AapSetting.AncMode.Value.ADAPTIVE -> R.drawable.ic_anc_adaptive
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package eu.darken.capod.main.ui.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.darken.capod.R
|
||||||
|
import eu.darken.capod.main.ui.components.shortLabel
|
||||||
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
|
import eu.darken.capod.monitor.core.visibleAncModes
|
||||||
|
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||||
|
|
||||||
|
object AncWidgetRenderStateMapper {
|
||||||
|
|
||||||
|
fun map(
|
||||||
|
context: Context,
|
||||||
|
device: PodDevice?,
|
||||||
|
theme: WidgetTheme,
|
||||||
|
isPro: Boolean,
|
||||||
|
hasConfiguredProfile: Boolean,
|
||||||
|
profileLabel: String?,
|
||||||
|
layout: AncLayout,
|
||||||
|
): AncWidgetRenderState {
|
||||||
|
val bgColor = WidgetRenderStateMapper.resolvedBgColor(context, theme)
|
||||||
|
val textColor = WidgetRenderStateMapper.resolvedTextColor(context, theme)
|
||||||
|
val iconColor = WidgetRenderStateMapper.resolvedIconColor(context, theme)
|
||||||
|
val activeColor = WidgetRenderStateMapper.resolveThemeColor(context, com.google.android.material.R.attr.colorSecondaryContainer)
|
||||||
|
val onActiveColor = WidgetRenderStateMapper.resolveThemeColor(context, com.google.android.material.R.attr.colorOnSecondaryContainer)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
!isPro -> AncWidgetRenderState.Message(
|
||||||
|
theme = theme,
|
||||||
|
resolvedBgColor = bgColor,
|
||||||
|
resolvedTextColor = textColor,
|
||||||
|
resolvedIconColor = iconColor,
|
||||||
|
primaryText = context.getString(R.string.upgrade_capod_label),
|
||||||
|
secondaryText = context.getString(R.string.upgrade_capod_description),
|
||||||
|
)
|
||||||
|
|
||||||
|
device == null -> {
|
||||||
|
val messageRes = if (hasConfiguredProfile) {
|
||||||
|
R.string.widget_no_data_label
|
||||||
|
} else {
|
||||||
|
R.string.overview_nomaindevice_label
|
||||||
|
}
|
||||||
|
AncWidgetRenderState.Message(
|
||||||
|
theme = theme,
|
||||||
|
resolvedBgColor = bgColor,
|
||||||
|
resolvedTextColor = textColor,
|
||||||
|
resolvedIconColor = iconColor,
|
||||||
|
primaryText = context.getString(messageRes),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
!device.hasAncControl -> AncWidgetRenderState.Message(
|
||||||
|
theme = theme,
|
||||||
|
resolvedBgColor = bgColor,
|
||||||
|
resolvedTextColor = textColor,
|
||||||
|
resolvedIconColor = iconColor,
|
||||||
|
primaryText = context.getString(R.string.anc_widget_no_anc_support_label),
|
||||||
|
)
|
||||||
|
|
||||||
|
!device.isAapConnected -> AncWidgetRenderState.Message(
|
||||||
|
theme = theme,
|
||||||
|
resolvedBgColor = bgColor,
|
||||||
|
resolvedTextColor = textColor,
|
||||||
|
resolvedIconColor = iconColor,
|
||||||
|
primaryText = context.getString(R.string.anc_widget_aap_not_connected_label),
|
||||||
|
)
|
||||||
|
|
||||||
|
!device.isAapReady -> AncWidgetRenderState.Message(
|
||||||
|
theme = theme,
|
||||||
|
resolvedBgColor = bgColor,
|
||||||
|
resolvedTextColor = textColor,
|
||||||
|
resolvedIconColor = iconColor,
|
||||||
|
primaryText = context.getString(R.string.anc_widget_aap_connecting_label),
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val ancMode = device.ancMode ?: return AncWidgetRenderState.Message(
|
||||||
|
theme = theme,
|
||||||
|
resolvedBgColor = bgColor,
|
||||||
|
resolvedTextColor = textColor,
|
||||||
|
resolvedIconColor = iconColor,
|
||||||
|
primaryText = context.getString(R.string.anc_widget_aap_connecting_label),
|
||||||
|
)
|
||||||
|
|
||||||
|
val currentMode = ancMode.current
|
||||||
|
val pendingMode = device.pendingAncMode
|
||||||
|
|
||||||
|
val filteredModes = device.visibleAncModes
|
||||||
|
|
||||||
|
// If the pending mode has been filtered out of visibleAncModes (e.g. device
|
||||||
|
// reports AllowOff=false while OFF is still pending), fall back to currentMode
|
||||||
|
// for the ACTIVE highlight so the widget never renders with zero active buttons.
|
||||||
|
val pendingVisible = pendingMode != null && filteredModes.contains(pendingMode)
|
||||||
|
val displayMode = if (pendingVisible) pendingMode else currentMode
|
||||||
|
|
||||||
|
val modeItems = filteredModes.map { mode ->
|
||||||
|
ModeItem(
|
||||||
|
mode = mode,
|
||||||
|
label = mode.shortLabel(context),
|
||||||
|
iconRes = mode.iconDrawableRes(),
|
||||||
|
state = when {
|
||||||
|
pendingVisible && mode == pendingMode -> ButtonState.PENDING
|
||||||
|
mode == displayMode -> ButtonState.ACTIVE
|
||||||
|
else -> ButtonState.INACTIVE
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AncWidgetRenderState.Active(
|
||||||
|
theme = theme,
|
||||||
|
resolvedBgColor = bgColor,
|
||||||
|
resolvedTextColor = textColor,
|
||||||
|
resolvedIconColor = iconColor,
|
||||||
|
resolvedActiveColor = activeColor,
|
||||||
|
resolvedOnActiveColor = onActiveColor,
|
||||||
|
modes = modeItems,
|
||||||
|
deviceLabel = profileLabel ?: device.getLabel(context),
|
||||||
|
layout = layout,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AapSetting.AncMode.Value.iconDrawableRes(): Int = when (this) {
|
||||||
|
AapSetting.AncMode.Value.OFF -> R.drawable.ic_anc_off
|
||||||
|
AapSetting.AncMode.Value.ON -> R.drawable.ic_anc_on
|
||||||
|
AapSetting.AncMode.Value.TRANSPARENCY -> R.drawable.ic_anc_transparency
|
||||||
|
AapSetting.AncMode.Value.ADAPTIVE -> R.drawable.ic_anc_adaptive
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package eu.darken.capod.main.ui.widget
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ComposeAncWidgetPreview(
|
||||||
|
state: AncWidgetRenderState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
when (state) {
|
||||||
|
is AncWidgetRenderState.Active -> ActiveAncPreview(state, modifier)
|
||||||
|
is AncWidgetRenderState.Message -> MessageAncPreview(state, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActiveAncPreview(
|
||||||
|
state: AncWidgetRenderState.Active,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(Color(state.resolvedBgColor))
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
AncGridRow(state.modes.take(2), state.resolvedIconColor, state.resolvedBgColor)
|
||||||
|
if (state.modes.size > 2) {
|
||||||
|
AncGridRow(state.modes.drop(2), state.resolvedIconColor, state.resolvedBgColor)
|
||||||
|
}
|
||||||
|
if (state.theme.showDeviceLabel && state.deviceLabel != null) {
|
||||||
|
Text(
|
||||||
|
text = state.deviceLabel,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(state.resolvedTextColor),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AncGridRow(
|
||||||
|
items: List<ModeItem>,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedBgColor: Int,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
items.forEach { item ->
|
||||||
|
AncPreviewButton(item, resolvedIconColor, resolvedBgColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AncPreviewButton(
|
||||||
|
item: ModeItem,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedBgColor: Int,
|
||||||
|
) {
|
||||||
|
val bgColor = when (item.state) {
|
||||||
|
ButtonState.ACTIVE -> Color(resolvedIconColor)
|
||||||
|
ButtonState.PENDING -> Color(resolvedIconColor).copy(alpha = 0.6f)
|
||||||
|
ButtonState.INACTIVE -> Color(resolvedIconColor).copy(alpha = 0.12f)
|
||||||
|
}
|
||||||
|
val tint = when (item.state) {
|
||||||
|
ButtonState.ACTIVE, ButtonState.PENDING -> Color(resolvedBgColor)
|
||||||
|
ButtonState.INACTIVE -> Color(resolvedIconColor)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(bgColor),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(item.iconRes),
|
||||||
|
contentDescription = item.label,
|
||||||
|
colorFilter = ColorFilter.tint(tint),
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageAncPreview(
|
||||||
|
state: AncWidgetRenderState.Message,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(Color(state.resolvedBgColor))
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = state.primaryText,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(state.resolvedTextColor),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
if (state.secondaryText != null) {
|
||||||
|
Text(
|
||||||
|
text = state.secondaryText,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color(state.resolvedTextColor),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,569 @@
|
|||||||
|
package eu.darken.capod.main.ui.widget
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.glance.ColorFilter
|
||||||
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.Image
|
||||||
|
import androidx.glance.ImageProvider
|
||||||
|
import androidx.glance.action.ActionParameters
|
||||||
|
import androidx.glance.action.clickable
|
||||||
|
import androidx.glance.appwidget.action.actionRunCallback
|
||||||
|
import androidx.glance.appwidget.action.actionStartActivity
|
||||||
|
import androidx.glance.appwidget.cornerRadius
|
||||||
|
import androidx.glance.background
|
||||||
|
import androidx.glance.layout.Alignment
|
||||||
|
import androidx.glance.layout.Box
|
||||||
|
import androidx.glance.layout.Column
|
||||||
|
import androidx.glance.layout.ColumnScope
|
||||||
|
import androidx.glance.layout.Row
|
||||||
|
import androidx.glance.layout.RowScope
|
||||||
|
import androidx.glance.layout.Spacer
|
||||||
|
import androidx.glance.layout.fillMaxHeight
|
||||||
|
import androidx.glance.layout.fillMaxSize
|
||||||
|
import androidx.glance.layout.fillMaxWidth
|
||||||
|
import androidx.glance.layout.height
|
||||||
|
import androidx.glance.layout.padding
|
||||||
|
import androidx.glance.layout.size
|
||||||
|
import androidx.glance.layout.width
|
||||||
|
import androidx.glance.text.FontWeight
|
||||||
|
import androidx.glance.text.Text
|
||||||
|
import androidx.glance.text.TextAlign
|
||||||
|
import androidx.glance.text.TextStyle
|
||||||
|
import androidx.glance.unit.ColorProvider
|
||||||
|
import eu.darken.capod.main.ui.MainActivity
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GlanceAncWidgetContent(
|
||||||
|
state: AncWidgetRenderState,
|
||||||
|
context: android.content.Context,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
val openApp = actionStartActivity(
|
||||||
|
Intent(context, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
when (state) {
|
||||||
|
is AncWidgetRenderState.Active -> GlanceAncActive(state, appWidgetId)
|
||||||
|
is AncWidgetRenderState.Message -> GlanceAncMessage(state, GlanceModifier.clickable(openApp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GlanceAncActive(
|
||||||
|
state: AncWidgetRenderState.Active,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
val textColor = fixedAncColor(state.resolvedTextColor)
|
||||||
|
val iconColor = fixedAncColor(state.resolvedIconColor)
|
||||||
|
val showDeviceLabel = state.layout != AncLayout.ROW_ICONS
|
||||||
|
&& state.layout != AncLayout.COLUMN_ICONS
|
||||||
|
&& state.layout != AncLayout.QUAD_CORNERS
|
||||||
|
|
||||||
|
AncWidgetRoot(state.resolvedBgColor) {
|
||||||
|
when (state.layout) {
|
||||||
|
AncLayout.GRID_2X2 -> AncGrid(state.modes, iconColor, state.resolvedIconColor, state.resolvedActiveColor, state.resolvedOnActiveColor, appWidgetId)
|
||||||
|
AncLayout.ROW -> AncRow(state.modes, iconColor, textColor, state.resolvedIconColor, state.resolvedActiveColor, state.resolvedOnActiveColor, appWidgetId)
|
||||||
|
AncLayout.COLUMN -> AncColumn(state.modes, iconColor, textColor, state.resolvedIconColor, state.resolvedActiveColor, state.resolvedOnActiveColor, appWidgetId)
|
||||||
|
AncLayout.ROW_ICONS -> AncRowIcons(state.modes, state.resolvedIconColor, state.resolvedActiveColor, state.resolvedOnActiveColor, appWidgetId)
|
||||||
|
AncLayout.COLUMN_ICONS -> AncColumnIcons(state.modes, state.resolvedIconColor, state.resolvedActiveColor, state.resolvedOnActiveColor, appWidgetId)
|
||||||
|
AncLayout.QUAD_CORNERS -> AncQuadCorners(state.modes, state.resolvedIconColor, state.resolvedActiveColor, state.resolvedOnActiveColor, appWidgetId)
|
||||||
|
}
|
||||||
|
if (showDeviceLabel) {
|
||||||
|
GlanceAncDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AncRowIcons(
|
||||||
|
modes: List<ModeItem>,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
val dividerColor = fixedAncColor(applyAncAlpha(resolvedIconColor, 60))
|
||||||
|
Row(
|
||||||
|
modifier = GlanceModifier.fillMaxSize(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
modes.forEachIndexed { index, item ->
|
||||||
|
if (index > 0) {
|
||||||
|
Spacer(modifier = GlanceModifier.width(1.dp).fillMaxHeight().background(dividerColor))
|
||||||
|
}
|
||||||
|
DividerRowCell(item, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AncColumnIcons(
|
||||||
|
modes: List<ModeItem>,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
val dividerColor = fixedAncColor(applyAncAlpha(resolvedIconColor, 60))
|
||||||
|
Column(
|
||||||
|
modifier = GlanceModifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
modes.forEachIndexed { index, item ->
|
||||||
|
if (index > 0) {
|
||||||
|
Spacer(modifier = GlanceModifier.fillMaxWidth().height(1.dp).background(dividerColor))
|
||||||
|
}
|
||||||
|
DividerColumnCell(item, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.DividerRowCell(
|
||||||
|
item: ModeItem,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = GlanceModifier
|
||||||
|
.defaultWeight()
|
||||||
|
.fillMaxHeight()
|
||||||
|
.clickable(ancModeAction(item, appWidgetId)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
DividerCellIcon(item, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColumnScope.DividerColumnCell(
|
||||||
|
item: ModeItem,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = GlanceModifier
|
||||||
|
.defaultWeight()
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(ancModeAction(item, appWidgetId)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
DividerCellIcon(item, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DividerCellIcon(
|
||||||
|
item: ModeItem,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
) {
|
||||||
|
val bgModifier = when (item.state) {
|
||||||
|
ButtonState.ACTIVE -> GlanceModifier.background(fixedAncColor(resolvedActiveColor))
|
||||||
|
ButtonState.PENDING -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedActiveColor, 153)))
|
||||||
|
ButtonState.INACTIVE -> GlanceModifier
|
||||||
|
}
|
||||||
|
val tint = when (item.state) {
|
||||||
|
ButtonState.ACTIVE, ButtonState.PENDING -> ColorFilter.tint(fixedAncColor(resolvedOnActiveColor))
|
||||||
|
ButtonState.INACTIVE -> ColorFilter.tint(fixedAncColor(applyAncAlpha(resolvedIconColor, 160)))
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = bgModifier
|
||||||
|
.size(36.dp)
|
||||||
|
.cornerRadius(18.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
provider = ImageProvider(item.iconRes),
|
||||||
|
contentDescription = item.label,
|
||||||
|
modifier = GlanceModifier.size(22.dp),
|
||||||
|
colorFilter = tint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AncQuadCorners(
|
||||||
|
modes: List<ModeItem>,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
val dividerColor = fixedAncColor(applyAncAlpha(resolvedIconColor, 60))
|
||||||
|
|
||||||
|
Column(modifier = GlanceModifier.fillMaxSize()) {
|
||||||
|
Row(modifier = GlanceModifier.fillMaxWidth().defaultWeight()) {
|
||||||
|
QuadCornerCell(modes.getOrNull(0), resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId)
|
||||||
|
Spacer(modifier = GlanceModifier.width(1.dp).fillMaxHeight().background(dividerColor))
|
||||||
|
QuadCornerCell(modes.getOrNull(1), resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId)
|
||||||
|
}
|
||||||
|
Spacer(modifier = GlanceModifier.fillMaxWidth().height(1.dp).background(dividerColor))
|
||||||
|
Row(modifier = GlanceModifier.fillMaxWidth().defaultWeight()) {
|
||||||
|
QuadCornerCell(modes.getOrNull(2), resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId)
|
||||||
|
Spacer(modifier = GlanceModifier.width(1.dp).fillMaxHeight().background(dividerColor))
|
||||||
|
QuadCornerCell(modes.getOrNull(3), resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.QuadCornerCell(
|
||||||
|
item: ModeItem?,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
val cellModifier = GlanceModifier.defaultWeight().fillMaxHeight()
|
||||||
|
val clickable = if (item != null) cellModifier.clickable(ancModeAction(item, appWidgetId)) else cellModifier
|
||||||
|
Box(
|
||||||
|
modifier = clickable,
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (item != null) {
|
||||||
|
DividerCellIcon(item, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AncGrid(
|
||||||
|
modes: List<ModeItem>,
|
||||||
|
iconColor: ColorProvider,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
val firstRow = modes.take(2)
|
||||||
|
val secondRow = modes.drop(2)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = GlanceModifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = GlanceModifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
firstRow.forEachIndexed { index, item ->
|
||||||
|
if (index > 0) Spacer(modifier = GlanceModifier.width(10.dp))
|
||||||
|
AncGridButton(item, iconColor, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (secondRow.isNotEmpty()) {
|
||||||
|
Spacer(modifier = GlanceModifier.height(10.dp))
|
||||||
|
Row(
|
||||||
|
modifier = GlanceModifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
secondRow.forEachIndexed { index, item ->
|
||||||
|
if (index > 0) Spacer(modifier = GlanceModifier.width(10.dp))
|
||||||
|
AncGridButton(item, iconColor, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AncGridButton(
|
||||||
|
item: ModeItem,
|
||||||
|
iconColor: ColorProvider,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
val bgModifier = when (item.state) {
|
||||||
|
ButtonState.ACTIVE -> GlanceModifier.background(fixedAncColor(resolvedActiveColor))
|
||||||
|
ButtonState.PENDING -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedActiveColor, 153)))
|
||||||
|
ButtonState.INACTIVE -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedIconColor, 30)))
|
||||||
|
}
|
||||||
|
val tint = when (item.state) {
|
||||||
|
ButtonState.ACTIVE -> ColorFilter.tint(fixedAncColor(resolvedOnActiveColor))
|
||||||
|
ButtonState.PENDING -> ColorFilter.tint(fixedAncColor(resolvedOnActiveColor))
|
||||||
|
ButtonState.INACTIVE -> ColorFilter.tint(iconColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = bgModifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(48.dp)
|
||||||
|
.cornerRadius(12.dp)
|
||||||
|
.clickable(ancModeAction(item, appWidgetId)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
provider = ImageProvider(item.iconRes),
|
||||||
|
contentDescription = item.label,
|
||||||
|
modifier = GlanceModifier.size(24.dp),
|
||||||
|
colorFilter = tint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AncRow(
|
||||||
|
modes: List<ModeItem>,
|
||||||
|
iconColor: ColorProvider,
|
||||||
|
textColor: ColorProvider,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = GlanceModifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
modes.forEachIndexed { index, item ->
|
||||||
|
if (index > 0) Spacer(modifier = GlanceModifier.width(6.dp))
|
||||||
|
AncLabeledButton(item, iconColor, textColor, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AncColumn(
|
||||||
|
modes: List<ModeItem>,
|
||||||
|
iconColor: ColorProvider,
|
||||||
|
textColor: ColorProvider,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = GlanceModifier.fillMaxSize().padding(vertical = 8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
modes.forEachIndexed { index, item ->
|
||||||
|
if (index > 0) Spacer(modifier = GlanceModifier.height(6.dp))
|
||||||
|
AncColumnItem(item, iconColor, textColor, resolvedIconColor, resolvedActiveColor, resolvedOnActiveColor, appWidgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColumnScope.AncColumnItem(
|
||||||
|
item: ModeItem,
|
||||||
|
iconColor: ColorProvider,
|
||||||
|
textColor: ColorProvider,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
appWidgetId: Int,
|
||||||
|
) {
|
||||||
|
val bgModifier = when (item.state) {
|
||||||
|
ButtonState.ACTIVE -> GlanceModifier.background(fixedAncColor(resolvedActiveColor))
|
||||||
|
ButtonState.PENDING -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedActiveColor, 153)))
|
||||||
|
ButtonState.INACTIVE -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedIconColor, 30)))
|
||||||
|
}
|
||||||
|
val tint = when (item.state) {
|
||||||
|
ButtonState.ACTIVE, ButtonState.PENDING -> ColorFilter.tint(fixedAncColor(resolvedOnActiveColor))
|
||||||
|
ButtonState.INACTIVE -> ColorFilter.tint(iconColor)
|
||||||
|
}
|
||||||
|
val labelColor = when (item.state) {
|
||||||
|
ButtonState.ACTIVE, ButtonState.PENDING -> fixedAncColor(resolvedOnActiveColor)
|
||||||
|
ButtonState.INACTIVE -> textColor
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = bgModifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.defaultWeight()
|
||||||
|
.cornerRadius(12.dp)
|
||||||
|
.clickable(ancModeAction(item, appWidgetId))
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
provider = ImageProvider(item.iconRes),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = GlanceModifier.size(18.dp),
|
||||||
|
colorFilter = tint,
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.width(6.dp))
|
||||||
|
Text(
|
||||||
|
text = item.label,
|
||||||
|
style = TextStyle(color = labelColor, fontSize = 12.sp, fontWeight = FontWeight.Medium),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AncLabeledButton(
|
||||||
|
item: ModeItem,
|
||||||
|
iconColor: ColorProvider,
|
||||||
|
textColor: ColorProvider,
|
||||||
|
resolvedIconColor: Int,
|
||||||
|
resolvedActiveColor: Int,
|
||||||
|
resolvedOnActiveColor: Int,
|
||||||
|
appWidgetId: Int,
|
||||||
|
horizontal: Boolean = false,
|
||||||
|
) {
|
||||||
|
val bgModifier = when (item.state) {
|
||||||
|
ButtonState.ACTIVE -> GlanceModifier.background(fixedAncColor(resolvedActiveColor))
|
||||||
|
ButtonState.PENDING -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedActiveColor, 153)))
|
||||||
|
ButtonState.INACTIVE -> GlanceModifier.background(fixedAncColor(applyAncAlpha(resolvedIconColor, 30)))
|
||||||
|
}
|
||||||
|
val tint = when (item.state) {
|
||||||
|
ButtonState.ACTIVE -> ColorFilter.tint(fixedAncColor(resolvedOnActiveColor))
|
||||||
|
ButtonState.PENDING -> ColorFilter.tint(fixedAncColor(resolvedOnActiveColor))
|
||||||
|
ButtonState.INACTIVE -> ColorFilter.tint(iconColor)
|
||||||
|
}
|
||||||
|
val labelColor = when (item.state) {
|
||||||
|
ButtonState.ACTIVE -> fixedAncColor(resolvedOnActiveColor)
|
||||||
|
ButtonState.PENDING -> fixedAncColor(resolvedOnActiveColor)
|
||||||
|
ButtonState.INACTIVE -> textColor
|
||||||
|
}
|
||||||
|
|
||||||
|
if (horizontal) {
|
||||||
|
Row(
|
||||||
|
modifier = bgModifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.cornerRadius(12.dp)
|
||||||
|
.clickable(ancModeAction(item, appWidgetId))
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
provider = ImageProvider(item.iconRes),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = GlanceModifier.size(18.dp),
|
||||||
|
colorFilter = tint,
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.width(6.dp))
|
||||||
|
Text(
|
||||||
|
text = item.label,
|
||||||
|
style = TextStyle(color = labelColor, fontSize = 12.sp, fontWeight = FontWeight.Medium),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = bgModifier
|
||||||
|
.padding(horizontal = 14.dp, vertical = 10.dp)
|
||||||
|
.cornerRadius(14.dp)
|
||||||
|
.clickable(ancModeAction(item, appWidgetId)),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
provider = ImageProvider(item.iconRes),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = GlanceModifier.size(22.dp),
|
||||||
|
colorFilter = tint,
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = item.label,
|
||||||
|
style = TextStyle(color = labelColor, fontSize = 12.sp, fontWeight = FontWeight.Medium),
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GlanceAncMessage(
|
||||||
|
state: AncWidgetRenderState.Message,
|
||||||
|
clickModifier: GlanceModifier,
|
||||||
|
) {
|
||||||
|
val textStyle = TextStyle(
|
||||||
|
color = fixedAncColor(state.resolvedTextColor),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
|
||||||
|
AncWidgetRoot(state.resolvedBgColor, clickModifier) {
|
||||||
|
Text(
|
||||||
|
text = state.primaryText,
|
||||||
|
style = textStyle,
|
||||||
|
modifier = GlanceModifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
if (state.secondaryText != null) {
|
||||||
|
Text(
|
||||||
|
text = state.secondaryText,
|
||||||
|
style = TextStyle(
|
||||||
|
color = fixedAncColor(state.resolvedTextColor),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
),
|
||||||
|
modifier = GlanceModifier.fillMaxWidth(),
|
||||||
|
maxLines = 4,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AncWidgetRoot(
|
||||||
|
bgColor: Int,
|
||||||
|
extraModifier: GlanceModifier = GlanceModifier,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = extraModifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(fixedAncColor(bgColor))
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GlanceAncDeviceLabel(
|
||||||
|
label: String?,
|
||||||
|
visible: Boolean,
|
||||||
|
textColor: Int,
|
||||||
|
) {
|
||||||
|
if (visible && label != null) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = TextStyle(
|
||||||
|
color = fixedAncColor(textColor),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = GlanceModifier.padding(top = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixedAncColor(argb: Int): ColorProvider = ColorProvider(Color(argb))
|
||||||
|
|
||||||
|
private fun applyAncAlpha(color: Int, alpha: Int): Int {
|
||||||
|
return android.graphics.Color.argb(
|
||||||
|
alpha,
|
||||||
|
android.graphics.Color.red(color),
|
||||||
|
android.graphics.Color.green(color),
|
||||||
|
android.graphics.Color.blue(color),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ancModeAction(item: ModeItem, appWidgetId: Int) = actionRunCallback<AncModeActionCallback>(
|
||||||
|
parameters = androidx.glance.action.actionParametersOf(
|
||||||
|
AncModeActionCallback.ANC_MODE_KEY to item.mode.name,
|
||||||
|
AncModeActionCallback.WIDGET_ID_KEY to appWidgetId,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -74,6 +74,7 @@ class WidgetConfigurationActivity : Activity2() {
|
|||||||
state?.let { currentState ->
|
state?.let { currentState ->
|
||||||
WidgetConfigurationScreen(
|
WidgetConfigurationScreen(
|
||||||
state = currentState,
|
state = currentState,
|
||||||
|
showAapRequiredHint = currentState.isAncWidget,
|
||||||
onSelectProfile = { profile -> vm.selectProfile(profile.id) },
|
onSelectProfile = { profile -> vm.selectProfile(profile.id) },
|
||||||
onSelectPreset = { preset -> vm.selectPreset(preset) },
|
onSelectPreset = { preset -> vm.selectPreset(preset) },
|
||||||
onEnterCustomMode = { bg, fg -> vm.enterCustomMode(bg, fg) },
|
onEnterCustomMode = { bg, fg -> vm.enterCustomMode(bg, fg) },
|
||||||
@@ -84,7 +85,7 @@ class WidgetConfigurationActivity : Activity2() {
|
|||||||
onReset = { vm.resetToDefaults() },
|
onReset = { vm.resetToDefaults() },
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
if (currentState.isPro) {
|
if (currentState.isPro) {
|
||||||
confirmSelection()
|
confirmSelection(currentState.isAncWidget)
|
||||||
} else {
|
} else {
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(this@WidgetConfigurationActivity, MainActivity::class.java).apply {
|
Intent(this@WidgetConfigurationActivity, MainActivity::class.java).apply {
|
||||||
@@ -100,7 +101,7 @@ class WidgetConfigurationActivity : Activity2() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun confirmSelection() {
|
private fun confirmSelection(isAncWidget: Boolean) {
|
||||||
vm.confirmSelection()
|
vm.confirmSelection()
|
||||||
|
|
||||||
val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
|
val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
|
||||||
@@ -109,7 +110,11 @@ class WidgetConfigurationActivity : Activity2() {
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val manager = GlanceAppWidgetManager(appContext)
|
val manager = GlanceAppWidgetManager(appContext)
|
||||||
val glanceId = manager.getGlanceIdBy(widgetId)
|
val glanceId = manager.getGlanceIdBy(widgetId)
|
||||||
BatteryGlanceWidget().update(appContext, glanceId)
|
if (isAncWidget) {
|
||||||
|
AncGlanceWidget().update(appContext, glanceId)
|
||||||
|
} else {
|
||||||
|
BatteryGlanceWidget().update(appContext, glanceId)
|
||||||
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ import eu.darken.capod.profiles.core.DeviceProfile
|
|||||||
@Composable
|
@Composable
|
||||||
fun WidgetConfigurationScreen(
|
fun WidgetConfigurationScreen(
|
||||||
state: WidgetConfigurationViewModel.State,
|
state: WidgetConfigurationViewModel.State,
|
||||||
|
showAapRequiredHint: Boolean = false,
|
||||||
onSelectProfile: (DeviceProfile) -> Unit,
|
onSelectProfile: (DeviceProfile) -> Unit,
|
||||||
onSelectPreset: (WidgetTheme.Preset) -> Unit,
|
onSelectPreset: (WidgetTheme.Preset) -> Unit,
|
||||||
onEnterCustomMode: (defaultBg: Int, defaultFg: Int) -> Unit,
|
onEnterCustomMode: (defaultBg: Int, defaultFg: Int) -> Unit,
|
||||||
@@ -118,6 +119,26 @@ fun WidgetConfigurationScreen(
|
|||||||
.padding(start = screenHPad, end = screenHPad, bottom = 16.dp),
|
.padding(start = screenHPad, end = screenHPad, bottom = 16.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (showAapRequiredHint) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = screenHPad),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.anc_widget_config_aap_required_hint),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
// Card A — Select Device
|
// Card A — Select Device
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -186,6 +207,17 @@ fun WidgetConfigurationScreen(
|
|||||||
WidgetConfigPreview(
|
WidgetConfigPreview(
|
||||||
theme = state.theme,
|
theme = state.theme,
|
||||||
deviceLabel = state.profiles.firstOrNull { it.id == state.selectedProfile }?.label,
|
deviceLabel = state.profiles.firstOrNull { it.id == state.selectedProfile }?.label,
|
||||||
|
isAncWidget = state.isAncWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.widget_config_preview_resize_hint),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -435,20 +467,12 @@ private fun ProfileSelectionItem(
|
|||||||
private fun WidgetConfigPreview(
|
private fun WidgetConfigPreview(
|
||||||
theme: WidgetTheme,
|
theme: WidgetTheme,
|
||||||
deviceLabel: String?,
|
deviceLabel: String?,
|
||||||
|
isAncWidget: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val hasTransparency = theme.backgroundColor != null && theme.backgroundAlpha < 255
|
val hasTransparency = theme.backgroundColor != null && theme.backgroundAlpha < 255
|
||||||
|
|
||||||
val previewState = remember(theme, deviceLabel) {
|
|
||||||
WidgetRenderState.previewDualPod(
|
|
||||||
theme = theme,
|
|
||||||
bgColor = WidgetRenderStateMapper.resolvedBgColor(context, theme),
|
|
||||||
textColor = WidgetRenderStateMapper.resolvedTextColor(context, theme),
|
|
||||||
iconColor = WidgetRenderStateMapper.resolvedIconColor(context, theme),
|
|
||||||
).copy(deviceLabel = deviceLabel)
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
@@ -461,15 +485,39 @@ private fun WidgetConfigPreview(
|
|||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
|
val previewContent: @Composable (Modifier) -> Unit = { innerModifier ->
|
||||||
|
if (isAncWidget) {
|
||||||
|
val ancState = remember(theme, deviceLabel, context) {
|
||||||
|
AncWidgetRenderState.previewActive(
|
||||||
|
context = context,
|
||||||
|
theme = theme,
|
||||||
|
bgColor = WidgetRenderStateMapper.resolvedBgColor(context, theme),
|
||||||
|
textColor = WidgetRenderStateMapper.resolvedTextColor(context, theme),
|
||||||
|
iconColor = WidgetRenderStateMapper.resolvedIconColor(context, theme),
|
||||||
|
activeColor = WidgetRenderStateMapper.resolveThemeColor(context, com.google.android.material.R.attr.colorSecondaryContainer),
|
||||||
|
onActiveColor = WidgetRenderStateMapper.resolveThemeColor(context, com.google.android.material.R.attr.colorOnSecondaryContainer),
|
||||||
|
deviceLabel = deviceLabel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ComposeAncWidgetPreview(state = ancState, modifier = innerModifier)
|
||||||
|
} else {
|
||||||
|
val batteryState = remember(theme, deviceLabel) {
|
||||||
|
WidgetRenderState.previewDualPod(
|
||||||
|
theme = theme,
|
||||||
|
bgColor = WidgetRenderStateMapper.resolvedBgColor(context, theme),
|
||||||
|
textColor = WidgetRenderStateMapper.resolvedTextColor(context, theme),
|
||||||
|
iconColor = WidgetRenderStateMapper.resolvedIconColor(context, theme),
|
||||||
|
).copy(deviceLabel = deviceLabel)
|
||||||
|
}
|
||||||
|
ComposeWidgetPreview(state = batteryState, modifier = innerModifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (hasTransparency) {
|
if (hasTransparency) {
|
||||||
CheckerboardBackground {
|
CheckerboardBackground {
|
||||||
ComposeWidgetPreview(
|
previewContent(Modifier.padding(6.dp))
|
||||||
state = previewState,
|
|
||||||
modifier = Modifier.padding(6.dp),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ComposeWidgetPreview(state = previewState)
|
previewContent(Modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.darken.capod.main.ui.widget
|
package eu.darken.capod.main.ui.widget
|
||||||
|
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -16,6 +17,7 @@ import eu.darken.capod.profiles.core.DeviceProfile
|
|||||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||||
import eu.darken.capod.profiles.core.ProfileId
|
import eu.darken.capod.profiles.core.ProfileId
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -33,8 +35,15 @@ class WidgetConfigurationViewModel @Inject constructor(
|
|||||||
val widgetId: Int
|
val widgetId: Int
|
||||||
get() = savedStateHandle.get<Int>(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: AppWidgetManager.INVALID_APPWIDGET_ID
|
get() = savedStateHandle.get<Int>(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: AppWidgetManager.INVALID_APPWIDGET_ID
|
||||||
|
|
||||||
|
private val isAncWidget: Boolean = run {
|
||||||
|
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@run false
|
||||||
|
val info = appWidgetManager.getAppWidgetInfo(widgetId) ?: return@run false
|
||||||
|
val ancProvider = ComponentName(context, AncWidgetProvider::class.java)
|
||||||
|
info.provider == ancProvider
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
log(TAG) { "ViewModel init(widgetId=$widgetId)" }
|
log(TAG) { "ViewModel init(widgetId=$widgetId, isAncWidget=$isAncWidget)" }
|
||||||
|
|
||||||
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||||
log(TAG) { "Invalid widget ID" }
|
log(TAG) { "Invalid widget ID" }
|
||||||
@@ -52,14 +61,18 @@ class WidgetConfigurationViewModel @Inject constructor(
|
|||||||
|
|
||||||
private val currentTheme = MutableStateFlow(initialTheme)
|
private val currentTheme = MutableStateFlow(initialTheme)
|
||||||
|
|
||||||
|
private val visibleProfiles = deviceProfilesRepo.profiles.map { profiles ->
|
||||||
|
if (isAncWidget) profiles.filter { it.model.features.hasAncControl } else profiles
|
||||||
|
}
|
||||||
|
|
||||||
val state = combine(
|
val state = combine(
|
||||||
selectedProfile,
|
selectedProfile,
|
||||||
currentTheme,
|
currentTheme,
|
||||||
forceCustomMode,
|
forceCustomMode,
|
||||||
deviceProfilesRepo.profiles,
|
visibleProfiles,
|
||||||
upgradeRepo.upgradeInfo,
|
upgradeRepo.upgradeInfo,
|
||||||
) { selected, theme, forceCustom, profiles, upgradeInfo ->
|
) { selected, theme, forceCustom, profiles, upgradeInfo ->
|
||||||
log(TAG) { "state update: profile=$selected, theme=$theme, forceCustom=$forceCustom" }
|
log(TAG) { "state update: profile=$selected, theme=$theme, forceCustom=$forceCustom, profiles=${profiles.size}" }
|
||||||
|
|
||||||
val activePreset = if (forceCustom) null else WidgetTheme.matchPreset(theme)
|
val activePreset = if (forceCustom) null else WidgetTheme.matchPreset(theme)
|
||||||
State(
|
State(
|
||||||
@@ -69,6 +82,7 @@ class WidgetConfigurationViewModel @Inject constructor(
|
|||||||
theme = theme,
|
theme = theme,
|
||||||
activePreset = activePreset,
|
activePreset = activePreset,
|
||||||
isCustomMode = activePreset == null,
|
isCustomMode = activePreset == null,
|
||||||
|
isAncWidget = isAncWidget,
|
||||||
)
|
)
|
||||||
}.asLiveState()
|
}.asLiveState()
|
||||||
|
|
||||||
@@ -79,8 +93,9 @@ class WidgetConfigurationViewModel @Inject constructor(
|
|||||||
val theme: WidgetTheme = WidgetTheme.DEFAULT,
|
val theme: WidgetTheme = WidgetTheme.DEFAULT,
|
||||||
val activePreset: WidgetTheme.Preset? = WidgetTheme.Preset.MATERIAL_YOU,
|
val activePreset: WidgetTheme.Preset? = WidgetTheme.Preset.MATERIAL_YOU,
|
||||||
val isCustomMode: Boolean = false,
|
val isCustomMode: Boolean = false,
|
||||||
|
val isAncWidget: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val canConfirm: Boolean = selectedProfile != null
|
val canConfirm: Boolean = selectedProfile != null && profiles.any { it.id == selectedProfile }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectProfile(profileId: ProfileId) {
|
fun selectProfile(profileId: ProfileId) {
|
||||||
@@ -125,12 +140,6 @@ class WidgetConfigurationViewModel @Inject constructor(
|
|||||||
currentTheme.value = currentTheme.value.copy(backgroundAlpha = alpha.coerceIn(0, 255))
|
currentTheme.value = currentTheme.value.copy(backgroundAlpha = alpha.coerceIn(0, 255))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleDeviceLabel() {
|
|
||||||
val newValue = !currentTheme.value.showDeviceLabel
|
|
||||||
log(TAG, INFO) { "toggleDeviceLabel(showDeviceLabel=$newValue)" }
|
|
||||||
currentTheme.value = currentTheme.value.copy(showDeviceLabel = newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setShowDeviceLabel(show: Boolean) {
|
fun setShowDeviceLabel(show: Boolean) {
|
||||||
log(TAG, INFO) { "setShowDeviceLabel(show=$show)" }
|
log(TAG, INFO) { "setShowDeviceLabel(show=$show)" }
|
||||||
currentTheme.value = currentTheme.value.copy(showDeviceLabel = show)
|
currentTheme.value = currentTheme.value.copy(showDeviceLabel = show)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class WidgetManager @Inject constructor(
|
|||||||
suspend fun refreshWidgets() {
|
suspend fun refreshWidgets() {
|
||||||
log(TAG, VERBOSE) { "refreshWidgets()" }
|
log(TAG, VERBOSE) { "refreshWidgets()" }
|
||||||
BatteryGlanceWidget().updateAll(context)
|
BatteryGlanceWidget().updateAll(context)
|
||||||
|
AncGlanceWidget().updateAll(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M19,9l1.25,-2.75L23,5l-2.75,-1.25L19,1l-1.25,2.75L15,5l2.75,1.25zM19,15l-1.25,2.75L15,19l2.75,1.25L19,23l1.25,-2.75L23,19l-2.75,-1.25zM11.5,9.5L9,4 6.5,9.5 1,12l5.5,2.5L9,20l2.5,-5.5L17,12z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13H7v-2h10V13z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8H5v-2c0,-3.87 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h3c1.66,0 3,-1.34 3,-3v-7c0,-4.97 -4.03,-9 -9,-9z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M17,20c-0.29,0 -0.56,-0.06 -0.76,-0.15 -0.71,-0.37 -1.21,-0.88 -1.71,-2.38 -0.51,-1.56 -1.47,-2.29 -2.39,-3 -0.79,-0.61 -1.61,-1.24 -2.32,-2.53C9.29,10.98 9,9.93 9,9c0,-2.8 2.2,-5 5,-5s5,2.2 5,5h2c0,-3.93 -3.07,-7 -7,-7S7,5.07 7,9c0,1.26 0.38,2.65 1.07,3.9 0.91,1.65 1.98,2.48 2.85,3.15 0.81,0.62 1.39,1.07 1.71,2.05 0.6,1.82 1.37,2.84 2.73,3.55 0.51,0.23 1.07,0.35 1.64,0.35 2.21,0 4,-1.79 4,-4h-2c0,1.1 -0.9,2 -2,2zM7.64,2.64L6.22,1.22C4.23,3.21 3,5.96 3,9s1.23,5.79 3.22,7.78l1.41,-1.41C6.01,13.74 5,11.49 5,9s1.01,-4.74 2.64,-6.36zM11.5,9c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5 -1.12,-2.5 -2.5,-2.5 -2.5,1.12 -2.5,2.5z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
style="@style/PodWidget.Container"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_anc_off"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_anc_on"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_anc_transparency"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_anc_adaptive"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -173,6 +173,7 @@
|
|||||||
<string name="widget_config_transparency_label">Background transparency</string>
|
<string name="widget_config_transparency_label">Background transparency</string>
|
||||||
<string name="widget_config_show_device_label">Show device name</string>
|
<string name="widget_config_show_device_label">Show device name</string>
|
||||||
<string name="widget_config_reset_label">Reset to defaults</string>
|
<string name="widget_config_reset_label">Reset to defaults</string>
|
||||||
|
<string name="widget_config_preview_resize_hint">You can resize the widget after placing it on your hoem screen.</string>
|
||||||
<string name="widget_config_custom_label">Custom</string>
|
<string name="widget_config_custom_label">Custom</string>
|
||||||
<string name="widget_config_preset_material_you">Material You</string>
|
<string name="widget_config_preset_material_you">Material You</string>
|
||||||
<string name="widget_config_preset_dark">Dark</string>
|
<string name="widget_config_preset_dark">Dark</string>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:minWidth="40dp"
|
||||||
|
android:minHeight="40dp"
|
||||||
|
android:targetCellWidth="2"
|
||||||
|
android:targetCellHeight="2"
|
||||||
|
android:updatePeriodMillis="86400000"
|
||||||
|
android:description="@string/anc_widget_description"
|
||||||
|
android:previewLayout="@layout/anc_widget_preview_layout"
|
||||||
|
android:initialLayout="@layout/widget_loading_layout"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:widgetCategory="home_screen"
|
||||||
|
android:widgetFeatures="reconfigurable"
|
||||||
|
android:configure="eu.darken.capod.main.ui.widget.WidgetConfigurationActivity"
|
||||||
|
tools:targetApi="s" />
|
||||||
Reference in New Issue
Block a user