Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ data class WidgetConfig(

// Behavior
val isShowShade: Boolean = true,
val timeout: Int = 5,
val timeout: Int = 0,

// Automation
val autoUpdate: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ data class CallModule(
@Serializable
data class AppThemeOverride(
@SerialName("highlight_color") val highlightColor: String? = null,
// [NEW] Added Shape and Padding overrides
@SerialName("use_app_colors") val useAppColors: Boolean? = null,
@SerialName("icon_shape_id") val iconShapeId: String? = null,
@SerialName("icon_padding_percent") val iconPaddingPercent: Int? = null,
// [NEW] Added Call Config override
@SerialName("call_config") val callConfig: CallModule? = null,
val actions: Map<String, ActionConfig>? = null,
val progress: ProgressModule? = null,
val navigation: NavigationModule? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,40 +81,50 @@ abstract class BaseTranslator(
// 1. App Specific Override (Highest Priority)
if (pkg != null) {
val override = theme.apps[pkg]
// A. Specific Color Override
val overrideColor = override?.highlightColor
if (!overrideColor.isNullOrEmpty()) {
return overrideColor
}

// B. App-Specific "Use App Colors"
// If explicit true -> extract. If explicit false -> skip extraction (fall to global).
if (override?.useAppColors == true) {
return getAppBrandColor(pkg) ?: theme.global.highlightColor ?: defaultHex
}
}

// 2. Global "Use App Colors" -> Extract from Icon
if (theme.global.useAppColors && pkg != null) {
// Check cache first
val cached = appColorCache[pkg]
if (cached != null) return cached

// Extract and Cache
val extracted = extractColorFromAppIcon(pkg)
if (extracted != null) {
appColorCache[pkg] = extracted
return extracted
}
// Only run if app override didn't explicitly disable it (useAppColors != false)
val appOverrideDisabled = theme.apps[pkg]?.useAppColors == false
if (theme.global.useAppColors && !appOverrideDisabled && pkg != null) {
return getAppBrandColor(pkg) ?: theme.global.highlightColor ?: defaultHex
}

// 3. Global Theme Highlight -> Default Fallback
return theme.global.highlightColor ?: defaultHex
}

private fun getAppBrandColor(pkg: String): String? {
// Check cache first
val cached = appColorCache[pkg]
if (cached != null) return cached

// Extract and Cache
val extracted = extractColorFromAppIcon(pkg)
if (extracted != null) {
appColorCache[pkg] = extracted
return extracted
}
return null
}

private fun extractColorFromAppIcon(pkg: String): String? {
return try {
val drawable = context.packageManager.getApplicationIcon(pkg)
// Use a scaled down bitmap for faster Palette generation
val bitmap = drawable.toBitmap(width = 128, height = 128)

// Clear filters to ensure we process the raw colors
val palette = Palette.from(bitmap).clearFilters().generate()

// Priority list for "brand" colors
val swatches = listOf(
palette.vibrantSwatch,
palette.darkVibrantSwatch,
Expand All @@ -123,9 +133,8 @@ abstract class BaseTranslator(
palette.mutedSwatch
)

// Find first valid swatch that is NOT Grayscale (White/Black/Grey)
val bestSwatch = swatches.firstOrNull { it != null && !isGrayscale(it.rgb) }
?: palette.dominantSwatch // Fallback to dominant if everything is gray
?: palette.dominantSwatch

if (bestSwatch != null) {
String.format("#%06X", (0xFFFFFF and bestSwatch.rgb))
Expand All @@ -137,17 +146,23 @@ abstract class BaseTranslator(
}
}

// Helper to detect if a color is White, Black, or Gray
private fun isGrayscale(color: Int): Boolean {
val r = (color shr 16) and 0xFF
val g = (color shr 8) and 0xFF
val b = color and 0xFF

// If RGB values are very close to each other, it's gray scale
val diff = abs(r - g) + abs(g - b) + abs(b - r)
return diff < 30
}

// --- NEW: Resolve Shape Logic ---
protected fun resolveShape(theme: HyperTheme?, pkg: String): String {
return theme?.apps?.get(pkg)?.iconShapeId ?: theme?.global?.iconShapeId ?: "circle"
}

protected fun resolvePadding(theme: HyperTheme?, pkg: String): Int {
return theme?.apps?.get(pkg)?.iconPaddingPercent ?: theme?.global?.iconPaddingPercent ?: 15
}

protected fun resolveActionConfig(theme: HyperTheme?, pkg: String, actionTitle: String): ActionConfig? {
if (theme == null) return null

Expand All @@ -162,18 +177,13 @@ abstract class BaseTranslator(
}?.value
}

protected fun resolveActionIcon(theme: HyperTheme?, pkg: String, actionTitle: String): Bitmap? {
if (repository == null) return null
val config = resolveActionConfig(theme, pkg, actionTitle) ?: return null
val resource = config.icon ?: return null
return if (resource.type == ResourceType.LOCAL_FILE) repository.getResourceBitmap(resource) else null
}

protected fun resolveIcon(sbn: StatusBarNotification, picKey: String): HyperPicture {
val originalBitmap = getNotificationBitmap(sbn) ?: createFallbackBitmap()
return HyperPicture(picKey, originalBitmap)
}

// --- THEME APPLICATION LOGIC ---

protected fun applyThemeToActionIcon(source: Bitmap, shapeId: String, paddingPercent: Int, bgColor: Int): Bitmap {
val size = 96
val output = createBitmap(size, size)
Expand Down Expand Up @@ -212,8 +222,11 @@ abstract class BaseTranslator(
return output
}

protected fun applyThemeToActionIcon(source: Bitmap, theme: HyperTheme, bgColor: Int): Bitmap {
return applyThemeToActionIcon(source, theme.global.iconShapeId, theme.global.iconPaddingPercent, bgColor)
// Overloaded to handle overrides automatically
protected fun applyThemeToActionIcon(source: Bitmap, theme: HyperTheme, pkg: String, bgColor: Int): Bitmap {
val shapeId = resolveShape(theme, pkg)
val padding = resolvePadding(theme, pkg)
return applyThemeToActionIcon(source, shapeId, padding, bgColor)
}

// --- CORE LOGIC ---
Expand All @@ -230,8 +243,9 @@ abstract class BaseTranslator(

val defaultActionBg = if (theme != null) {
try {
val hex = theme.global.highlightColor ?: "#007AFF"
resolveColor(theme, sbn.packageName, hex).toColorInt()
// Use updated resolveColor logic
val hex = resolveColor(theme, sbn.packageName, "#007AFF")
hex.toColorInt()
} catch (e: Exception) { "#007AFF".toColorInt() }
} else {
"#007AFF".toColorInt()
Expand Down Expand Up @@ -285,7 +299,8 @@ abstract class BaseTranslator(

if (bitmapToUse != null) {
val processedBitmap = if (theme != null) {
applyThemeToActionIcon(bitmapToUse, theme, finalBgColorInt)
// [FIX] Pass package name to respect app-specific shape overrides
applyThemeToActionIcon(bitmapToUse, theme, sbn.packageName, finalBgColorInt)
} else {
createRoundedIconWithBackground(bitmapToUse, finalBgColorInt, 12)
}
Expand All @@ -300,7 +315,6 @@ abstract class BaseTranslator(
androidAction.actionIntent
}

// [FIX] If mode is TEXT, strip the background color so it doesn't look like a solid block
val appliedBgColor = if (effectiveMode == ActionDisplayMode.TEXT) null else finalBgColorHex

val hyperAction = HyperAction(
Expand All @@ -309,7 +323,7 @@ abstract class BaseTranslator(
icon = actionIcon,
pendingIntent = finalIntent,
actionIntentType = 1,
actionBgColor = appliedBgColor, // Use conditional color
actionBgColor = appliedBgColor,
titleColor = finalTintColorHex
)

Expand All @@ -334,12 +348,6 @@ abstract class BaseTranslator(
return HyperPicture(key, bitmap)
}

protected fun getPictureFromResource(key: String, resId: Int): HyperPicture {
val drawable = ContextCompat.getDrawable(context, resId)
val bitmap = drawable?.toBitmap() ?: createFallbackBitmap()
return HyperPicture(key, bitmap)
}

protected fun getNotificationBitmap(sbn: StatusBarNotification): Bitmap? {
val pkg = sbn.packageName
val extras = sbn.notification.extras
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,12 @@ class CallTranslator(
val rawActions = sbn.notification.actions ?: return emptyList()
val results = mutableListOf<BridgeAction>()

val hangUpColor = theme?.callConfig?.declineColor ?: "#FF3B30"
val answerColor = theme?.callConfig?.answerColor ?: "#34C759"
// [FIX] Resolve Colors from Override -> Global
val appOverride = theme?.apps?.get(sbn.packageName)
val callOverride = appOverride?.callConfig

val hangUpColor = callOverride?.declineColor ?: theme?.callConfig?.declineColor ?: "#FF3B30"
val answerColor = callOverride?.answerColor ?: theme?.callConfig?.answerColor ?: "#34C759"
val neutralColor = "#8E8E93"

// Load custom icons from theme resources
Expand All @@ -163,16 +167,9 @@ class CallTranslator(
if (hangUpIndex != -1) indicesToShow.add(hangUpIndex)
}

// Fallback detection logic if keywords failed
if (indicesToShow.isEmpty()) {
if (rawActions.isNotEmpty()) {
indicesToShow.add(0)
hangUpIndex = 0
}
if (rawActions.size > 1) {
indicesToShow.add(1)
answerIndex = 1
}
if (rawActions.isNotEmpty()) { indicesToShow.add(0); hangUpIndex = 0 }
if (rawActions.size > 1) { indicesToShow.add(1); answerIndex = 1 }
}

indicesToShow.take(2).forEach { index ->
Expand All @@ -181,15 +178,13 @@ class CallTranslator(
val isHangUp = index == hangUpIndex
val isAnswer = index == answerIndex

// Determine Background Color
val bgColorHex = when {
isHangUp -> hangUpColor
isAnswer -> answerColor
else -> neutralColor
}
val bgColorInt = try { bgColorHex.toColorInt() } catch(e: Exception) { 0xFF8E8E93.toInt() }

// Determine Icon Source (Custom vs System)
var originalBitmap: Bitmap? = null
if (isAnswer && customAnswerBitmap != null) {
originalBitmap = customAnswerBitmap
Expand All @@ -203,21 +198,19 @@ class CallTranslator(
var actionIcon: Icon? = null
var hyperPic: HyperPicture? = null

// [NEW] Determine Shape
// [FIX] Resolve shape from Call Override -> Global Call -> Global Icon
val actionShapeId = if (theme != null) {
when {
isAnswer -> theme.callConfig.answerShapeId
isHangUp -> theme.callConfig.declineShapeId
else -> theme.global.iconShapeId
isAnswer -> callOverride?.answerShapeId ?: theme.callConfig.answerShapeId
isHangUp -> callOverride?.declineShapeId ?: theme.callConfig.declineShapeId
else -> resolveShape(theme, sbn.packageName) // Fallback for other buttons
}
} else "circle"

val padding = theme?.global?.iconPaddingPercent ?: 15
val padding = resolvePadding(theme, sbn.packageName)

if (originalBitmap != null) {
// Apply Shape & Padding to ACTION ICON
val processedBitmap = if (theme != null) {
// [FIX] Use specific shape for this action
applyThemeToActionIcon(originalBitmap, actionShapeId, padding, bgColorInt)
} else {
createRoundedIconWithBackground(originalBitmap, bgColorInt, 12)
Expand All @@ -228,14 +221,13 @@ class CallTranslator(
hyperPic = HyperPicture(picKey, processedBitmap)
}

// Create Action with Color String
val hyperAction = io.github.d4viddf.hyperisland_kit.HyperAction(
key = uniqueKey,
title = action.title?.toString() ?: "",
icon = actionIcon,
pendingIntent = action.actionIntent,
actionIntentType = 1,
actionBgColor = bgColorHex, // [FIX] Hex String
actionBgColor = bgColorHex,
titleColor = "#FFFFFF"
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ class NavTranslator(context: Context, repo: ThemeRepository) : BaseTranslator(co
val themeProgressBarColor = theme?.defaultNavigation?.progressBarColor
?: resolveColor(theme, sbn.packageName, "#34C759")

// [FIX] Use progress bar color as fallback for actions instead of grey
val themeActionBgColor = try {
themeProgressBarColor.toColorInt()
} catch (e: Exception) {
Expand Down Expand Up @@ -122,7 +121,8 @@ class NavTranslator(context: Context, repo: ThemeRepository) : BaseTranslator(co

if (originalBitmap != null) {
val roundedBitmap = if (theme != null) {
applyThemeToActionIcon(originalBitmap, theme, themeActionBgColor)
// [FIX] Updated to use new BaseTranslator signature with packageName
applyThemeToActionIcon(originalBitmap, theme, sbn.packageName, themeActionBgColor)
} else {
createRoundedIconWithBackground(originalBitmap, themeActionBgColor, themeActionPadding)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ fun HeroSection() {
modifier = Modifier.fillMaxWidth(),
state = state,
itemSpacing = 8.dp,
contentPadding = PaddingValues(horizontal = 16.dp)
contentPadding = PaddingValues(horizontal = 8.dp)
) { i ->
val item = items[i]
HeroCard(item,
Expand Down
Loading