tv-anarchy/Sources/TVAnarchyiOS/QRScanView.swift
Natalie 4a2ceb9781 feat(offline): inline star-to-keep and trash-to-cull on cache rows
Surface the existing pin (keep-from-cull) and per-file delete actions as
visible inline buttons on each offline cache row instead of context-menu-only:
a star toggles protection from auto-cull (and restore-if-missing), a trash
culls that file early. Aligns wording/icons to the star metaphor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:12:41 -04:00

75 lines
2.8 KiB
Swift

// Native iOS QR capture for the join page AVCaptureMetadataOutput does QR
// detection in hardware-adjacent fashion on iOS (no Vision pass needed, unlike
// the macOS scanner). Emits every decoded payload; the caller decides what to
// do (and when to dismiss).
import SwiftUI
import AVFoundation
struct QRScanView: UIViewControllerRepresentable {
let onCode: (String) -> Void
func makeUIViewController(context: Context) -> ScannerController {
let vc = ScannerController()
vc.onCode = onCode
return vc
}
func updateUIViewController(_ uiViewController: ScannerController, context: Context) {}
final class ScannerController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
var onCode: ((String) -> Void)?
private let session = AVCaptureSession()
private let sessionQueue = DispatchQueue(label: "tva.qr-scan")
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
let preview = AVCaptureVideoPreviewLayer(session: session)
preview.videoGravity = .resizeAspectFill
preview.frame = view.bounds
view.layer.addSublayer(preview)
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
guard granted, let self else { return }
self.sessionQueue.async { self.configure() }
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
view.layer.sublayers?.first?.frame = view.bounds
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sessionQueue.async { [session] in
if session.isRunning { session.stopRunning() }
}
}
private func configure() {
guard session.inputs.isEmpty,
let camera = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: camera),
session.canAddInput(input) else { return }
session.addInput(input)
let output = AVCaptureMetadataOutput()
guard session.canAddOutput(output) else { return }
session.addOutput(output)
output.setMetadataObjectsDelegate(self, queue: .main)
output.metadataObjectTypes = [.qr]
session.startRunning()
}
func metadataOutput(_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection) {
guard let payload = (metadataObjects.first as? AVMetadataMachineReadableCodeObject)?
.stringValue else { return }
onCode?(payload)
}
}
}