You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Musubi/App/Sources/IntercomView.swift

164 lines
7.0 KiB

import CoreImage.CIFilterBuiltins
import DesignSystem
import Intercom
import SwiftUI
/// Intercom on the control: QR code for the phones, who is connected, what they see and hear.
struct IntercomView: View {
let intercom: IntercomController
let sourceList: SourceListModel
let audio: AudioController
@Environment(\.openSettings) private var openSettings
private var server: IntercomServer { intercom.server }
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: Theme.Space.card) {
if !intercom.isEnabled {
VStack(alignment: .leading, spacing: Theme.Space.unit) {
Text("Intercom ist aus. Wähl in den Einstellungen das Klinik-Netz, dann verbinden sich Handys per QR-Code.")
.font(Theme.Font.body)
.foregroundStyle(Theme.Color.nezumi)
Button("Einstellungen öffnen") { openSettings() }
}
} else {
if let error = server.error {
Text(error).font(Theme.Font.body).foregroundStyle(Theme.Color.kohaku)
}
if intercom.sharesTechnicalInterface {
Text("Technik- und Klinik-Netz laufen über dieselbe Schnittstelle. Zum Testen okay, im Einsatz müssen sie getrennt sein.")
.font(Theme.Font.caption)
.foregroundStyle(Theme.Color.kohaku)
}
HStack(alignment: .top, spacing: Theme.Space.card) {
joinCard
VStack(alignment: .leading, spacing: Theme.Space.card) {
sourcesCard
participantsCard
}
}
}
}
.padding(Theme.Space.card)
.frame(maxWidth: .infinity, alignment: .leading)
}
.background(Theme.Color.sumi)
}
private var joinCard: some View {
Card {
Text("Handy verbinden").font(Theme.Font.title).foregroundStyle(Theme.Color.washi)
if let url = server.url {
QRCode(text: url.absoluteString)
.frame(width: 200, height: 200)
Text(verbatim: url.absoluteString)
.font(Theme.Font.number)
.lineLimit(1)
.minimumScaleFactor(0.6)
.foregroundStyle(Theme.Color.washi)
.textSelection(.enabled)
Text("Handy ins Klinik-Netz, QR-Code scannen, Name und Rolle eintragen.")
.font(Theme.Font.caption)
.foregroundStyle(Theme.Color.nezumi)
.fixedSize(horizontal: false, vertical: true)
} else {
Text("Startet …").font(Theme.Font.body).foregroundStyle(Theme.Color.nezumi)
}
}
.frame(width: 240)
}
private var sourcesCard: some View {
Card {
Picker("Vorschau", selection: Binding(get: { intercom.previewSourceName }, set: { intercom.previewSourceName = $0 })) {
Text("Keine").tag("")
ForEach(sourceList.sources, id: \.ndiName) { source in
Text(verbatim: source.ndiName).tag(source.ndiName)
}
if !intercom.previewSourceName.isEmpty, !sourceList.sources.contains(where: { $0.ndiName == intercom.previewSourceName }) {
Text(verbatim: "\(intercom.previewSourceName) · nicht gefunden").tag(intercom.previewSourceName)
}
}
Picker("Talkback", selection: Binding(get: { intercom.talkbackDeviceUID }, set: { intercom.talkbackDeviceUID = $0 })) {
if let name = intercom.automaticTalkbackName {
Text("Automatisch · \(name)").tag("")
} else {
Text("Automatisch").tag("")
}
ForEach(audio.matrix.inputDevices) { device in
Text(verbatim: device.name).tag(device.uid)
}
}
Text("Handys hören das Mikro, solange die Sprechtaste (\(audio.talkKey.displayName)) gedrückt ist.")
.font(Theme.Font.caption)
.foregroundStyle(Theme.Color.nezumi)
}
}
private var participantsCard: some View {
Card {
HStack {
Text("Verbunden").font(Theme.Font.title).foregroundStyle(Theme.Color.washi)
Spacer()
Text(verbatim: "\(server.participants.filter(\.connected).count)")
.font(Theme.Font.number)
.foregroundStyle(Theme.Color.nezumi)
}
if server.participants.isEmpty {
Text("Noch niemand da. Scann den QR-Code mit einem Handy.")
.font(Theme.Font.body)
.foregroundStyle(Theme.Color.nezumi)
}
ForEach(server.participants) { participant in
HStack(spacing: Theme.Space.unit) {
Circle()
.fill(participant.connected ? Theme.Color.moegi : Theme.Color.kohaku)
.frame(width: 8, height: 8)
Text(verbatim: participant.name).font(Theme.Font.body).foregroundStyle(Theme.Color.washi)
Text(verbatim: participant.role).font(Theme.Font.caption).foregroundStyle(Theme.Color.nezumi)
Spacer()
if !participant.connected {
Text("verbindet …").font(Theme.Font.caption).foregroundStyle(Theme.Color.nezumi)
}
}
}
}
}
}
private struct Card<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
VStack(alignment: .leading, spacing: Theme.Space.unit) { content }
.padding(Theme.Space.card)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Theme.Color.kasumi, in: RoundedRectangle(cornerRadius: Theme.Radius.card))
.overlay(RoundedRectangle(cornerRadius: Theme.Radius.card).stroke(Theme.Color.usuzumi, lineWidth: Theme.Stroke.line))
}
}
/// Black on white with a quiet zone: what phone cameras read best, regardless of the dark theme.
private struct QRCode: View {
let text: String
var body: some View {
if let image = Self.image(for: text) {
Image(decorative: image, scale: 1)
.interpolation(.none)
.resizable()
.padding(Theme.Space.unit)
.background(.white, in: RoundedRectangle(cornerRadius: Theme.Radius.button))
}
}
static func image(for text: String) -> CGImage? {
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(text.utf8)
filter.correctionLevel = "M"
guard let output = filter.outputImage else { return nil }
return CIContext().createCGImage(output, from: output.extent)
}
}

Powered by TurnKey Linux.