From dd64a214042bfe66d124e3564cee237c9a19a3e6 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 4 Oct 2026 12:07:00 +0800 Subject: [PATCH] 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 --- .gitignore | 2 + Resources/AppIcon.icns | Bin 0 -> 79051 bytes Sources/Localization.swift | 10 + Sources/MiniPlayerApp.swift | 49 ++++- Sources/PlayerBridge.swift | 93 ++++++++- Sources/QRDisplayView.swift | 164 ++++++++++++++++ Sources/QRMediaPayload.swift | 80 ++++++++ Sources/QRScannerSheet.swift | 181 ++++++++++++++++++ Sources/QRScannerView.swift | 15 +- .../Resources/en.lproj/Localizable.strings | 11 ++ .../zh-Hans.lproj/Localizable.strings | 11 ++ scripts/build-macos.sh | 23 ++- 12 files changed, 616 insertions(+), 23 deletions(-) create mode 100644 Resources/AppIcon.icns create mode 100644 Sources/QRDisplayView.swift create mode 100644 Sources/QRMediaPayload.swift create mode 100644 Sources/QRScannerSheet.swift diff --git a/.gitignore b/.gitignore index e1853cb..01fc0da 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ MiniPlayer.app/ project.yml .swiftpm/ MiniPlayer.ipa +build/ +devops diff --git a/Resources/AppIcon.icns b/Resources/AppIcon.icns new file mode 100644 index 0000000000000000000000000000000000000000..38ef9fade04d47a4d152dae38d95b6188681fcf6 GIT binary patch literal 79051 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 literal 0 HcmV?d00001 diff --git a/Sources/Localization.swift b/Sources/Localization.swift index 4ca1dd0..83eb026 100644 --- a/Sources/Localization.swift +++ b/Sources/Localization.swift @@ -55,6 +55,16 @@ enum L { static let scanQRCode = NSLocalizedString("scan_qr_code", value: "Scan QR Code", comment: "") static let scanHint = NSLocalizedString("scan_hint", value: "Point camera at a QR code containing a media URL", comment: "") static let detectedCode = NSLocalizedString("detected_code", value: "Detected:", comment: "") + + // 二维码显示 + static let showQRCode = NSLocalizedString("show_qr_code", value: "QR Code", comment: "") + static let qrGenFailed = NSLocalizedString("qr_gen_failed", value: "Could not generate QR code", comment: "") + static let copyURL = NSLocalizedString("copy_url", value: "Copy URL", comment: "") + static let copied = NSLocalizedString("copied", value: "Copied", comment: "") + static let done = NSLocalizedString("done", value: "Done", comment: "") + static let noCurrentURL = NSLocalizedString("no_current_url", value: "No video is currently playing", comment: "") + static let channelNameField = NSLocalizedString("channel_name_field", value: "Channel name (optional)", comment: "") + static let channelGroupField = NSLocalizedString("channel_group_field", value: "Group (optional)", comment: "") // 媒体库 static let mediaLibrary = NSLocalizedString("media_library", value: "Media Library", comment: "") diff --git a/Sources/MiniPlayerApp.swift b/Sources/MiniPlayerApp.swift index f19e55a..ec9c8eb 100644 --- a/Sources/MiniPlayerApp.swift +++ b/Sources/MiniPlayerApp.swift @@ -10,10 +10,14 @@ import UniformTypeIdentifiers #endif #if os(macOS) +extension Notification.Name { + /// 应用即将退出时发送,用于在终止前保存播放状态 + static let miniPlayerWillTerminate = Notification.Name("MiniPlayer.willTerminate") +} + // 主窗口关闭时退出应用 class AppDel: NSObject, NSApplicationDelegate { private var closeObserver: Any? - func applicationDidFinishLaunching(_ notification: Notification) { // 监听所有窗口关闭事件 closeObserver = NotificationCenter.default.addObserver( @@ -34,6 +38,8 @@ class AppDel: NSObject, NSApplicationDelegate { } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + // 退出前保存播放状态,供下次启动恢复 + NotificationCenter.default.post(name: .miniPlayerWillTerminate, object: nil) if RecorderState.isFinalizing { NSLog("[MiniPlayer] ⏳ Termination blocked: recording is finalizing") RecorderState.pendingTerminate = true @@ -85,8 +91,7 @@ public struct MiniPlayerApp: App { bridge.setup() setupBridgeEvents() loadI18n() - // iOS: 首次启动时恢复上次播放状态 - #if os(iOS) + // 首次启动时恢复上次播放状态(macOS + iOS 均适用) if !hasRestored { hasRestored = true // 延迟恢复,等 setup 和 KVO 就绪 @@ -94,10 +99,8 @@ public struct MiniPlayerApp: App { bridge.restoreSessionState() } } - #endif } .onChange(of: scenePhase) { _, newPhase in - #if os(iOS) switch newPhase { case .background, .inactive: // App 进后台/挂起:保存当前队列和播放位置 @@ -107,8 +110,13 @@ public struct MiniPlayerApp: App { @unknown default: break } - #endif } + #if os(macOS) + .onReceive(NotificationCenter.default.publisher(for: .miniPlayerWillTerminate)) { _ in + // macOS 退出前保存播放状态 + bridge.saveSessionState() + } + #endif } #if os(macOS) .commands { @@ -596,6 +604,19 @@ struct BricksAppView: View { NetworkFilePanel(bridge: bridge, isPresented: $bridge.showNetworkBrowser) } #if os(iOS) + .sheet(isPresented: Binding( + get: { bridge.qrDisplayURL != nil }, + set: { if !$0 { bridge.qrDisplayURL = nil } } + )) { + if let urlStr = bridge.qrDisplayURL { + QRDisplayView(urlString: urlStr) { bridge.qrDisplayURL = nil } + } + } + .sheet(isPresented: $bridge.showQRScanner) { + QRScannerSheet(bridge: bridge) { bridge.showQRScanner = false } + } + #endif + #if os(iOS) .fileImporter( isPresented: $bridge.showFileImporter, allowedContentTypes: [.movie, .video, .audio, .mpeg4Movie, .quickTimeMovie, .mp3, .wav, .mpeg4Audio, .plainText, .item], @@ -696,9 +717,8 @@ struct ControlToolbar: View { TB(icon: "folder") { bridge.openFileDialog(); onTouch() } TB(icon: "network") { bridge.toggleNetworkBrowser(); onTouch() } TB(icon: "link") { bridge.showURLDialog = true; onTouch() } - #if os(macOS) TB(icon: "qrcode.viewfinder") { bridge.showQRScannerWindow(); onTouch() } - #endif + TB(icon: "qrcode") { bridge.showCurrentQRCode(); onTouch() } TB(icon: "backward.fill") { bridge.playPrev(); onTouch() } TB(icon: bridge.isPlaying ? "pause.fill" : "play.fill") { bridge.togglePlayPause(); onTouch() } TB(icon: "forward.fill") { bridge.playNext(); onTouch() } @@ -871,6 +891,8 @@ struct PlaylistWindowView: View { struct URLInputDialog: View { @ObservedObject var bridge: PlayerBridge @State private var url = "" + @State private var name = "" + @State private var group = "" @Environment(\.dismiss) private var dismiss var body: some View { @@ -878,11 +900,20 @@ struct URLInputDialog: View { Text(L.addMediaURL).font(.headline) TextField("https://example.com/video.m3u8", text: $url) .textFieldStyle(.roundedBorder) + TextField(L.channelNameField, text: $name) + .textFieldStyle(.roundedBorder) + TextField(L.channelGroupField, text: $group) + .textFieldStyle(.roundedBorder) HStack { Button(L.cancel) { dismiss() } Spacer() Button(L.add) { - if !url.isEmpty { bridge.addURL(url); dismiss() } + if !url.isEmpty { + bridge.addURL(url, + name: name.isEmpty ? nil : name, + group: group.isEmpty ? nil : group) + dismiss() + } } .keyboardShortcut(.defaultAction) .disabled(url.isEmpty) diff --git a/Sources/PlayerBridge.swift b/Sources/PlayerBridge.swift index 945c719..b0887ff 100644 --- a/Sources/PlayerBridge.swift +++ b/Sources/PlayerBridge.swift @@ -80,7 +80,10 @@ final class PlayerBridge: ObservableObject { @Published var showNetworkBrowser = false #if os(macOS) var qrScannerController: QRScannerWindowController? + var qrDisplayController: QRDisplayWindowController? #endif + /// iOS: 当前待显示的二维码 URL(非 nil 时弹出 sheet) + @Published var qrDisplayURL: String? @Published var toastMessage: String? @Published var availableTracks: [String] = [] @Published var currentTrackIndex: Int = 0 @@ -714,17 +717,18 @@ final class PlayerBridge: ObservableObject { #endif } - func addURL(_ urlString: String) { + func addURL(_ urlString: String, name: String? = nil, group: String? = nil, logo: String? = nil) { let urlString = urlString.trimmingCharacters(in: .whitespacesAndNewlines) print("[MiniPlayer] addURL called: \(urlString)") guard let url = URL(string: urlString) else { showToast(L.invalidURL); return } - let name = url.lastPathComponent.isEmpty ? (url.host ?? urlString) : url.lastPathComponent + let resolvedName = (name?.isEmpty == false ? name! : + (url.lastPathComponent.isEmpty ? (url.host ?? urlString) : url.lastPathComponent)) let type = urlString.contains(".m3u8") ? "stream" : "url" - print("[MiniPlayer] Adding item: name=\(name), type=\(type)") - addItem(url: url, name: name, type: type) + print("[MiniPlayer] Adding item: name=\(resolvedName), type=\(type)") + addItem(url: url, name: resolvedName, type: type, group: group, logoURL: logo) } - func addItem(url: URL, name: String, type: String) { + func addItem(url: URL, name: String, type: String, group: String? = nil, logoURL: String? = nil) { let urlString = url.absoluteString print("[MiniPlayer] addItem: name=\(name), type=\(type), scheme=\(url.scheme ?? "nil")") // 检查队列是否已有相同URL @@ -734,7 +738,8 @@ final class PlayerBridge: ObservableObject { return } // 自动入库(库内已自动去重) - let libItem = library.addItem(url: urlString, name: name, type: type) + let libItem = library.addItem(url: urlString, name: name, type: type, + group: group, logoURL: logoURL) queue.append(MediaItem(id: UUID().uuidString, url: url, name: name, mediaType: type, libraryID: libItem.id)) print("[MiniPlayer] addItem: added to queue, count=\(queue.count)") @@ -777,8 +782,84 @@ final class PlayerBridge: ObservableObject { qrScannerController = QRScannerWindowController(bridge: self) } qrScannerController?.show() + #else + showQRScanner = true #endif } + + // MARK: - 当前播放 URL 的二维码 + /// 当前正在播放的媒体 URL(无播放内容时为 nil) + var currentURLString: String? { + guard currentIndex >= 0, currentIndex < queue.count else { return nil } + let s = queue[currentIndex].url.absoluteString + return s.isEmpty ? nil : s + } + + /// 当前播放条目对应的媒体库条目(用于取频道分组/台标) + var currentLibraryItem: LibraryItem? { + guard currentIndex >= 0, currentIndex < queue.count, + let libID = queue[currentIndex].libraryID else { return nil } + return library.items.first { $0.id == libID } + } + + /// 把当前播放的视频 URL + 频道信息编码成二维码载荷 + func currentQRPayload() -> QRMediaPayload? { + guard let urlString = currentURLString else { return nil } + let item = queue[currentIndex] + let lib = currentLibraryItem + return QRMediaPayload( + url: urlString, + name: item.name, + type: item.mediaType, + group: lib?.group, + logo: lib?.logoURL, + from: "MiniPlayer" + ) + } + + /// 把当前播放的视频 URL(含频道信息)生成二维码并弹窗显示 + func showCurrentQRCode() { + guard let payload = currentQRPayload() else { + showToast(L.noCurrentURL) + return + } + let text = payload.encoded() + #if os(macOS) + if qrDisplayController == nil { + qrDisplayController = QRDisplayWindowController() + } + qrDisplayController?.show(urlString: text, displayText: payload.url) + #else + qrDisplayURL = text + #endif + } + + /// 处理扫码/粘贴得到的二维码内容:解析频道信息 → 入库 → 播放 + /// 返回是否成功识别(无效内容返回 false,调用方决定提示) + @discardableResult + func handleScannedCode(_ code: String) -> Bool { + guard let payload = QRMediaPayload.decode(code), + let url = URL(string: payload.url) else { return false } + + let name = (payload.name?.isEmpty == false ? payload.name! : + (url.lastPathComponent.isEmpty ? (url.host ?? payload.url) : url.lastPathComponent)) + let type = payload.type?.isEmpty == false ? payload.type! : + (payload.url.contains(".m3u8") ? "stream" : "url") + + // 入库:带上频道分组/台标;已存在则返回原有条目 + let libItem = library.addItem(url: payload.url, name: name, type: type, + group: payload.group, logoURL: payload.logo) + + // 已在队列则直接跳到该条播放 + if let idx = queue.firstIndex(where: { $0.url.absoluteString == payload.url }) { + playIndex(idx) + return true + } + queue.append(MediaItem(id: UUID().uuidString, url: url, name: name, + mediaType: type, libraryID: libItem.id)) + playIndex(queue.count - 1) + return true + } func togglePlaylistWindow() { #if os(macOS) diff --git a/Sources/QRDisplayView.swift b/Sources/QRDisplayView.swift new file mode 100644 index 0000000..c48a137 --- /dev/null +++ b/Sources/QRDisplayView.swift @@ -0,0 +1,164 @@ +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 diff --git a/Sources/QRMediaPayload.swift b/Sources/QRMediaPayload.swift new file mode 100644 index 0000000..78e2c96 --- /dev/null +++ b/Sources/QRMediaPayload.swift @@ -0,0 +1,80 @@ +import Foundation + +// MARK: - 二维码载荷(当前播放视频 URL + 频道信息) +// +// 扫码后直接把该条目保存进扫码方的媒体库,并带上频道/分组/台标等元数据。 +// +// 编码格式:优先 JSON(携带频道信息), +// 若编码后过长(> ~900 字符,QR 版本会过大影响识别率), +// 退化为纯 URL —— 扫码方仍能添加,只是没有频道元数据。 + +public struct QRMediaPayload: Codable, Equatable { + /// 媒体 URL(必填) + public var url: String + /// 频道/节目名称 + public var name: String? + /// 类型:"stream" / "url" / "file" + public var type: String? + /// 频道分组(M3U group-title) + public var group: String? + /// 台标 URL(M3U tvg-logo) + public var logo: String? + /// 发送方标识,便于扫码方区分来源 + public var from: String? + + public init(url: String, name: String? = nil, type: String? = nil, + group: String? = nil, logo: String? = nil, from: String? = nil) { + self.url = url + self.name = name + self.type = type + self.group = group + self.logo = logo + self.from = from + } + + /// 短键名,压缩二维码体积 + enum CodingKeys: String, CodingKey { + case url = "u" + case name = "n" + case type = "t" + case group = "g" + case logo = "l" + case from = "f" + } + + /// 二维码中实际使用的文本:短负载用 JSON,超长退回纯 URL + public static let maxJSONLength = 900 + + public func encoded() -> String { + let enc = JSONEncoder() + enc.outputFormatting = [.withoutEscapingSlashes] + if let data = try? enc.encode(self), + let json = String(data: data, encoding: .utf8), + json.count <= Self.maxJSONLength { + return json + } + return url // 退化:纯 URL + } + + /// 解析扫码结果:支持 JSON 载荷,也兼容纯 URL(旧二维码/手输) + public static func decode(_ text: String) -> QRMediaPayload? { + let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + + if trimmed.hasPrefix("{") { + if let data = trimmed.data(using: .utf8), + let p = try? JSONDecoder().decode(QRMediaPayload.self, from: data), + !p.url.isEmpty { + return p + } + return nil // 是 JSON 但解析失败 → 不当成 URL 处理 + } + + // 纯 URL 回退 + if let u = URL(string: trimmed), let scheme = u.scheme?.lowercased(), + scheme == "http" || scheme == "https" || scheme == "file" { + return QRMediaPayload(url: trimmed, name: u.lastPathComponent.isEmpty ? u.host : u.lastPathComponent) + } + return nil + } +} diff --git a/Sources/QRScannerSheet.swift b/Sources/QRScannerSheet.swift new file mode 100644 index 0000000..3e231ad --- /dev/null +++ b/Sources/QRScannerSheet.swift @@ -0,0 +1,181 @@ +import SwiftUI +import AVFoundation + +#if os(iOS) +import UIKit + +// MARK: - iOS 扫码界面(SwiftUI sheet,替代 macOS 的独立 NSWindow 控制器) + +/// 使用 AVCaptureSession + AVCaptureMetadataOutput 扫描包含媒体 URL 的二维码。 +/// 扫到有效 http/https URL 后调用 `bridge.addURL`(自动入库 + 播放)并关闭。 +struct QRScannerSheet: View { + @ObservedObject var bridge: PlayerBridge + let onDismiss: () -> Void + + @State private var statusText: String = "" + @State private var statusColor: Color = .white + @State private var isAuthorized = false + @State private var hasCamera = true + + var body: some View { + VStack(spacing: 0) { + HStack { + Text(L.scanQRCode).font(.headline).foregroundColor(.white) + Spacer() + Button(L.cancel) { onDismiss() } + .foregroundColor(.white) + } + .padding() + .background(Color.black) + + ZStack { + Color.black + if isAuthorized && hasCamera { + QRCaptureRepresentable(onCode: handleCode) + .ignoresSafeArea(edges: .horizontal) + } else { + VStack(spacing: 12) { + Image(systemName: hasCamera ? "lock.shield" : "video.slash") + .font(.system(size: 40)) + .foregroundColor(.white.opacity(0.6)) + Text(hasCamera ? "Camera access required" : "No camera available") + .foregroundColor(.white.opacity(0.8)) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + VStack(spacing: 6) { + Text(statusText.isEmpty ? L.scanHint : statusText) + .font(.system(size: 12, design: .monospaced)) + .foregroundColor(statusColor.opacity(0.9)) + .multilineTextAlignment(.center) + .lineLimit(3) + .truncationMode(.middle) + .padding(.horizontal) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(Color.black) + } + .background(Color.black) + .onAppear(perform: checkPermission) + } + + private func checkPermission() { + hasCamera = AVCaptureDevice.default(for: .video) != nil + guard hasCamera else { return } + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + isAuthorized = true + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { granted in + DispatchQueue.main.async { isAuthorized = granted } + } + default: + isAuthorized = false + } + } + + private func handleCode(_ code: String) { + statusText = code + // 解析 JSON 载荷(含频道信息)或纯 URL → 入库 + 播放 + if bridge.handleScannedCode(code) { + statusColor = .green + DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { onDismiss() } + } else { + statusColor = .red + } + } +} + +// MARK: - UIKit 摄像头预览桥接(AVCaptureVideoPreviewLayer) + +private struct QRCaptureRepresentable: UIViewRepresentable { + let onCode: (String) -> Void + + func makeCoordinator() -> Coordinator { Coordinator(onCode: onCode) } + + func makeUIView(context: Context) -> QRCaptureView { + let v = QRCaptureView() + v.coordinator = context.coordinator + v.configure() + return v + } + + func updateUIView(_ uiView: QRCaptureView, context: Context) {} + + static func dismantleUIView(_ uiView: QRCaptureView, coordinator: Coordinator) { + coordinator.stop() + } + + final class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { + let onCode: (String) -> Void + private var lastCode: String? + private var lastTime = Date.distantPast + + init(onCode: @escaping (String) -> Void) { self.onCode = onCode } + + func metadataOutput(_ output: AVCaptureMetadataOutput, + didOutput metadataObjects: [AVMetadataObject], + from connection: AVCaptureConnection) { + guard let m = metadataObjects.first as? AVMetadataMachineReadableCodeObject, + let code = m.stringValue else { return } + let now = Date() + if code == lastCode, now.timeIntervalSince(lastTime) < 3 { return } + lastCode = code + lastTime = now + DispatchQueue.main.async { self.onCode(code) } + } + + func stop() {} + } + + /// 承载 AVCaptureVideoPreviewLayer 的 UIView + final class QRCaptureView: UIView { + weak var coordinator: Coordinator? + private let session = AVCaptureSession() + private var previewLayer: AVCaptureVideoPreviewLayer? + + func configure() { + // 摄像头配置是同步硬件 I/O,必须放到后台线程,否则主线程冻结 + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let self else { return } + self.session.sessionPreset = .high + + guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) + ?? AVCaptureDevice.default(for: .video), + let input = try? AVCaptureDeviceInput(device: device), + self.session.canAddInput(input) else { return } + self.session.addInput(input) + + let output = AVCaptureMetadataOutput() + guard self.session.canAddOutput(output) else { return } + self.session.addOutput(output) + output.setMetadataObjectsDelegate(self.coordinator, queue: .main) + self.session.startRunning() + // availableMetadataObjectTypes 在 startRunning 之后才非空 + if output.availableMetadataObjectTypes.contains(.qr) { + output.metadataObjectTypes = [.qr] + } + + DispatchQueue.main.async { [weak self] in + guard let self else { return } + let pl = AVCaptureVideoPreviewLayer(session: self.session) + pl.videoGravity = .resizeAspectFill + pl.frame = self.bounds + self.layer.addSublayer(pl) + self.previewLayer = pl + } + } + } + + override func layoutSubviews() { + super.layoutSubviews() + previewLayer?.frame = bounds + } + + deinit { session.stopRunning() } + } +} +#endif diff --git a/Sources/QRScannerView.swift b/Sources/QRScannerView.swift index 66cb004..b6af637 100644 --- a/Sources/QRScannerView.swift +++ b/Sources/QRScannerView.swift @@ -222,15 +222,16 @@ class QRScannerWindowController: NSObject, AVCaptureMetadataOutputObjectsDelegat resultLabel?.stringValue = code resultLabel?.textColor = .systemGreen - // 有效 URL → 添加并播放 - if let url = URL(string: code), (url.scheme == "http" || url.scheme == "https") { - DispatchQueue.main.async { [weak self] in - self?.bridge?.addURL(code) - } - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in - self?.close() + // 解析:JSON 载荷(含频道信息)或纯 URL → 入库并播放 + DispatchQueue.main.async { [weak self] in + guard let self = self, let bridge = self.bridge else { return } + if !bridge.handleScannedCode(code) { + self.resultLabel?.textColor = .systemRed } } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in + self?.close() + } } deinit { diff --git a/Sources/Resources/en.lproj/Localizable.strings b/Sources/Resources/en.lproj/Localizable.strings index d7ef7b0..020c48a 100644 --- a/Sources/Resources/en.lproj/Localizable.strings +++ b/Sources/Resources/en.lproj/Localizable.strings @@ -29,3 +29,14 @@ "invalid_url" = "Invalid URL"; "open" = "Open"; "list" = "List"; +"scan_qr_code" = "Scan QR Code"; +"scan_hint" = "Point camera at a QR code containing a media URL"; +"detected_code" = "Detected:"; +"show_qr_code" = "QR Code"; +"qr_gen_failed" = "Could not generate QR code"; +"copy_url" = "Copy URL"; +"copied" = "Copied"; +"done" = "Done"; +"no_current_url" = "No video is currently playing"; +"channel_name_field" = "Channel name (optional)"; +"channel_group_field" = "Group (optional)"; diff --git a/Sources/Resources/zh-Hans.lproj/Localizable.strings b/Sources/Resources/zh-Hans.lproj/Localizable.strings index 626acff..6472725 100644 --- a/Sources/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/Resources/zh-Hans.lproj/Localizable.strings @@ -29,3 +29,14 @@ "invalid_url" = "无效URL"; "open" = "打开"; "list" = "列表"; +"scan_qr_code" = "扫码"; +"scan_hint" = "将摄像头对准包含媒体URL的二维码"; +"detected_code" = "检测到:"; +"show_qr_code" = "二维码"; +"qr_gen_failed" = "无法生成二维码"; +"copy_url" = "复制URL"; +"copied" = "已复制"; +"done" = "完成"; +"no_current_url" = "当前没有正在播放的视频"; +"channel_name_field" = "频道名称(可选)"; +"channel_group_field" = "分组(可选)"; diff --git a/scripts/build-macos.sh b/scripts/build-macos.sh index 956b1fc..f348bbc 100755 --- a/scripts/build-macos.sh +++ b/scripts/build-macos.sh @@ -50,11 +50,32 @@ cp "$PROJECT_DIR/Sources/Resources/i18n/zh.json" "$APP_BUNDLE/Contents/Resources cp "$PROJECT_DIR/Sources/Resources/zh-Hans.lproj/Localizable.strings" "$APP_BUNDLE/Contents/Resources/zh-Hans.lproj/" cp "$PROJECT_DIR/Sources/Resources/en.lproj/Localizable.strings" "$APP_BUNDLE/Contents/Resources/en.lproj/" -# App 图标(如果有) +# App 图标:若缺 .icns 则从 iconset 自动生成 +if [ ! -f "$PROJECT_DIR/Resources/AppIcon.icns" ] && [ -d "$PROJECT_DIR/Icons_macOS/MiniPlayer.appiconset" ]; then + echo " → 生成 AppIcon.icns..." + ICONSET_SRC="$PROJECT_DIR/Icons_macOS/MiniPlayer.appiconset" + ICONSET_TMP="$(mktemp -d)/AppIcon.iconset" + mkdir -p "$ICONSET_TMP" + cp "$ICONSET_SRC/icon_16.png" "$ICONSET_TMP/icon_16x16.png" + cp "$ICONSET_SRC/icon_16@2x.png" "$ICONSET_TMP/icon_16x16@2x.png" + cp "$ICONSET_SRC/icon_32.png" "$ICONSET_TMP/icon_32x32.png" + cp "$ICONSET_SRC/icon_32@2x.png" "$ICONSET_TMP/icon_32x32@2x.png" + cp "$ICONSET_SRC/icon_128.png" "$ICONSET_TMP/icon_128x128.png" + cp "$ICONSET_SRC/icon_128@2x.png" "$ICONSET_TMP/icon_128x128@2x.png" + cp "$ICONSET_SRC/icon_256.png" "$ICONSET_TMP/icon_256x256.png" + cp "$ICONSET_SRC/icon_256@2x.png" "$ICONSET_TMP/icon_256x256@2x.png" + cp "$ICONSET_SRC/icon_512.png" "$ICONSET_TMP/icon_512x512.png" + cp "$ICONSET_SRC/icon_512@2x.png" "$ICONSET_TMP/icon_512x512@2x.png" + mkdir -p "$PROJECT_DIR/Resources" + iconutil -c icns "$ICONSET_TMP" -o "$PROJECT_DIR/Resources/AppIcon.icns" 2>/dev/null || true + rm -rf "$(dirname "$ICONSET_TMP")" +fi + if [ -f "$PROJECT_DIR/Resources/AppIcon.icns" ]; then cp "$PROJECT_DIR/Resources/AppIcon.icns" "$APP_BUNDLE/Contents/Resources/" ICON_FILE="AppIcon.icns" else + echo " ⚠ 未找到/无法生成 AppIcon.icns,应用将没有图标" ICON_FILE="" fi