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/ScreensView.swift

112 lines
4.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import DesignSystem
import MusubiCore
import SwiftUI
/// Cards for all connected screens; drop a source on a card to show it fullscreen there.
struct ScreensView: View {
let outputs: OutputController
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: Theme.Space.card) {
Text("Bildschirme")
.font(Theme.Font.title)
.foregroundStyle(Theme.Color.washi)
if outputs.outputs.isEmpty {
Text("Noch keine Szene. Zieh eine Quelle auf einen Bildschirm.")
.font(Theme.Font.body)
.foregroundStyle(Theme.Color.nezumi)
}
LazyVGrid(columns: [GridItem(.adaptive(minimum: 260), spacing: Theme.Space.card)], spacing: Theme.Space.card) {
ForEach(outputs.screens) { screen in
ScreenCard(screen: screen, output: outputs.outputs[screen.id], tick: outputs.tick) {
outputs.stop(screen.id)
}
.dropDestination(for: String.self) { names, _ in
guard let name = names.first else { return false }
outputs.show(Source(ndiName: name), on: screen.id)
return true
}
}
}
}
.padding(Theme.Space.card)
}
.background(Theme.Color.sumi)
}
}
private struct ScreenCard: View {
let screen: ScreenInfo
let output: ActiveOutput?
let tick: Int
let onStop: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: Theme.Space.unit) {
Text(verbatim: screen.name)
.font(Theme.Font.title)
.foregroundStyle(Theme.Color.washi)
Text(verbatim: "\(screen.width) × \(screen.height) · \(screen.refreshRate.formatted(.number.precision(.fractionLength(0...2)))) Hz")
.font(Theme.Font.number)
.foregroundStyle(Theme.Color.nezumi)
Divider().overlay(Theme.Color.usuzumi)
if let output {
OutputStatus(output: output, tick: tick)
Button("Ausgabe beenden", action: onStop)
.buttonStyle(.bordered)
} else {
Text("Quelle hierher ziehen")
.font(Theme.Font.body)
.foregroundStyle(Theme.Color.nezumi)
}
}
.frame(maxWidth: .infinity, minHeight: 150, alignment: .topLeading)
.padding(Theme.Space.card)
.background(Theme.Color.kasumi, in: RoundedRectangle(cornerRadius: Theme.Radius.card))
.overlay(
RoundedRectangle(cornerRadius: Theme.Radius.card)
.strokeBorder(output == nil ? Theme.Color.usuzumi : Theme.Color.ai, lineWidth: output == nil ? Theme.Stroke.line : Theme.Stroke.cord)
)
}
}
private struct OutputStatus: View {
let output: ActiveOutput
let tick: Int
var body: some View {
let snapshot = output.snapshot
VStack(alignment: .leading, spacing: Theme.Space.unit / 2) {
Text(verbatim: output.source.label)
.font(Theme.Font.body)
.foregroundStyle(Theme.Color.washi)
switch snapshot.state {
case .connecting:
Text("Verbinde …")
.font(Theme.Font.caption)
.foregroundStyle(Theme.Color.nezumi)
case .live:
HStack(spacing: Theme.Space.unit / 2) {
Circle().fill(Theme.Color.moegi).frame(width: Theme.Stroke.knot, height: Theme.Stroke.knot)
Text(verbatim: "\(snapshot.width) × \(snapshot.height) · \(snapshot.frameRate.formatted(.number.precision(.fractionLength(0...2)))) fps")
.font(Theme.Font.number)
.foregroundStyle(Theme.Color.nezumi)
}
case .stalled(let retryIn):
Text("\(output.source.label) antwortet nicht mehr. Neuer Versuch in \(Int(retryIn.rounded(.up))) s.")
.font(Theme.Font.caption)
.foregroundStyle(Theme.Color.kohaku)
}
let lost = snapshot.stats.missing + snapshot.stats.droppedByReceiver
Text("\(lost) Frames verloren (\(snapshot.stats.lossRate.formatted(.percent.precision(.fractionLength(2)))))")
.font(Theme.Font.number)
.foregroundStyle(snapshot.stats.lossRate >= 0.001 ? Theme.Color.kohaku : Theme.Color.nezumi)
}
}
}

Powered by TurnKey Linux.