mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 10:46:12 -04:00
feat(widget): Add theme customization to widget configuration
Add color presets, custom color picker, transparency slider, and device label toggle to the widget configuration screen. Widget preview updates in real-time. Use profile label as device name in both preview and actual widget.
This commit is contained in:
@@ -36,6 +36,27 @@ inline fun <T1, T2, T3, R> combine(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "LongParameterList")
|
||||
inline fun <T1, T2, T3, T4, R> combine(
|
||||
flow: Flow<T1>,
|
||||
flow2: Flow<T2>,
|
||||
flow3: Flow<T3>,
|
||||
flow4: Flow<T4>,
|
||||
crossinline transform: suspend (T1, T2, T3, T4) -> R
|
||||
): Flow<R> = kotlinx.coroutines.flow.combine(
|
||||
flow,
|
||||
flow2,
|
||||
flow3,
|
||||
flow4,
|
||||
) { args: Array<*> ->
|
||||
transform(
|
||||
args[0] as T1,
|
||||
args[1] as T2,
|
||||
args[2] as T3,
|
||||
args[3] as T4,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "LongParameterList")
|
||||
inline fun <T1, T2, T3, T4, T5, R> combine(
|
||||
flow: Flow<T1>,
|
||||
|
||||
@@ -3,10 +3,32 @@ package eu.darken.capod.main.ui.widget
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.Shader
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.InputFilter
|
||||
import android.text.TextWatcher
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.GridLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.chip.Chip
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
@@ -30,14 +52,15 @@ class WidgetConfigurationActivity : Activity2() {
|
||||
|
||||
private var widgetId: Int = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
|
||||
private var isUpdatingHexFromCode = false
|
||||
private val checkerboardDrawable: BitmapDrawable by lazy { createCheckerboardDrawable() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
// Set result to CANCELED in case user backs out
|
||||
setResult(RESULT_CANCELED)
|
||||
|
||||
// Get widget ID from intent
|
||||
widgetId = intent.getIntExtra(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
@@ -45,7 +68,6 @@ class WidgetConfigurationActivity : Activity2() {
|
||||
|
||||
log(TAG) { "onCreate(widgetId=$widgetId)" }
|
||||
|
||||
// If widget ID is invalid, finish
|
||||
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
log(TAG) { "Invalid widget ID, finishing" }
|
||||
finish()
|
||||
@@ -61,6 +83,15 @@ class WidgetConfigurationActivity : Activity2() {
|
||||
|
||||
ui.profilesRecycler.adapter = profileAdapter
|
||||
|
||||
setupPreviewContainer()
|
||||
setupPresetChips()
|
||||
setupColorSwatches(ui.bgColorGrid, isBg = true)
|
||||
setupColorSwatches(ui.fgColorGrid, isBg = false)
|
||||
setupHexInput()
|
||||
setupTransparencySlider()
|
||||
setupShowDeviceLabelSwitch()
|
||||
setupResetButton()
|
||||
|
||||
ui.cancelButton.setOnClickListener {
|
||||
log(TAG) { "Cancel clicked" }
|
||||
finish()
|
||||
@@ -95,6 +126,310 @@ class WidgetConfigurationActivity : Activity2() {
|
||||
ui.confirmButton.text = getString(R.string.general_upgrade_action)
|
||||
ui.confirmButton.isEnabled = true
|
||||
}
|
||||
|
||||
updatePresetChipSelection(state.activePreset)
|
||||
updateCustomSectionsVisibility(state.isCustomMode)
|
||||
updateSwatchSelection(ui.bgColorGrid, state.theme.backgroundColor)
|
||||
updateSwatchSelection(ui.fgColorGrid, state.theme.foregroundColor)
|
||||
updateHexInputs(state.theme)
|
||||
|
||||
// Disable transparency slider when using Material You (no custom bg to apply alpha to)
|
||||
val hasCustomBg = state.theme.backgroundColor != null
|
||||
ui.transparencySlider.isEnabled = hasCustomBg
|
||||
ui.transparencyLabel.alpha = if (hasCustomBg) 1.0f else 0.5f
|
||||
|
||||
val transparencyPercent = ((255 - state.theme.backgroundAlpha) / 255f * 100f)
|
||||
val displayPercent = (transparencyPercent / 5f).toInt() * 5
|
||||
if (!ui.transparencySlider.isPressed) {
|
||||
ui.transparencySlider.value = displayPercent.toFloat()
|
||||
}
|
||||
ui.transparencyLabel.text = getString(
|
||||
R.string.widget_config_transparency_label
|
||||
) + if (hasCustomBg && displayPercent > 0) " ($displayPercent%)" else ""
|
||||
|
||||
ui.showDeviceLabelSwitch.isChecked = state.theme.showDeviceLabel
|
||||
|
||||
val selectedProfileLabel = state.profiles.firstOrNull { it.id == state.selectedProfile }?.label
|
||||
val deviceLabel = ui.previewCard.findViewById<android.widget.TextView>(R.id.preview_device_label)
|
||||
deviceLabel?.text = selectedProfileLabel ?: ""
|
||||
|
||||
updatePreview(state.theme)
|
||||
}
|
||||
}
|
||||
|
||||
private val widgetThemeContext: Context by lazy {
|
||||
ContextThemeWrapper(this, com.google.android.material.R.style.Theme_Material3_DynamicColors_DayNight)
|
||||
}
|
||||
|
||||
private fun resolveWidgetThemeColor(attr: Int): Int {
|
||||
val typedArray = widgetThemeContext.theme.obtainStyledAttributes(intArrayOf(attr))
|
||||
val color = typedArray.getColor(0, Color.BLACK)
|
||||
typedArray.recycle()
|
||||
return color
|
||||
}
|
||||
|
||||
private fun setupPreviewContainer() {
|
||||
// Initial background set from XML, updated dynamically in updatePreview
|
||||
}
|
||||
|
||||
private fun updatePreview(theme: WidgetTheme) {
|
||||
val previewRoot = ui.previewCard.findViewById<View>(R.id.preview_widget_root)
|
||||
|
||||
// Background color applied to the inner view, matching the real widget
|
||||
val bgColor = theme.backgroundColor
|
||||
if (bgColor != null) {
|
||||
previewRoot.setBackgroundColor(WidgetTheme.applyAlpha(bgColor, theme.backgroundAlpha))
|
||||
} else {
|
||||
previewRoot.setBackgroundColor(resolveWidgetThemeColor(android.R.attr.colorBackground))
|
||||
}
|
||||
|
||||
// Show checkerboard behind preview only when there's actual transparency to visualize
|
||||
val hasTransparency = bgColor != null && theme.backgroundAlpha < 255
|
||||
ui.previewContainer.background = if (hasTransparency) {
|
||||
checkerboardDrawable
|
||||
} else {
|
||||
AppCompatResources.getDrawable(this, R.drawable.widget_preview_checkerboard)
|
||||
}
|
||||
|
||||
// Foreground (text + icon colors)
|
||||
val fgColor = theme.foregroundColor
|
||||
val defaultTextColor = resolveWidgetThemeColor(android.R.attr.textColorPrimary)
|
||||
val defaultIconColor = resolveWidgetThemeColor(android.R.attr.colorAccent)
|
||||
|
||||
val textViews = listOf(
|
||||
R.id.preview_left_label,
|
||||
R.id.preview_right_label,
|
||||
R.id.preview_case_label,
|
||||
R.id.preview_device_label,
|
||||
)
|
||||
val iconViews = listOf(
|
||||
R.id.preview_left_icon,
|
||||
R.id.preview_right_icon,
|
||||
R.id.preview_case_icon,
|
||||
)
|
||||
|
||||
for (id in textViews) {
|
||||
val tv = ui.previewCard.findViewById<android.widget.TextView>(id) ?: continue
|
||||
tv.setTextColor(fgColor ?: defaultTextColor)
|
||||
}
|
||||
|
||||
for (id in iconViews) {
|
||||
val iv = ui.previewCard.findViewById<ImageView>(id) ?: continue
|
||||
if (fgColor != null) {
|
||||
iv.setColorFilter(fgColor, PorterDuff.Mode.SRC_IN)
|
||||
} else {
|
||||
iv.setColorFilter(defaultIconColor, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
|
||||
// Device label visibility
|
||||
val deviceLabel = ui.previewCard.findViewById<View>(R.id.preview_device_label)
|
||||
deviceLabel?.isVisible = theme.showDeviceLabel
|
||||
}
|
||||
|
||||
private fun createCheckerboardDrawable(): BitmapDrawable {
|
||||
val cellSize = (8 * resources.displayMetrics.density).toInt()
|
||||
val bitmap = createBitmap(cellSize * 2, cellSize * 2)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint()
|
||||
// Light squares
|
||||
paint.color = 0xFFE8E8E8.toInt()
|
||||
canvas.drawRect(0f, 0f, (cellSize * 2).toFloat(), (cellSize * 2).toFloat(), paint)
|
||||
// Dark squares
|
||||
paint.color = 0xFFD0D0D0.toInt()
|
||||
canvas.drawRect(0f, 0f, cellSize.toFloat(), cellSize.toFloat(), paint)
|
||||
canvas.drawRect(
|
||||
cellSize.toFloat(),
|
||||
cellSize.toFloat(),
|
||||
(cellSize * 2).toFloat(),
|
||||
(cellSize * 2).toFloat(),
|
||||
paint
|
||||
)
|
||||
|
||||
return bitmap.toDrawable(resources).apply {
|
||||
tileModeX = Shader.TileMode.REPEAT
|
||||
tileModeY = Shader.TileMode.REPEAT
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupPresetChips() {
|
||||
val presetNames = mapOf(
|
||||
WidgetTheme.Preset.MATERIAL_YOU to getString(R.string.widget_config_preset_material_you),
|
||||
WidgetTheme.Preset.CLASSIC_DARK to getString(R.string.widget_config_preset_dark),
|
||||
WidgetTheme.Preset.CLASSIC_LIGHT to getString(R.string.widget_config_preset_light),
|
||||
WidgetTheme.Preset.BLUE to getString(R.string.widget_config_preset_blue),
|
||||
WidgetTheme.Preset.GREEN to getString(R.string.widget_config_preset_green),
|
||||
WidgetTheme.Preset.RED to getString(R.string.widget_config_preset_red),
|
||||
)
|
||||
|
||||
for (preset in WidgetTheme.Preset.entries) {
|
||||
val chip = Chip(this).apply {
|
||||
text = presetNames[preset] ?: preset.name
|
||||
isCheckable = true
|
||||
tag = preset
|
||||
setOnClickListener { vm.selectPreset(preset) }
|
||||
}
|
||||
ui.presetChipGroup.addView(chip)
|
||||
}
|
||||
|
||||
// Custom chip
|
||||
val customChip = Chip(this).apply {
|
||||
text = getString(R.string.widget_config_custom_label)
|
||||
isCheckable = true
|
||||
tag = CUSTOM_CHIP_TAG
|
||||
setOnClickListener {
|
||||
val defaultBg = resolveWidgetThemeColor(android.R.attr.colorBackground)
|
||||
val defaultFg = resolveWidgetThemeColor(android.R.attr.textColorPrimary)
|
||||
vm.enterCustomMode(defaultBg, defaultFg)
|
||||
}
|
||||
}
|
||||
ui.presetChipGroup.addView(customChip)
|
||||
}
|
||||
|
||||
private fun updatePresetChipSelection(activePreset: WidgetTheme.Preset?) {
|
||||
for (i in 0 until ui.presetChipGroup.childCount) {
|
||||
val chip = ui.presetChipGroup.getChildAt(i) as? Chip ?: continue
|
||||
chip.isChecked = if (activePreset != null) {
|
||||
chip.tag == activePreset
|
||||
} else {
|
||||
chip.tag == CUSTOM_CHIP_TAG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCustomSectionsVisibility(isCustomMode: Boolean) {
|
||||
val visibility = if (isCustomMode) View.VISIBLE else View.GONE
|
||||
ui.bgColorLabel.visibility = visibility
|
||||
ui.bgColorGrid.visibility = visibility
|
||||
ui.bgHexInputLayout.visibility = visibility
|
||||
ui.fgColorLabel.visibility = visibility
|
||||
ui.fgColorGrid.visibility = visibility
|
||||
ui.fgHexInputLayout.visibility = visibility
|
||||
}
|
||||
|
||||
private fun setupColorSwatches(grid: GridLayout, isBg: Boolean) {
|
||||
for (color in SWATCH_COLORS) {
|
||||
val itemView = LayoutInflater.from(this)
|
||||
.inflate(R.layout.widget_color_swatch_item, grid, false)
|
||||
|
||||
val swatchColor = itemView.findViewById<View>(R.id.swatch_color)
|
||||
val bgDrawable = swatchColor.background?.mutate() as? GradientDrawable ?: continue
|
||||
bgDrawable.setColor(color)
|
||||
swatchColor.background = bgDrawable
|
||||
|
||||
itemView.setOnClickListener {
|
||||
ui.bgHexInput.clearFocus()
|
||||
ui.fgHexInput.clearFocus()
|
||||
if (isBg) vm.setBackgroundColor(color) else vm.setForegroundColor(color)
|
||||
}
|
||||
|
||||
val params = GridLayout.LayoutParams(
|
||||
GridLayout.spec(GridLayout.UNDEFINED),
|
||||
GridLayout.spec(GridLayout.UNDEFINED, 1f),
|
||||
).apply {
|
||||
width = GridLayout.LayoutParams.WRAP_CONTENT
|
||||
height = GridLayout.LayoutParams.WRAP_CONTENT
|
||||
setGravity(Gravity.CENTER)
|
||||
}
|
||||
grid.addView(itemView, params)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSwatchSelection(grid: GridLayout, selectedColor: Int?) {
|
||||
for (i in 0 until grid.childCount) {
|
||||
val itemView = grid.getChildAt(i) as? FrameLayout ?: continue
|
||||
val color = SWATCH_COLORS.getOrNull(i) ?: continue
|
||||
val isSelected =
|
||||
selectedColor != null && (selectedColor or 0xFF000000.toInt()) == (color or 0xFF000000.toInt())
|
||||
|
||||
itemView.findViewById<View>(R.id.swatch_selected_ring)?.isVisible = isSelected
|
||||
itemView.findViewById<ImageView>(R.id.swatch_check)?.apply {
|
||||
isVisible = isSelected
|
||||
if (isSelected) {
|
||||
val checkColor = WidgetTheme.bestContrastForeground(color)
|
||||
setColorFilter(checkColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupHexInput() {
|
||||
val hexFilter = InputFilter { source, _, _, _, _, _ ->
|
||||
val filtered = source.toString().uppercase().filter { it in "0123456789ABCDEF" }
|
||||
if (filtered == source.toString()) null else filtered
|
||||
}
|
||||
|
||||
ui.bgHexInput.filters = arrayOf(hexFilter, InputFilter.LengthFilter(6))
|
||||
ui.fgHexInput.filters = arrayOf(hexFilter, InputFilter.LengthFilter(6))
|
||||
|
||||
ui.bgHexInput.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
if (isUpdatingHexFromCode) return
|
||||
val hex = s?.toString() ?: return
|
||||
if (hex.length == 6) {
|
||||
try {
|
||||
val color = "#$hex".toColorInt()
|
||||
vm.setBackgroundColor(color)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ui.fgHexInput.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
if (isUpdatingHexFromCode) return
|
||||
val hex = s?.toString() ?: return
|
||||
if (hex.length == 6) {
|
||||
try {
|
||||
val color = "#$hex".toColorInt()
|
||||
vm.setForegroundColor(color)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateHexInputs(theme: WidgetTheme) {
|
||||
isUpdatingHexFromCode = true
|
||||
try {
|
||||
val bgHex = theme.backgroundColor?.let { String.format("%06X", 0xFFFFFF and it) } ?: ""
|
||||
if (ui.bgHexInput.text.toString() != bgHex && !ui.bgHexInput.hasFocus()) {
|
||||
ui.bgHexInput.setText(bgHex)
|
||||
}
|
||||
|
||||
val fgHex = theme.foregroundColor?.let { String.format("%06X", 0xFFFFFF and it) } ?: ""
|
||||
if (ui.fgHexInput.text.toString() != fgHex && !ui.fgHexInput.hasFocus()) {
|
||||
ui.fgHexInput.setText(fgHex)
|
||||
}
|
||||
} finally {
|
||||
isUpdatingHexFromCode = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTransparencySlider() {
|
||||
ui.transparencySlider.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
val alpha = 255 - (value / 100f * 255f).toInt()
|
||||
vm.setBackgroundAlpha(alpha)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupShowDeviceLabelSwitch() {
|
||||
ui.showDeviceLabelSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
vm.setShowDeviceLabel(isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupResetButton() {
|
||||
ui.resetButton.setOnClickListener {
|
||||
vm.resetToDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +448,37 @@ class WidgetConfigurationActivity : Activity2() {
|
||||
)
|
||||
|
||||
finish()
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Widget", "ConfigurationActivity")
|
||||
private const val CUSTOM_CHIP_TAG = "custom"
|
||||
|
||||
private val SWATCH_COLORS = intArrayOf(
|
||||
0xFFF44336.toInt(), // Red
|
||||
0xFFE91E63.toInt(), // Pink
|
||||
0xFF9C27B0.toInt(), // Purple
|
||||
0xFF673AB7.toInt(), // Deep Purple
|
||||
0xFF3F51B5.toInt(), // Indigo
|
||||
0xFF2196F3.toInt(), // Blue
|
||||
0xFF03A9F4.toInt(), // Light Blue
|
||||
0xFF00BCD4.toInt(), // Cyan
|
||||
0xFF009688.toInt(), // Teal
|
||||
0xFF4CAF50.toInt(), // Green
|
||||
0xFF8BC34A.toInt(), // Light Green
|
||||
0xFFCDDC39.toInt(), // Lime
|
||||
0xFFFFEB3B.toInt(), // Yellow
|
||||
0xFFFFC107.toInt(), // Amber
|
||||
0xFFFF9800.toInt(), // Orange
|
||||
0xFFFF5722.toInt(), // Deep Orange
|
||||
0xFF795548.toInt(), // Brown
|
||||
0xFF9E9E9E.toInt(), // Grey
|
||||
0xFF607D8B.toInt(), // Blue Grey
|
||||
0xFFFFFFFF.toInt(), // White
|
||||
0xFF1E1E1E.toInt(), // Near Black
|
||||
0xFF37474F.toInt(), // Dark Blue Grey
|
||||
0xFF1B5E20.toInt(), // Dark Green
|
||||
0xFF0D47A1.toInt(), // Dark Blue
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package eu.darken.capod.main.ui.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
@@ -22,8 +24,11 @@ class WidgetConfigurationViewModel @Inject constructor(
|
||||
private val deviceProfilesRepo: DeviceProfilesRepo,
|
||||
private val widgetSettings: WidgetSettings,
|
||||
private val upgradeRepo: UpgradeRepo,
|
||||
@ApplicationContext private val context: Context,
|
||||
) : ViewModel3(dispatcherProvider) {
|
||||
|
||||
private val appWidgetManager by lazy { AppWidgetManager.getInstance(context) }
|
||||
|
||||
val widgetId: Int
|
||||
get() = savedStateHandle.get<Int>(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
|
||||
@@ -37,18 +42,32 @@ class WidgetConfigurationViewModel @Inject constructor(
|
||||
|
||||
private val selectedProfile = MutableStateFlow(widgetSettings.getWidgetProfile(widgetId))
|
||||
|
||||
val state = combine(
|
||||
private val initialTheme: WidgetTheme = run {
|
||||
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@run WidgetTheme.DEFAULT
|
||||
WidgetTheme.fromBundle(appWidgetManager.getAppWidgetOptions(widgetId))
|
||||
}
|
||||
|
||||
private val forceCustomMode = MutableStateFlow(WidgetTheme.matchPreset(initialTheme) == null)
|
||||
|
||||
private val currentTheme = MutableStateFlow(initialTheme)
|
||||
|
||||
val state = eu.darken.capod.common.flow.combine(
|
||||
selectedProfile,
|
||||
currentTheme,
|
||||
forceCustomMode,
|
||||
deviceProfilesRepo.profiles,
|
||||
upgradeRepo.upgradeInfo,
|
||||
) { selected, profiles, upgradeInfo ->
|
||||
log(TAG) { "loadProfiles()" }
|
||||
|
||||
) { selected, theme, forceCustom, profiles, upgradeInfo ->
|
||||
log(TAG) { "state update: profile=$selected, theme=$theme, forceCustom=$forceCustom" }
|
||||
|
||||
val activePreset = if (forceCustom) null else WidgetTheme.matchPreset(theme)
|
||||
State(
|
||||
profiles = profiles,
|
||||
isPro = upgradeInfo.isPro,
|
||||
selectedProfile = selected,
|
||||
theme = theme,
|
||||
activePreset = activePreset,
|
||||
isCustomMode = activePreset == null,
|
||||
)
|
||||
}.asLiveData2()
|
||||
|
||||
@@ -56,13 +75,70 @@ class WidgetConfigurationViewModel @Inject constructor(
|
||||
val profiles: List<DeviceProfile> = emptyList(),
|
||||
val selectedProfile: ProfileId? = null,
|
||||
val isPro: Boolean = false,
|
||||
val theme: WidgetTheme = WidgetTheme.DEFAULT,
|
||||
val activePreset: WidgetTheme.Preset? = WidgetTheme.Preset.MATERIAL_YOU,
|
||||
val isCustomMode: Boolean = false,
|
||||
) {
|
||||
val canConfirm: Boolean = selectedProfile != null
|
||||
}
|
||||
|
||||
fun selectProfile(profileId: ProfileId) {
|
||||
log(TAG) { "selectProfile(profileId=$profileId)" }
|
||||
selectedProfile.value = profileId
|
||||
selectedProfile.value = profileId
|
||||
}
|
||||
|
||||
fun selectPreset(preset: WidgetTheme.Preset) {
|
||||
log(TAG) { "selectPreset(preset=$preset)" }
|
||||
forceCustomMode.value = false
|
||||
currentTheme.value = WidgetTheme(
|
||||
backgroundColor = preset.presetBg,
|
||||
foregroundColor = preset.presetFg,
|
||||
backgroundAlpha = currentTheme.value.backgroundAlpha,
|
||||
showDeviceLabel = currentTheme.value.showDeviceLabel,
|
||||
)
|
||||
}
|
||||
|
||||
fun enterCustomMode(resolvedBg: Int, resolvedFg: Int) {
|
||||
log(TAG) { "enterCustomMode(resolvedBg=${String.format("#%06X", 0xFFFFFF and resolvedBg)}, resolvedFg=${String.format("#%06X", 0xFFFFFF and resolvedFg)})" }
|
||||
forceCustomMode.value = true
|
||||
// Populate null colors with the currently displayed values so the user has a starting point
|
||||
val theme = currentTheme.value
|
||||
currentTheme.value = theme.copy(
|
||||
backgroundColor = theme.backgroundColor ?: (resolvedBg or 0xFF000000.toInt()),
|
||||
foregroundColor = theme.foregroundColor ?: (resolvedFg or 0xFF000000.toInt()),
|
||||
)
|
||||
}
|
||||
|
||||
fun setBackgroundColor(color: Int) {
|
||||
log(TAG) { "setBackgroundColor(color=${String.format("#%06X", 0xFFFFFF and color)})" }
|
||||
currentTheme.value = currentTheme.value.copy(backgroundColor = color or 0xFF000000.toInt())
|
||||
}
|
||||
|
||||
fun setForegroundColor(color: Int) {
|
||||
log(TAG) { "setForegroundColor(color=${String.format("#%06X", 0xFFFFFF and color)})" }
|
||||
currentTheme.value = currentTheme.value.copy(foregroundColor = color or 0xFF000000.toInt())
|
||||
}
|
||||
|
||||
fun setBackgroundAlpha(alpha: Int) {
|
||||
log(TAG) { "setBackgroundAlpha(alpha=$alpha)" }
|
||||
currentTheme.value = currentTheme.value.copy(backgroundAlpha = alpha.coerceIn(0, 255))
|
||||
}
|
||||
|
||||
fun toggleDeviceLabel() {
|
||||
val newValue = !currentTheme.value.showDeviceLabel
|
||||
log(TAG) { "toggleDeviceLabel(showDeviceLabel=$newValue)" }
|
||||
currentTheme.value = currentTheme.value.copy(showDeviceLabel = newValue)
|
||||
}
|
||||
|
||||
fun setShowDeviceLabel(show: Boolean) {
|
||||
log(TAG) { "setShowDeviceLabel(show=$show)" }
|
||||
currentTheme.value = currentTheme.value.copy(showDeviceLabel = show)
|
||||
}
|
||||
|
||||
fun resetToDefaults() {
|
||||
log(TAG) { "resetToDefaults()" }
|
||||
forceCustomMode.value = false
|
||||
currentTheme.value = WidgetTheme.DEFAULT
|
||||
}
|
||||
|
||||
fun confirmSelection() {
|
||||
@@ -71,9 +147,16 @@ class WidgetConfigurationViewModel @Inject constructor(
|
||||
log(TAG) { "confirmSelection(widgetId=$widgetId, selectedProfile=$selectedProfile)" }
|
||||
widgetSettings.saveWidgetProfile(widgetId, selectedProfile)
|
||||
}
|
||||
|
||||
// Save theme to AppWidgetOptions bundle
|
||||
val theme = currentTheme.value
|
||||
log(TAG) { "confirmSelection: saving theme=$theme" }
|
||||
val options = appWidgetManager.getAppWidgetOptions(widgetId)
|
||||
theme.toBundle(options)
|
||||
appWidgetManager.updateAppWidgetOptions(widgetId, options)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Widget", "ConfigurationVM")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@ import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
@@ -32,8 +35,10 @@ import eu.darken.capod.pods.core.PodFactory
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import eu.darken.capod.profiles.core.ProfileId
|
||||
import finish2
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
@@ -49,6 +54,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
@Inject lateinit var podFactory: PodFactory
|
||||
@Inject lateinit var upgradeRepo: UpgradeRepo
|
||||
@Inject lateinit var widgetSettings: WidgetSettings
|
||||
@Inject lateinit var deviceProfilesRepo: DeviceProfilesRepo
|
||||
@AppScope @Inject lateinit var appScope: CoroutineScope
|
||||
|
||||
private fun executeAsync(
|
||||
@@ -136,9 +142,15 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
log(TAG) { "updateWidget(widgetId=$widgetId, profileId=$profileId options=$options)" }
|
||||
|
||||
val device: PodDevice? = profileId?.let { podMonitor.getDeviceForProfile(it) }
|
||||
val profileLabel: String? = profileId?.let { id ->
|
||||
deviceProfilesRepo.profiles.first().firstOrNull { it.id == id }?.label
|
||||
}
|
||||
|
||||
val theme = WidgetTheme.fromBundle(widgetManager.getAppWidgetOptions(widgetId))
|
||||
log(TAG, VERBOSE) { "updateWidget: theme=$theme" }
|
||||
|
||||
val layout = when {
|
||||
!upgradeRepo.isPro() -> createUpgradeRequiredLayout(context, widgetId)
|
||||
!upgradeRepo.isPro() -> createUpgradeRequiredLayout(context, widgetId, theme)
|
||||
device is DualPodDevice -> {
|
||||
val minWidth = widgetManager.getAppWidgetOptions(widgetId)
|
||||
.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
|
||||
@@ -154,19 +166,84 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
else -> R.layout.widget_pod_dual_wide_layout
|
||||
}
|
||||
|
||||
createDualPodLayout(context, device, layout, widgetId)
|
||||
createDualPodLayout(context, device, layout, widgetId, theme, profileLabel)
|
||||
}
|
||||
|
||||
device is SinglePodDevice -> createSinglePodLayout(context, device, widgetId)
|
||||
device is PodDevice -> createUnknownPodLayout(context, device, widgetId)
|
||||
else -> createNoDeviceLayout(context, profileId != null, widgetId)
|
||||
device is SinglePodDevice -> createSinglePodLayout(context, device, widgetId, theme, profileLabel)
|
||||
device is PodDevice -> createUnknownPodLayout(context, device, widgetId, theme)
|
||||
else -> createNoDeviceLayout(context, profileId != null, widgetId, theme)
|
||||
}
|
||||
widgetManager.updateAppWidget(widgetId, layout)
|
||||
}
|
||||
|
||||
private fun applyThemeColors(
|
||||
context: Context,
|
||||
views: RemoteViews,
|
||||
theme: WidgetTheme,
|
||||
textViewIds: List<Int>,
|
||||
iconViewIds: List<Int>,
|
||||
hasDeviceLabel: Boolean,
|
||||
) {
|
||||
// Always explicitly set background color to ensure previous custom colors are overwritten
|
||||
val bgColor = theme.backgroundColor
|
||||
if (bgColor != null) {
|
||||
val colorWithAlpha = WidgetTheme.applyAlpha(bgColor, theme.backgroundAlpha)
|
||||
views.setInt(R.id.widget_root, "setBackgroundColor", colorWithAlpha)
|
||||
} else {
|
||||
// Reset to theme default — resolve ?android:attr/colorBackground
|
||||
val defaultBg = resolveThemeColor(context, android.R.attr.colorBackground)
|
||||
views.setInt(R.id.widget_root, "setBackgroundColor", defaultBg)
|
||||
}
|
||||
|
||||
// Always explicitly set text/icon colors
|
||||
val fgColor = theme.foregroundColor
|
||||
if (fgColor != null) {
|
||||
for (textViewId in textViewIds) {
|
||||
views.setTextColor(textViewId, fgColor)
|
||||
}
|
||||
for (iconViewId in iconViewIds) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
views.setColorStateList(iconViewId, "setImageTintList", ColorStateList.valueOf(fgColor))
|
||||
} else {
|
||||
views.setInt(iconViewId, "setColorFilter", fgColor)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Reset to theme defaults
|
||||
val defaultTextColor = resolveThemeColor(context, android.R.attr.textColorPrimary)
|
||||
val defaultIconColor = resolveThemeColor(context, android.R.attr.colorAccent)
|
||||
for (textViewId in textViewIds) {
|
||||
views.setTextColor(textViewId, defaultTextColor)
|
||||
}
|
||||
for (iconViewId in iconViewIds) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
views.setColorStateList(iconViewId, "setImageTintList", ColorStateList.valueOf(defaultIconColor))
|
||||
} else {
|
||||
views.setInt(iconViewId, "setColorFilter", defaultIconColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDeviceLabel) {
|
||||
views.setViewVisibility(
|
||||
R.id.headphones_label,
|
||||
if (theme.showDeviceLabel) View.VISIBLE else View.GONE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveThemeColor(context: Context, attr: Int): Int {
|
||||
val themedContext = ContextThemeWrapper(context, com.google.android.material.R.style.Theme_Material3_DynamicColors_DayNight)
|
||||
val typedArray = themedContext.theme.obtainStyledAttributes(intArrayOf(attr))
|
||||
val color = typedArray.getColor(0, android.graphics.Color.BLACK)
|
||||
typedArray.recycle()
|
||||
return color
|
||||
}
|
||||
|
||||
private suspend fun createUpgradeRequiredLayout(
|
||||
context: Context,
|
||||
widgetId: Int
|
||||
widgetId: Int,
|
||||
theme: WidgetTheme,
|
||||
) = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
|
||||
log(TAG, VERBOSE) { "createUpgradeRequiredLayout(context=$context)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
@@ -181,12 +258,22 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
setTextViewText(R.id.primary, context.getString(R.string.upgrade_capod_label))
|
||||
setTextViewText(R.id.secondary, context.getString(R.string.upgrade_capod_description))
|
||||
setViewVisibility(R.id.secondary, View.VISIBLE)
|
||||
|
||||
applyThemeColors(
|
||||
context = context,
|
||||
views = this,
|
||||
theme = theme,
|
||||
textViewIds = listOf(R.id.primary, R.id.secondary),
|
||||
iconViewIds = emptyList(),
|
||||
hasDeviceLabel = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createUnknownPodLayout(
|
||||
context: Context,
|
||||
podDevice: PodDevice,
|
||||
widgetId: Int
|
||||
widgetId: Int,
|
||||
theme: WidgetTheme,
|
||||
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
|
||||
log(TAG, VERBOSE) { "createUnknownPodLayout(context=$context, podDevice=$podDevice)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
@@ -199,12 +286,22 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||
|
||||
setTextViewText(R.id.primary, context.getString(R.string.pods_unknown_label))
|
||||
|
||||
applyThemeColors(
|
||||
context = context,
|
||||
views = this,
|
||||
theme = theme,
|
||||
textViewIds = listOf(R.id.primary),
|
||||
iconViewIds = emptyList(),
|
||||
hasDeviceLabel = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNoDeviceLayout(
|
||||
context: Context,
|
||||
hasConfiguredProfile: Boolean = false,
|
||||
widgetId: Int
|
||||
widgetId: Int,
|
||||
theme: WidgetTheme,
|
||||
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
|
||||
log(TAG, VERBOSE) { "createNoDeviceLayout(context=$context, hasConfiguredProfile=$hasConfiguredProfile)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
@@ -222,15 +319,26 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
R.string.overview_nomaindevice_label
|
||||
}
|
||||
setTextViewText(R.id.primary, context.getString(messageRes))
|
||||
|
||||
applyThemeColors(
|
||||
context = context,
|
||||
views = this,
|
||||
theme = theme,
|
||||
textViewIds = listOf(R.id.primary),
|
||||
iconViewIds = emptyList(),
|
||||
hasDeviceLabel = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createDualPodLayout(
|
||||
context: Context,
|
||||
podDevice: DualPodDevice,
|
||||
@LayoutRes layout: Int,
|
||||
widgetId: Int
|
||||
widgetId: Int,
|
||||
theme: WidgetTheme,
|
||||
profileLabel: String?,
|
||||
): RemoteViews = RemoteViews(context.packageName, layout).apply {
|
||||
log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice), layout=${layout}" }
|
||||
log(TAG, VERBOSE) { "createDualPodLayout(context=$context, podDevice=$podDevice), layout=${layout}" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
widgetId,
|
||||
@@ -240,7 +348,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
|
||||
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||
|
||||
setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
|
||||
setTextViewText(R.id.headphones_label, profileLabel ?: podDevice.getLabel(context))
|
||||
|
||||
// Left
|
||||
val leftPercent = podDevice.batteryLeftPodPercent
|
||||
@@ -276,12 +384,37 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
R.id.pod_right_ear,
|
||||
if (podDevice is HasEarDetectionDual && podDevice.isRightPodInEar) View.VISIBLE else View.GONE
|
||||
)
|
||||
|
||||
applyThemeColors(
|
||||
context = context,
|
||||
views = this,
|
||||
theme = theme,
|
||||
textViewIds = listOf(
|
||||
R.id.headphones_label,
|
||||
R.id.pod_left_label,
|
||||
R.id.pod_right_label,
|
||||
R.id.pod_case_label,
|
||||
),
|
||||
iconViewIds = listOf(
|
||||
R.id.pod_left_icon,
|
||||
R.id.pod_left_charging,
|
||||
R.id.pod_left_ear,
|
||||
R.id.pod_case_icon,
|
||||
R.id.pod_case_charging,
|
||||
R.id.pod_right_icon,
|
||||
R.id.pod_right_charging,
|
||||
R.id.pod_right_ear,
|
||||
),
|
||||
hasDeviceLabel = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createSinglePodLayout(
|
||||
context: Context,
|
||||
podDevice: SinglePodDevice,
|
||||
widgetId: Int
|
||||
widgetId: Int,
|
||||
theme: WidgetTheme,
|
||||
profileLabel: String?,
|
||||
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_pod_single_layout).apply {
|
||||
log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
@@ -294,7 +427,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||
|
||||
val headsetPercent = podDevice.batteryHeadsetPercent
|
||||
setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
|
||||
setTextViewText(R.id.headphones_label, profileLabel ?: podDevice.getLabel(context))
|
||||
setImageViewResource(R.id.headphones_icon, podDevice.iconRes)
|
||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(headsetPercent))
|
||||
setTextViewText(R.id.headphones_battery_label, formatBatteryPercent(context, headsetPercent))
|
||||
@@ -308,6 +441,23 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
R.id.headphones_charging,
|
||||
if (podDevice is HasChargeDetectionDual && podDevice.isHeadsetBeingCharged) View.VISIBLE else View.GONE
|
||||
)
|
||||
|
||||
applyThemeColors(
|
||||
context = context,
|
||||
views = this,
|
||||
theme = theme,
|
||||
textViewIds = listOf(
|
||||
R.id.headphones_label,
|
||||
R.id.headphones_battery_label,
|
||||
),
|
||||
iconViewIds = listOf(
|
||||
R.id.headphones_icon,
|
||||
R.id.headphones_battery_icon,
|
||||
R.id.headphones_charging,
|
||||
R.id.headphones_worn,
|
||||
),
|
||||
hasDeviceLabel = true,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -321,4 +471,4 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package eu.darken.capod.main.ui.widget
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.graphics.ColorUtils
|
||||
|
||||
data class WidgetTheme(
|
||||
@ColorInt val backgroundColor: Int? = null,
|
||||
@ColorInt val foregroundColor: Int? = null,
|
||||
val backgroundAlpha: Int = 255,
|
||||
val showDeviceLabel: Boolean = true,
|
||||
) {
|
||||
|
||||
enum class Preset(
|
||||
@ColorInt val presetBg: Int?,
|
||||
@ColorInt val presetFg: Int?,
|
||||
) {
|
||||
MATERIAL_YOU(null, null),
|
||||
CLASSIC_DARK(0xFF1E1E1E.toInt(), 0xFFFFFFFF.toInt()),
|
||||
CLASSIC_LIGHT(0xFFFFFFFF.toInt(), 0xFF1E1E1E.toInt()),
|
||||
BLUE(0xFF1565C0.toInt(), 0xFFFFFFFF.toInt()),
|
||||
GREEN(0xFF2E7D32.toInt(), 0xFFFFFFFF.toInt()),
|
||||
RED(0xFFC62828.toInt(), 0xFFFFFFFF.toInt()),
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_THEME_MODE = "capod_theme_mode"
|
||||
private const val KEY_CUSTOM_BG = "capod_custom_bg"
|
||||
private const val KEY_CUSTOM_FG = "capod_custom_fg"
|
||||
private const val KEY_BG_ALPHA = "capod_bg_alpha"
|
||||
private const val KEY_SHOW_LABEL = "capod_show_label"
|
||||
|
||||
private const val MODE_MATERIAL_YOU = "material_you"
|
||||
private const val MODE_CUSTOM = "custom"
|
||||
|
||||
val DEFAULT = WidgetTheme()
|
||||
|
||||
fun fromBundle(bundle: Bundle): WidgetTheme {
|
||||
val mode = bundle.getString(KEY_THEME_MODE, MODE_MATERIAL_YOU)
|
||||
val showLabel = bundle.getBoolean(KEY_SHOW_LABEL, true)
|
||||
val alpha = bundle.getInt(KEY_BG_ALPHA, 255).coerceIn(0, 255)
|
||||
return if (mode == MODE_CUSTOM) {
|
||||
WidgetTheme(
|
||||
backgroundColor = if (bundle.containsKey(KEY_CUSTOM_BG)) bundle.getInt(KEY_CUSTOM_BG) else null,
|
||||
foregroundColor = if (bundle.containsKey(KEY_CUSTOM_FG)) bundle.getInt(KEY_CUSTOM_FG) else null,
|
||||
backgroundAlpha = alpha,
|
||||
showDeviceLabel = showLabel,
|
||||
)
|
||||
} else {
|
||||
WidgetTheme(
|
||||
backgroundAlpha = alpha,
|
||||
showDeviceLabel = showLabel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun applyAlpha(@ColorInt color: Int, alpha: Int): Int {
|
||||
val clampedAlpha = alpha.coerceIn(0, 255)
|
||||
return Color.argb(clampedAlpha, Color.red(color), Color.green(color), Color.blue(color))
|
||||
}
|
||||
|
||||
fun bestContrastForeground(@ColorInt bgColor: Int): Int {
|
||||
val whiteContrast = ColorUtils.calculateContrast(Color.WHITE, bgColor)
|
||||
val blackContrast = ColorUtils.calculateContrast(Color.BLACK, bgColor)
|
||||
return if (whiteContrast >= blackContrast) Color.WHITE else Color.BLACK
|
||||
}
|
||||
|
||||
fun matchPreset(theme: WidgetTheme): Preset? = Preset.entries.firstOrNull { preset ->
|
||||
preset.presetBg == theme.backgroundColor && preset.presetFg == theme.foregroundColor && theme.backgroundAlpha == 255
|
||||
}
|
||||
}
|
||||
|
||||
fun toBundle(bundle: Bundle) {
|
||||
if (backgroundColor == null && foregroundColor == null) {
|
||||
bundle.putString(KEY_THEME_MODE, MODE_MATERIAL_YOU)
|
||||
bundle.remove(KEY_CUSTOM_BG)
|
||||
bundle.remove(KEY_CUSTOM_FG)
|
||||
} else {
|
||||
bundle.putString(KEY_THEME_MODE, MODE_CUSTOM)
|
||||
if (backgroundColor != null) {
|
||||
bundle.putInt(KEY_CUSTOM_BG, backgroundColor)
|
||||
} else {
|
||||
bundle.remove(KEY_CUSTOM_BG)
|
||||
}
|
||||
if (foregroundColor != null) {
|
||||
bundle.putInt(KEY_CUSTOM_FG, foregroundColor)
|
||||
} else {
|
||||
bundle.remove(KEY_CUSTOM_FG)
|
||||
}
|
||||
}
|
||||
bundle.putInt(KEY_BG_ALPHA, backgroundAlpha.coerceIn(0, 255))
|
||||
bundle.putBoolean(KEY_SHOW_LABEL, showDeviceLabel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#FFFFFF" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#20000000" />
|
||||
</shape>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<stroke
|
||||
android:width="3dp"
|
||||
android:color="?colorPrimary" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="16dp" />
|
||||
<solid android:color="@android:color/transparent" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="16dp" />
|
||||
<solid android:color="?colorSurfaceVariant" />
|
||||
</shape>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_margin="4dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/swatch_color"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/widget_color_swatch_bg" />
|
||||
|
||||
<View
|
||||
android:id="@+id/swatch_selected_ring"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/widget_color_swatch_selected_ring"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/swatch_check"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_baseline_check_24"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,100 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/preview_widget_root"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="160dp"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/preview_left_icon"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/devic_airpods_gen1_left"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/preview_left_label"
|
||||
style="@style/PodInfoItemText.Notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="80%"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/preview_right_icon"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/devic_airpods_gen1_right"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/preview_right_label"
|
||||
style="@style/PodInfoItemText.Notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="95%"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/preview_case_icon"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/devic_airpods_gen1_case"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/preview_case_label"
|
||||
style="@style/PodInfoItemText.Notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="100%"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/preview_device_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
tools:text="AirPods Pro"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -6,49 +6,267 @@
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/content_container"
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
android:fillViewport="true">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
<LinearLayout
|
||||
android:id="@+id/content_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/widget_configuration_title"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleLarge" />
|
||||
android:layout_marginTop="24dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/widget_configuration_description"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
<!-- Profile selection -->
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:text="@string/widget_configuration_title"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleLarge" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/profiles_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/widget_configuration_profile_item" />
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:text="@string/widget_configuration_description"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:textColor="?colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/profiles_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/widget_configuration_profile_item" />
|
||||
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginVertical="16dp" />
|
||||
|
||||
<!-- Appearance section header + reset button -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/appearance_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/widget_config_appearance_label"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleLarge" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/reset_button"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/widget_config_reset_label" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Live preview with container -->
|
||||
<FrameLayout
|
||||
android:id="@+id/preview_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="@drawable/widget_preview_checkerboard"
|
||||
android:padding="24dp">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/preview_card"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/widget_preview_bg"
|
||||
android:clipToOutline="true">
|
||||
|
||||
<include layout="@layout/widget_config_preview" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Transparency slider — directly below preview for live feedback -->
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/transparency_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/widget_config_transparency_label"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?colorOnSurfaceVariant" />
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/transparency_slider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:value="0"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="100"
|
||||
android:stepSize="5" />
|
||||
|
||||
<!-- Show device name toggle -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="48dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/widget_config_show_device_label"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/show_device_label_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Preset chips -->
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/widget_config_preset_label"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?colorOnSurfaceVariant" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/preset_chip_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:selectionRequired="true"
|
||||
app:singleSelection="true" />
|
||||
|
||||
<!-- Background color (custom mode only) -->
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/bg_color_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/widget_config_background_color_label"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
|
||||
<GridLayout
|
||||
android:id="@+id/bg_color_grid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:columnCount="8"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/bg_hex_input_layout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="#"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/bg_hex_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textCapCharacters|textNoSuggestions"
|
||||
android:maxLength="7"
|
||||
android:singleLine="true" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Foreground color (custom mode only) -->
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/fg_color_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/widget_config_foreground_color_label"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
|
||||
<GridLayout
|
||||
android:id="@+id/fg_color_grid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:columnCount="8"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/fg_hex_input_layout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="#"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/fg_hex_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textCapCharacters|textNoSuggestions"
|
||||
android:maxLength="7"
|
||||
android:singleLine="true" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/pro_required_caption"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/common_feature_requires_pro_msg"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
android:text="@string/common_feature_requires_pro_msg"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?colorError"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@@ -56,15 +274,15 @@
|
||||
android:id="@+id/buttons_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel_button"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
@@ -79,4 +297,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/Widget.Material3.CardView.Outlined"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="4dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="2dp">
|
||||
app:cardCornerRadius="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -57,4 +57,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -122,6 +122,20 @@
|
||||
<string name="common_feature_requires_pro_msg">This feature requires CAPod Pro.</string>
|
||||
|
||||
<string name="widget_no_data_label">No data</string>
|
||||
<string name="widget_config_appearance_label">Appearance</string>
|
||||
<string name="widget_config_preset_label">Presets</string>
|
||||
<string name="widget_config_background_color_label">Background color</string>
|
||||
<string name="widget_config_foreground_color_label">Text & icon color</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_reset_label">Reset to defaults</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>
|
||||
<string name="widget_config_preset_light">Light</string>
|
||||
<string name="widget_config_preset_blue">Blue</string>
|
||||
<string name="widget_config_preset_green">Green</string>
|
||||
<string name="widget_config_preset_red">Red</string>
|
||||
<string name="settings_compat_indirectcallback_title">Indirect data delivery</string>
|
||||
<string name="settings_compat_indirectcallback_summary">Use an alternative method to receive BLE data from the system (broadcast instead of callback).</string>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user