The monogram placeholder wasn't really session-specific — the same hue-from-seed + initials logic fits every target that has an optional bundle icon. Rename `SessionPlaceholder` → `IconPlaceholder` (data in LumaCore) and the two view wrappers to `IconPlaceholderView` so the scope reads correctly, then wire the target picker's app and process rows to fall back on it when no Frida-provided icon is available. Seeds are app.identifier for applications and proc.name for processes so repeat targets keep consistent colours.
49 lines
1.5 KiB
Swift
49 lines
1.5 KiB
Swift
import LumaCore
|
|
import SwiftUI
|
|
|
|
struct IconPlaceholderView: View {
|
|
let seed: String
|
|
let displayName: String
|
|
var cornerRadius: CGFloat = 6
|
|
|
|
var body: some View {
|
|
GeometryReader { geo in
|
|
let side = min(geo.size.width, geo.size.height)
|
|
ZStack {
|
|
LinearGradient(
|
|
colors: gradientColors,
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
|
|
Text(IconPlaceholder.initials(for: displayName))
|
|
.font(.system(size: side * 0.46, weight: .semibold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
.minimumScaleFactor(0.5)
|
|
.lineLimit(1)
|
|
.allowsTightening(true)
|
|
.shadow(color: Color.black.opacity(0.15), radius: 1, y: 0.5)
|
|
}
|
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
|
|
}
|
|
.aspectRatio(1, contentMode: .fit)
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
private var gradientColors: [Color] {
|
|
let palette = IconPlaceholder.palette(for: seed)
|
|
return [
|
|
Color(
|
|
hue: palette.primaryHue,
|
|
saturation: palette.primarySaturation,
|
|
brightness: palette.primaryBrightness
|
|
),
|
|
Color(
|
|
hue: palette.secondaryHue,
|
|
saturation: palette.secondarySaturation,
|
|
brightness: palette.secondaryBrightness
|
|
),
|
|
]
|
|
}
|
|
}
|