MiniPlayer/Sources/QRMediaPayload.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

81 lines
2.8 KiB
Swift
Raw 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 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
}
}