mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
154 lines
3.9 KiB
Swift
154 lines
3.9 KiB
Swift
//
|
|
// SidebarChannelIcon.swift
|
|
// Yattee
|
|
//
|
|
// Pre-scaled channel icon for TabSection labels.
|
|
// TabSection labels don't support frame/resizable modifiers,
|
|
// so we pre-scale the image at the platform image level.
|
|
//
|
|
|
|
import SwiftUI
|
|
import Nuke
|
|
|
|
#if os(macOS)
|
|
import AppKit
|
|
#else
|
|
import UIKit
|
|
#endif
|
|
|
|
/// A channel icon that pre-scales the image for use in TabSection labels.
|
|
/// Standard SwiftUI frame/resizable modifiers don't work in Tab labels.
|
|
struct SidebarChannelIcon: View {
|
|
let url: URL?
|
|
let name: String
|
|
var authHeader: String?
|
|
|
|
private let size: CGFloat = 22
|
|
|
|
@State private var platformImage: PlatformImage?
|
|
@State private var isLoading = false
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let platformImage, let scaledImage = scaledImage(from: platformImage) {
|
|
scaledImage
|
|
} else {
|
|
// Placeholder - use SF Symbol which scales correctly
|
|
Image(systemName: "person.circle.fill")
|
|
.symbolRenderingMode(.hierarchical)
|
|
}
|
|
}
|
|
.onAppear {
|
|
loadImage()
|
|
}
|
|
.onChange(of: url) { _, _ in
|
|
platformImage = nil
|
|
loadImage()
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func scaledImage(from image: PlatformImage) -> Image? {
|
|
#if os(macOS)
|
|
if let scaled = image.scaledCircular(to: NSSize(width: size, height: size)) {
|
|
Image(nsImage: scaled)
|
|
}
|
|
#else
|
|
if let scaled = image.scaledCircular(to: CGSize(width: size, height: size)) {
|
|
Image(uiImage: scaled)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private func loadImage() {
|
|
guard let request = AvatarURLBuilder.imageRequest(url: url, authHeader: authHeader), !isLoading else { return }
|
|
|
|
// Check memory cache first (synchronous)
|
|
if let cached = ImagePipeline.shared.cache.cachedImage(for: request)?.image {
|
|
platformImage = cached
|
|
return
|
|
}
|
|
|
|
isLoading = true
|
|
|
|
Task {
|
|
do {
|
|
let image = try await ImagePipeline.shared.image(for: request)
|
|
await MainActor.run {
|
|
platformImage = image
|
|
isLoading = false
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Platform Image Scaling
|
|
|
|
#if os(macOS)
|
|
private extension NSImage {
|
|
func scaledCircular(to targetSize: NSSize) -> NSImage? {
|
|
let newImage = NSImage(size: targetSize)
|
|
newImage.lockFocus()
|
|
|
|
// Create circular clipping path
|
|
let path = NSBezierPath(ovalIn: NSRect(origin: .zero, size: targetSize))
|
|
path.addClip()
|
|
|
|
NSGraphicsContext.current?.imageInterpolation = .high
|
|
|
|
draw(
|
|
in: NSRect(origin: .zero, size: targetSize),
|
|
from: NSRect(origin: .zero, size: size),
|
|
operation: .copy,
|
|
fraction: 1.0
|
|
)
|
|
|
|
newImage.unlockFocus()
|
|
return newImage
|
|
}
|
|
}
|
|
#else
|
|
private extension UIImage {
|
|
func scaledCircular(to targetSize: CGSize) -> UIImage? {
|
|
let renderer = UIGraphicsImageRenderer(size: targetSize)
|
|
return renderer.image { context in
|
|
// Create circular clipping path
|
|
let rect = CGRect(origin: .zero, size: targetSize)
|
|
context.cgContext.addEllipse(in: rect)
|
|
context.cgContext.clip()
|
|
|
|
draw(in: rect)
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// MARK: - Preview
|
|
|
|
#if !os(tvOS)
|
|
#Preview {
|
|
List {
|
|
Label {
|
|
Text("Apple")
|
|
} icon: {
|
|
SidebarChannelIcon(url: nil, name: "Apple")
|
|
}
|
|
|
|
Label {
|
|
Text("Test Channel")
|
|
} icon: {
|
|
SidebarChannelIcon(
|
|
url: URL(string: "https://example.com/avatar.jpg"),
|
|
name: "Test"
|
|
)
|
|
}
|
|
}
|
|
.listStyle(.sidebar)
|
|
}
|
|
#endif
|