Add raster panel icon family
Some checks failed
Build: Android / Android Rust Core Stub (push) Failing after 25s
Build Rust / Cargo Test (push) Successful in 4m2s
Build Site / Next.js Build (push) Failing after 4s
Lint Governance / BEP Metadata (push) Successful in 2s

This commit is contained in:
Conrad Kramer 2026-06-08 00:20:25 -07:00
parent c81712b781
commit 60cc48a4b7
278 changed files with 1992 additions and 621 deletions

View file

@ -4,17 +4,26 @@ import SwiftUI
#if canImport(AuthenticationServices)
import AuthenticationServices
#endif
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
@MainActor
public protocol AppIconManaging {
var supportsAlternateIcons: Bool { get }
var alternateIconName: String? { get }
func setAlternateIconName(_ iconName: String?) async throws
}
public struct BurrowView: View {
private let appIconManager: (any AppIconManaging)?
@State private var networkViewModel: NetworkViewModel
@State private var accountStore = NetworkAccountStore()
@State private var activeSheet: ConfigurationSheet?
@State private var didRunAutomation = false
#if os(iOS)
@State private var selectedAppIconName: String?
@State private var appIconError: String?
@State private var isSettingAppIcon = false
#endif
public var body: some View {
NavigationStack {
@ -56,6 +65,10 @@ public struct BurrowView: View {
quickAddSection
}
if showsAppIconSection {
appIconSection
}
VStack(alignment: .leading, spacing: 12) {
sectionHeader(
title: "Networks",
@ -121,10 +134,12 @@ public struct BurrowView: View {
}
.onAppear {
runAutomationIfNeeded()
refreshAppIconSelection()
}
}
public init() {
public init(appIconManager: (any AppIconManaging)? = nil) {
self.appIconManager = appIconManager
_networkViewModel = State(
initialValue: NetworkViewModel(
socketURLResult: Result { try Constants.socketURL }
@ -143,6 +158,12 @@ public struct BurrowView: View {
activeSheet = .tailnet
}
private func refreshAppIconSelection() {
#if os(iOS)
selectedAppIconName = appIconManager?.alternateIconName
#endif
}
@ViewBuilder
private var quickAddSection: some View {
VStack(alignment: .leading, spacing: 12) {
@ -157,6 +178,81 @@ public struct BurrowView: View {
}
}
@ViewBuilder
private var appIconSection: some View {
#if os(iOS)
VStack(alignment: .leading, spacing: 12) {
sectionHeader(title: "App Icon", detail: nil)
LazyVGrid(
columns: [GridItem(.adaptive(minimum: 86), spacing: 12)],
alignment: .leading,
spacing: 12
) {
ForEach(AppIconChoice.allCases) { choice in
let isSelected = selectedAppIconName == choice.alternateIconName
Button {
setAppIcon(choice)
} label: {
VStack(spacing: 8) {
Image(choice.previewAssetName)
.resizable()
.aspectRatio(1, contentMode: .fit)
.frame(width: 72, height: 72)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 3)
}
.shadow(color: .black.opacity(0.18), radius: 5, y: 2)
Text(choice.title)
.font(.caption2.weight(.semibold))
.foregroundStyle(isSelected ? Color.accentColor : Color.primary)
.lineLimit(1)
.minimumScaleFactor(0.75)
}
.frame(width: 86, height: 104)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.disabled(isSettingAppIcon)
.accessibilityLabel(choice.accessibilityLabel)
}
}
if let appIconError {
Text(appIconError)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
#else
EmptyView()
#endif
}
#if os(iOS)
private func setAppIcon(_ choice: AppIconChoice) {
guard let appIconManager, appIconManager.supportsAlternateIcons else {
return
}
guard selectedAppIconName != choice.alternateIconName else {
return
}
isSettingAppIcon = true
appIconError = nil
Task { @MainActor in
do {
try await appIconManager.setAlternateIconName(choice.alternateIconName)
selectedAppIconName = appIconManager.alternateIconName
} catch {
appIconError = error.localizedDescription
selectedAppIconName = appIconManager.alternateIconName
}
isSettingAppIcon = false
}
}
#endif
@ViewBuilder
private func sectionHeader(title: String, detail: String?) -> some View {
VStack(alignment: .leading, spacing: 4) {
@ -193,6 +289,80 @@ public struct BurrowView: View {
true
#endif
}
private var showsAppIconSection: Bool {
#if os(iOS)
appIconManager?.supportsAlternateIcons == true
#else
false
#endif
}
}
private enum AppIconChoice: CaseIterable, Identifiable {
case guardian
case field
case burrow
case sentinel
case stride
case post
var id: String { previewAssetName }
var title: String {
switch self {
case .guardian:
"Guardian"
case .field:
"Field"
case .burrow:
"Burrow"
case .sentinel:
"Sentinel"
case .stride:
"Stride"
case .post:
"Post"
}
}
var previewAssetName: String {
switch self {
case .guardian:
"BurrowPanel01Preview"
case .field:
"BurrowPanel02Preview"
case .burrow:
"BurrowPanel03Preview"
case .sentinel:
"BurrowPanel04Preview"
case .stride:
"BurrowPanel05Preview"
case .post:
"BurrowPanel06Preview"
}
}
var alternateIconName: String? {
switch self {
case .guardian:
nil
case .field:
"BurrowPanel02"
case .burrow:
"BurrowPanel03"
case .sentinel:
"BurrowPanel04"
case .stride:
"BurrowPanel05"
case .post:
"BurrowPanel06"
}
}
var accessibilityLabel: String {
"\(title) app icon"
}
}
private enum ConfigurationSheet: String, CaseIterable, Identifiable {