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