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" />
|
||||
</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
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
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 ->
|
||||
WidgetConfigurationScreen(
|
||||
state = currentState,
|
||||
showAapRequiredHint = currentState.isAncWidget,
|
||||
onSelectProfile = { profile -> vm.selectProfile(profile.id) },
|
||||
onSelectPreset = { preset -> vm.selectPreset(preset) },
|
||||
onEnterCustomMode = { bg, fg -> vm.enterCustomMode(bg, fg) },
|
||||
@@ -84,7 +85,7 @@ class WidgetConfigurationActivity : Activity2() {
|
||||
onReset = { vm.resetToDefaults() },
|
||||
onConfirm = {
|
||||
if (currentState.isPro) {
|
||||
confirmSelection()
|
||||
confirmSelection(currentState.isAncWidget)
|
||||
} else {
|
||||
startActivity(
|
||||
Intent(this@WidgetConfigurationActivity, MainActivity::class.java).apply {
|
||||
@@ -100,7 +101,7 @@ class WidgetConfigurationActivity : Activity2() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmSelection() {
|
||||
private fun confirmSelection(isAncWidget: Boolean) {
|
||||
vm.confirmSelection()
|
||||
|
||||
val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
|
||||
@@ -109,7 +110,11 @@ class WidgetConfigurationActivity : Activity2() {
|
||||
lifecycleScope.launch {
|
||||
val manager = GlanceAppWidgetManager(appContext)
|
||||
val glanceId = manager.getGlanceIdBy(widgetId)
|
||||
BatteryGlanceWidget().update(appContext, glanceId)
|
||||
if (isAncWidget) {
|
||||
AncGlanceWidget().update(appContext, glanceId)
|
||||
} else {
|
||||
BatteryGlanceWidget().update(appContext, glanceId)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ import eu.darken.capod.profiles.core.DeviceProfile
|
||||
@Composable
|
||||
fun WidgetConfigurationScreen(
|
||||
state: WidgetConfigurationViewModel.State,
|
||||
showAapRequiredHint: Boolean = false,
|
||||
onSelectProfile: (DeviceProfile) -> Unit,
|
||||
onSelectPreset: (WidgetTheme.Preset) -> Unit,
|
||||
onEnterCustomMode: (defaultBg: Int, defaultFg: Int) -> Unit,
|
||||
@@ -118,6 +119,26 @@ fun WidgetConfigurationScreen(
|
||||
.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(
|
||||
modifier = Modifier
|
||||
@@ -186,6 +207,17 @@ fun WidgetConfigurationScreen(
|
||||
WidgetConfigPreview(
|
||||
theme = state.theme,
|
||||
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))
|
||||
@@ -435,20 +467,12 @@ private fun ProfileSelectionItem(
|
||||
private fun WidgetConfigPreview(
|
||||
theme: WidgetTheme,
|
||||
deviceLabel: String?,
|
||||
isAncWidget: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
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(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
@@ -461,15 +485,39 @@ private fun WidgetConfigPreview(
|
||||
.padding(24.dp),
|
||||
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) {
|
||||
CheckerboardBackground {
|
||||
ComposeWidgetPreview(
|
||||
state = previewState,
|
||||
modifier = Modifier.padding(6.dp),
|
||||
)
|
||||
previewContent(Modifier.padding(6.dp))
|
||||
}
|
||||
} else {
|
||||
ComposeWidgetPreview(state = previewState)
|
||||
previewContent(Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.darken.capod.main.ui.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
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.ProfileId
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -33,8 +35,15 @@ class WidgetConfigurationViewModel @Inject constructor(
|
||||
val widgetId: Int
|
||||
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 {
|
||||
log(TAG) { "ViewModel init(widgetId=$widgetId)" }
|
||||
log(TAG) { "ViewModel init(widgetId=$widgetId, isAncWidget=$isAncWidget)" }
|
||||
|
||||
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
log(TAG) { "Invalid widget ID" }
|
||||
@@ -52,14 +61,18 @@ class WidgetConfigurationViewModel @Inject constructor(
|
||||
|
||||
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(
|
||||
selectedProfile,
|
||||
currentTheme,
|
||||
forceCustomMode,
|
||||
deviceProfilesRepo.profiles,
|
||||
visibleProfiles,
|
||||
upgradeRepo.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)
|
||||
State(
|
||||
@@ -69,6 +82,7 @@ class WidgetConfigurationViewModel @Inject constructor(
|
||||
theme = theme,
|
||||
activePreset = activePreset,
|
||||
isCustomMode = activePreset == null,
|
||||
isAncWidget = isAncWidget,
|
||||
)
|
||||
}.asLiveState()
|
||||
|
||||
@@ -79,8 +93,9 @@ class WidgetConfigurationViewModel @Inject constructor(
|
||||
val theme: WidgetTheme = WidgetTheme.DEFAULT,
|
||||
val activePreset: WidgetTheme.Preset? = WidgetTheme.Preset.MATERIAL_YOU,
|
||||
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) {
|
||||
@@ -125,12 +140,6 @@ class WidgetConfigurationViewModel @Inject constructor(
|
||||
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) {
|
||||
log(TAG, INFO) { "setShowDeviceLabel(show=$show)" }
|
||||
currentTheme.value = currentTheme.value.copy(showDeviceLabel = show)
|
||||
|
||||
@@ -18,6 +18,7 @@ class WidgetManager @Inject constructor(
|
||||
suspend fun refreshWidgets() {
|
||||
log(TAG, VERBOSE) { "refreshWidgets()" }
|
||||
BatteryGlanceWidget().updateAll(context)
|
||||
AncGlanceWidget().updateAll(context)
|
||||
}
|
||||
|
||||
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_show_device_label">Show device name</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_preset_material_you">Material You</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