MiniPlayer/Sources/QRDisplayView.swift
yumoqing dd64a21404 feat: QR code for current media with channel info, cross-platform
- Add QR generation of the currently playing URL (macOS window / iOS sheet)
- Encode channel metadata (name/type/group/logo) in the QR payload as
  compact JSON; fall back to a plain URL when the payload is too long
  for a reliably scannable QR
- Scanning saves the entry into the local library with its channel
  metadata and starts playback; legacy plain-URL QR codes still work
- Add iOS QR scanner sheet (AVCaptureMetadataOutput) so scanning is no
  longer macOS-only; both scanners share handleScannedCode
- Add optional channel name / group fields to the Add URL dialog and
  thread group/logo through addURL/addItem into the library
- Restore session (queue + position) on launch and save on quit for
  macOS, not just iOS
- Bundle app icons: generate AppIcon.icns from the macOS iconset when
  missing, and compile Assets.xcassets into the iOS app
2026-10-04 12:07:00 +08:00

165 lines
5.4 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import CoreImage
import CoreImage.CIFilterBuiltins
// MARK: - QR 码生成器(macOS + iOS 通用)
/// 把任意文本编码成 QR 码图片。
public enum QRCodeGenerator {
private static let context = CIContext()
/// 生成 QR 码位图。返回 nil 表示编码失败。
public static func image(from string: String, size: CGFloat = 260) -> PlatformImage? {
guard !string.isEmpty else { return nil }
let data = Data(string.utf8)
let filter = CIFilter.qrCodeGenerator()
filter.message = data
// 容错等级 H:即使二维码中间叠 logo 也能扫出,同时更耐屏幕反光/压缩
filter.correctionLevel = "H"
guard let output = filter.outputImage else { return nil }
// 原始 QR 码每模块 1px,放大到目标尺寸
let scale = size / output.extent.width
let scaled = output.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
#if os(macOS)
guard let cg = context.createCGImage(scaled, from: scaled.extent) else { return nil }
return NSImage(cgImage: cg, size: NSSize(width: size, height: size))
#else
guard let cg = context.createCGImage(scaled, from: scaled.extent) else { return nil }
return UIImage(cgImage: cg)
#endif
}
}
#if os(macOS)
public typealias PlatformImage = NSImage
#else
public typealias PlatformImage = UIImage
#endif
// MARK: - 二维码弹窗内容视图(macOS 窗口 / iOS sheet 共用)
public struct QRDisplayView: View {
let urlString: String // 实际编码进二维码的内容(可能是 JSON 载荷)
let displayText: String? // 界面上给用户看的明文(通常是纯 URL)
let onDismiss: () -> Void
@State private var copied = false
public init(urlString: String, displayText: String? = nil, onDismiss: @escaping () -> Void) {
self.urlString = urlString
self.displayText = displayText
self.onDismiss = onDismiss
}
/// 界面上展示的文本:优先用明文 URL,没有则展示编码内容
private var shownText: String { displayText ?? urlString }
public var body: some View {
VStack(spacing: 18) {
Text(L.showQRCode)
.font(.headline)
Group {
if let img = QRCodeGenerator.image(from: urlString, size: 260) {
#if os(macOS)
Image(nsImage: img)
.interpolation(.none)
.resizable()
.frame(width: 260, height: 260)
.padding(10)
.background(Color.white)
.cornerRadius(8)
#else
Image(uiImage: img)
.interpolation(.none)
.resizable()
.frame(width: 260, height: 260)
.padding(10)
.background(Color.white)
.cornerRadius(8)
#endif
} else {
Text(L.qrGenFailed)
.foregroundColor(.red)
.frame(width: 260, height: 260)
}
}
// URL 明文:方便长按/复制/手动核对
Text(shownText)
.font(.system(size: 12, design: .monospaced))
.multilineTextAlignment(.center)
.lineLimit(4)
.truncationMode(.middle)
.textSelection(.enabled)
.frame(maxWidth: 300)
HStack(spacing: 12) {
Button(copied ? L.copied : L.copyURL) {
copyToClipboard(shownText)
copied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { copied = false }
}
Button(L.done, action: onDismiss)
.keyboardShortcut(.defaultAction)
}
}
.padding(24)
.frame(minWidth: 340)
}
private func copyToClipboard(_ s: String) {
#if os(macOS)
let pb = NSPasteboard.general
pb.clearContents()
pb.setString(s, forType: .string)
#else
UIPasteboard.general.string = s
#endif
}
}
#if os(macOS)
/// macOS:独立窗口控制器,复用与扫码窗口一致的 NSWindow 模式
public final class QRDisplayWindowController {
private var window: NSWindow?
private let onClose: () -> Void
public init(onClose: @escaping () -> Void = {}) {
self.onClose = onClose
}
public func show(urlString: String, displayText: String? = nil) {
if let win = window, win.isVisible {
win.makeKeyAndOrderFront(nil)
return
}
let win = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 380, height: 460),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
win.title = L.showQRCode
win.isReleasedWhenClosed = false
win.center()
win.contentView = NSHostingView(rootView: QRDisplayView(urlString: urlString, displayText: displayText) { [weak self] in
self?.close()
})
win.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
self.window = win
}
public func close() {
window?.close()
window = nil
onClose()
}
}
#endif