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