181 lines
6.0 KiB
Swift
181 lines
6.0 KiB
Swift
import AppKit
|
||
import SwiftUI
|
||
import ShotdeckCore
|
||
|
||
struct MenuBarView: View {
|
||
@Environment(AppModel.self) private var model
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
statusRow
|
||
SessionStrip()
|
||
Divider()
|
||
actionsList
|
||
if !model.allReturns.isEmpty {
|
||
Divider()
|
||
returnsBlock
|
||
}
|
||
}
|
||
.padding(10)
|
||
.frame(width: 320, alignment: .leading)
|
||
.controlSize(.small)
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var statusRow: some View {
|
||
if !model.screenRecordingGranted {
|
||
HStack(alignment: .top, spacing: 6) {
|
||
Image(systemName: "exclamationmark.triangle")
|
||
.foregroundStyle(.yellow)
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text(
|
||
ShotdeckError.screenRecordingNotGranted.errorDescription
|
||
?? "Screen Recording is turned off."
|
||
)
|
||
.font(.caption)
|
||
.fixedSize(horizontal: false, vertical: true)
|
||
Button("Open Screen Recording settings") {
|
||
model.openScreenRecordingSettings()
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
Text(model.statusLine ?? defaultStatusText)
|
||
.font(.caption)
|
||
.foregroundStyle(.primary)
|
||
.fixedSize(horizontal: false, vertical: true)
|
||
}
|
||
}
|
||
|
||
private var defaultStatusText: String {
|
||
guard let region = model.region else {
|
||
return "No region yet — press \(model.hotkeyDisplayString) to pick one."
|
||
}
|
||
let w = Int(region.rect.width)
|
||
let h = Int(region.rect.height)
|
||
let display = displayName(for: region)
|
||
if model.session.isEmpty {
|
||
return "Region \(w) × \(h) on \(display) · Nothing captured yet."
|
||
}
|
||
let count = model.session.captures.count
|
||
return "\(count) captures · region \(w) × \(h) on \(display)"
|
||
}
|
||
|
||
private func displayName(for region: CaptureRegion) -> String {
|
||
for (index, screen) in NSScreen.screens.enumerated() {
|
||
let id = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?
|
||
.uint32Value
|
||
if id == region.displayID {
|
||
return "Display \(index + 1)"
|
||
}
|
||
}
|
||
return "Display 1"
|
||
}
|
||
|
||
private var actionsList: some View {
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Button {
|
||
let anchor = NSApp.keyWindow?.contentView
|
||
if let sender = model as? SendCapable {
|
||
Task { await sender.send(anchor: anchor) }
|
||
} else {
|
||
model.setStatus("Send is not available in this build.")
|
||
}
|
||
} label: {
|
||
actionLabel("Send…")
|
||
}
|
||
.disabled(model.session.isEmpty || model.isSending)
|
||
|
||
Button {
|
||
Task { await model.captureNow() }
|
||
} label: {
|
||
actionLabel("Capture now", trailing: model.hotkeyDisplayString)
|
||
}
|
||
.disabled(model.isCapturing)
|
||
|
||
Button {
|
||
Task { await model.rePickRegion() }
|
||
} label: {
|
||
actionLabel("Re-select area")
|
||
}
|
||
|
||
Button {
|
||
Task { await model.copyCommentedLinks() }
|
||
} label: {
|
||
actionLabel(
|
||
"Copy commented links",
|
||
trailing: model.commentedReturns.isEmpty ? nil : "\(model.commentedReturns.count)"
|
||
)
|
||
}
|
||
.disabled(model.commentedReturns.isEmpty)
|
||
|
||
Button {
|
||
model.openSpoolFolder()
|
||
} label: {
|
||
actionLabel("Open spool folder")
|
||
}
|
||
|
||
Button {
|
||
if let presenter = model as? SettingsWindowPresenting {
|
||
presenter.presentSettingsWindow()
|
||
} else {
|
||
model.setStatus("Settings is not available in this build.")
|
||
}
|
||
} label: {
|
||
actionLabel("Settings…")
|
||
}
|
||
|
||
Button {
|
||
NSApp.terminate(nil)
|
||
} label: {
|
||
actionLabel("Quit Redline")
|
||
}
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
|
||
private func actionLabel(_ title: String, trailing: String? = nil) -> some View {
|
||
HStack(spacing: 8) {
|
||
Text(title)
|
||
Spacer(minLength: 8)
|
||
if let trailing {
|
||
Text(trailing)
|
||
.foregroundStyle(.secondary)
|
||
.monospacedDigit()
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.contentShape(Rectangle())
|
||
.padding(.vertical, 3)
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var returnsBlock: some View {
|
||
if let provider = model as? ReturnsSectionProviding {
|
||
provider.returnsSection()
|
||
} else {
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text("Came back from your device")
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
ForEach(newestReturns.prefix(8)) { doc in
|
||
HStack(spacing: 8) {
|
||
Text(doc.fileURL.lastPathComponent)
|
||
.lineLimit(1)
|
||
Spacer(minLength: 8)
|
||
Text(doc.isCommented
|
||
? "\(doc.annotatedPages.count) page\(doc.annotatedPages.count == 1 ? "" : "s") marked"
|
||
: "not marked")
|
||
.font(.caption)
|
||
.foregroundStyle(doc.isCommented ? .primary : .secondary)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private var newestReturns: [ReturnedDocument] {
|
||
model.allReturns.sorted { $0.detectedAt > $1.detectedAt }
|
||
}
|
||
}
|