- 菜单栏可每项独立(点击弹出该项窄详情)或合并为一个(点击打开主窗口),详情区块可在设置中勾选 - 网络详情:镜像流量历史、ICMP 连接探测格子、接口与 Wi-Fi、本地与公网 IPv4 / IPv6、SVG 国旗归属地与 ASN、各进程流量 - DNS:刷新缓存,一键切换自动 / Cloudflare / Google / 腾讯 / 阿里云或手动填写;辅助工具协议升至 3,服务名与地址二次校验 - 主窗口:侧边栏切换仪表盘、各指标详情与工具页,宽高可调,卡片按比例伸缩 - 统一外观:白底蓝 / 黑底蓝,窗口、设置与弹窗共用底色,去掉玻璃效果 - 新增网络解析、DNS 校验与国旗代码校验的单元测试;更新 README、架构文档与隐私说明
43 lines
1.5 KiB
Swift
43 lines
1.5 KiB
Swift
import AppKit
|
||
import SwiftUI
|
||
|
||
/// 国旗:使用包内的 SVG(flag-icons,MIT),不使用 emoji
|
||
struct FlagImage: View {
|
||
let countryCode: String
|
||
var height: CGFloat = DS.Space.s3
|
||
|
||
var body: some View {
|
||
if let image = FlagCache.shared.image(for: countryCode) {
|
||
Image(nsImage: image)
|
||
.resizable()
|
||
.aspectRatio(4 / 3, contentMode: .fit)
|
||
.frame(height: height)
|
||
.clipShape(RoundedRectangle(cornerRadius: DS.Radius.sm / 2))
|
||
.overlay(RoundedRectangle(cornerRadius: DS.Radius.sm / 2).strokeBorder(DS.Palette.border, lineWidth: DS.Size.stroke))
|
||
.accessibilityHidden(true)
|
||
}
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
final class FlagCache {
|
||
static let shared = FlagCache()
|
||
private var images: [String: NSImage] = [:]
|
||
private var missing: Set<String> = []
|
||
|
||
func image(for countryCode: String) -> NSImage? {
|
||
let code = countryCode.lowercased()
|
||
// 只接受两位字母,避免拼出包外路径
|
||
guard code.count == 2, code.unicodeScalars.allSatisfy({ ("a"..."z").contains($0) }) else { return nil }
|
||
if let image = images[code] { return image }
|
||
guard !missing.contains(code) else { return nil }
|
||
guard let url = Bundle.module.url(forResource: code, withExtension: "svg", subdirectory: "Flags"),
|
||
let image = NSImage(contentsOf: url) else {
|
||
missing.insert(code)
|
||
return nil
|
||
}
|
||
images[code] = image
|
||
return image
|
||
}
|
||
}
|