Add "Save File" and "Save Content JSON" options to profile share menu, allowing users to save profiles directly to a chosen location using fileExporter instead of the system share sheet.
194 lines
5.6 KiB
Swift
194 lines
5.6 KiB
Swift
import Foundation
|
|
import QRCode
|
|
import SwiftUI
|
|
|
|
private extension CGColor {
|
|
static var labelColor: CGColor {
|
|
#if canImport(UIKit)
|
|
UIColor.label.cgColor
|
|
#elseif canImport(AppKit)
|
|
NSColor.labelColor.cgColor
|
|
#endif
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
public struct QRSDisplayView: View {
|
|
private let data: Data
|
|
|
|
@State private var encoder: LubyTransformEncoder?
|
|
@State private var currentBlock: EncodedBlock?
|
|
@State private var frameCount = 0
|
|
@State private var isPlaying = true
|
|
@State private var fps: Double = 10
|
|
@State private var sliceSize: Int = 500
|
|
@State private var timer: Timer?
|
|
|
|
public init(data: Data) {
|
|
self.data = data
|
|
}
|
|
|
|
public var body: some View {
|
|
VStack(spacing: 16) {
|
|
if let block = currentBlock {
|
|
QRCodeViewUI(
|
|
content: block.toBase64(),
|
|
errorCorrection: .low,
|
|
foregroundColor: .labelColor,
|
|
backgroundColor: CGColor(gray: 1.0, alpha: 0.0)
|
|
)
|
|
.aspectRatio(1, contentMode: .fit)
|
|
#if os(macOS)
|
|
.frame(minWidth: 280, minHeight: 280)
|
|
#else
|
|
.frame(maxWidth: 300, maxHeight: 300)
|
|
#endif
|
|
} else {
|
|
ProgressView()
|
|
.frame(width: 280, height: 280)
|
|
}
|
|
|
|
VStack(spacing: 4) {
|
|
Text("Frame: \(frameCount)")
|
|
.font(.caption)
|
|
if let encoder {
|
|
Text("Source blocks: \(encoder.k)")
|
|
.font(.caption)
|
|
Text("Data size: \(encoder.bytes) bytes")
|
|
.font(.caption)
|
|
}
|
|
}
|
|
.foregroundStyle(.secondary)
|
|
|
|
VStack(spacing: 12) {
|
|
HStack {
|
|
Button {
|
|
isPlaying.toggle()
|
|
} label: {
|
|
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
|
|
}
|
|
#if os(macOS)
|
|
.buttonStyle(.bordered)
|
|
#endif
|
|
|
|
Spacer()
|
|
|
|
Text(String(localized: "Ideal FPS"))
|
|
.font(.caption)
|
|
|
|
Slider(value: $fps, in: 1 ... 30, step: 1)
|
|
.frame(maxWidth: 120)
|
|
|
|
Text("\(Int(fps))")
|
|
.font(.caption)
|
|
.frame(width: 24, alignment: .trailing)
|
|
}
|
|
|
|
HStack {
|
|
Text(String(localized: "Slice Size"))
|
|
.font(.caption)
|
|
|
|
Picker("", selection: $sliceSize) {
|
|
Text("200").tag(200)
|
|
Text("500").tag(500)
|
|
Text("1000").tag(1000)
|
|
}
|
|
.pickerStyle(.segmented)
|
|
.frame(maxWidth: 200)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
.padding()
|
|
.onAppear {
|
|
setupEncoder()
|
|
startAnimation()
|
|
}
|
|
.onDisappear {
|
|
stopAnimation()
|
|
}
|
|
.onChange(of: fps) { _ in
|
|
if isPlaying {
|
|
restartTimer()
|
|
}
|
|
}
|
|
.onChange(of: isPlaying) { playing in
|
|
if playing {
|
|
startAnimation()
|
|
} else {
|
|
stopAnimation()
|
|
}
|
|
}
|
|
.onChange(of: sliceSize) { _ in
|
|
setupEncoder()
|
|
frameCount = 0
|
|
}
|
|
}
|
|
|
|
private func setupEncoder() {
|
|
encoder = LubyTransformEncoder(data: data, sliceSize: sliceSize, compress: true)
|
|
}
|
|
|
|
private func startAnimation() {
|
|
nextFrame()
|
|
restartTimer()
|
|
}
|
|
|
|
private func restartTimer() {
|
|
timer?.invalidate()
|
|
timer = Timer.scheduledTimer(withTimeInterval: 1.0 / fps, repeats: true) { _ in
|
|
Task { @MainActor in
|
|
nextFrame()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func stopAnimation() {
|
|
timer?.invalidate()
|
|
timer = nil
|
|
}
|
|
|
|
private func nextFrame() {
|
|
guard let encoder else { return }
|
|
currentBlock = encoder.fountain().next()
|
|
frameCount += 1
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
public struct QRSSheet: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
private let profileName: String
|
|
private let profileData: Data
|
|
|
|
public init(profileName: String, profileData: Data) {
|
|
self.profileName = profileName
|
|
self.profileData = profileData
|
|
}
|
|
|
|
public var body: some View {
|
|
#if os(macOS)
|
|
NavigationSheet(title: String(localized: "Share as QRS")) {
|
|
VStack {
|
|
QRSDisplayView(data: profileData)
|
|
Text("Ask the receiver to scan continuously until complete.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.padding(.bottom)
|
|
}
|
|
}
|
|
.frame(minWidth: 400, minHeight: 520)
|
|
#elseif os(iOS) || os(tvOS)
|
|
NavigationSheet(title: String(localized: "Share as QRS"), size: .large) {
|
|
VStack {
|
|
QRSDisplayView(data: profileData)
|
|
Text("Ask the receiver to scan continuously until complete.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.padding(.bottom)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|