Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ listed. If a route's folder cannot be created (an unmounted volume, say), the
session falls back to `recordings_dir` and yap logs a warning naming the route.
Manage the list under Recordings in the Settings window: + picks the app and
then its folder, − removes the selected route, and double-clicking a row
changes its folder.
changes its folder. What holds the microphone is not always an app you could
pick — a FaceTime call belongs to `avconferenced`, which has no app to find —
so once detection has named something since yap started, + offers it beside
"Choose Application…" and only asks for the folder.

`mic_voice_processing` cancels speaker echo on the mic track. On by default: a
call coming out of your speakers goes back into the mic. Without it, the other
Expand Down
3 changes: 3 additions & 0 deletions Sources/yap/Daemon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,9 @@ final class Daemon: NSObject, NSApplicationDelegate {
detector?.ignoreCurrentClient()
return
}
// A daemon like avconferenced has no .app for the Settings picker
// to find, so the route list offers what detection has named.
if let app { SettingsModel.noteSeen(app) }

let title = MeetingTitle.capture(forCapturePID: pid)
let who = app?.name ?? "Your microphone"
Expand Down
19 changes: 19 additions & 0 deletions Sources/yap/UI/SettingsModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ final class SettingsModel: ObservableObject {
}
@Published private(set) var excludedApps: [AppRow]
@Published private(set) var routes: [AppRow]
/// What meeting detection has named since launch, newest first — the
/// route list's `+` offers these beside the application picker, because
/// a daemon like avconferenced has no .app for the picker to find. A
/// snapshot at open, like everything else here; in memory only, so the
/// list starts empty at each launch.
let seenClients: [MeetingApp]
private static var seen: [MeetingApp] = []

static func noteSeen(_ app: MeetingApp) {
seen.removeAll { $0.bundleID == app.bundleID }
seen.insert(app, at: 0)
}

/// Suppresses the write-through while `init` fills the properties in.
private var loading = true
Expand Down Expand Up @@ -91,6 +103,7 @@ final class SettingsModel: ObservableObject {
meetingAutoRecord = Config.meetingAutoRecord()
excludedApps = Config.meetingExcludedApps().map { Self.resolve($0, detail: nil) }
routes = Self.sorted(Config.recordingRoutes().map { Self.resolve($0.key, detail: $0.value) })
seenClients = Self.seen
loading = false
updateObserver = Updater.shared.observe { [weak self] state in
self?.updateStatus = state.description
Expand Down Expand Up @@ -130,6 +143,12 @@ final class SettingsModel: ObservableObject {
setRoute(bundleID, folder: Self.abbreviated(folder))
}

/// A client detection has already named: no app picker, only the folder.
func addRoute(for app: MeetingApp) {
guard let folder = pickFolder() else { return }
setRoute(app.bundleID, folder: Self.abbreviated(folder))
}

func changeRouteFolder(_ bundleID: String) {
guard let folder = pickFolder() else { return }
setRoute(bundleID, folder: Self.abbreviated(folder))
Expand Down
54 changes: 44 additions & 10 deletions Sources/yap/UI/SettingsPanes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,19 @@ struct RecordingPane: View {
model.removeRoute(routeSelection)
self.routeSelection = nil
},
// Named by detection since launch: the only way to route
// a daemon, which has no .app for the picker to find.
addChoices: model.seenClients.map { app in
AppList.AddChoice(id: app.bundleID, label: app.name) {
model.addRoute(for: app)
}
},
onActivate: { model.changeRouteFolder($0) }
)
} header: {
Text("Route by app")
} footer: {
Text("Calls detected from these apps are saved here instead of the folder above. Double-click a row to change its folder.")
Text("Calls detected from these apps are saved here instead of the folder above. Double-click a row to change its folder. Anything detection has named since yap started is listed under +, so a background process with no app can be routed too.")
.font(.system(size: 11))
.foregroundStyle(.secondary)
}
Expand Down Expand Up @@ -186,9 +193,18 @@ private struct AppList: View {
let removeHelp: String
let onAdd: () -> Void
let onRemove: () -> Void
/// Entries offered beside the picker. Any at all turn `+` into a menu,
/// with the picker first; none leaves it the plain button.
var addChoices: [AddChoice] = []
/// Double-click on a row, for lists where a row has something to edit.
var onActivate: ((String) -> Void)? = nil

struct AddChoice: Identifiable {
let id: String
let label: String
let action: () -> Void
}

var body: some View {
VStack(spacing: 0) {
ScrollView {
Expand Down Expand Up @@ -217,7 +233,23 @@ private struct AppList: View {

Divider()
HStack(spacing: 0) {
stepper("plus", help: addHelp, action: onAdd)
if addChoices.isEmpty {
stepper("plus", help: addHelp, action: onAdd)
} else {
Menu {
Button("Choose Application…", action: onAdd)
Divider()
ForEach(addChoices) { choice in
Button(choice.label, action: choice.action)
}
} label: {
symbol("plus")
}
.menuStyle(.borderlessButton)
.menuIndicator(.hidden)
.fixedSize()
.help(addHelp)
}
Divider().frame(height: 16)
stepper("minus", help: removeHelp, action: onRemove)
.disabled(selection == nil)
Expand Down Expand Up @@ -290,14 +322,16 @@ private struct AppList: View {
private func stepper(
_ symbol: String, help: String, action: @escaping () -> Void
) -> some View {
Button(action: action) {
Image(systemName: symbol)
.font(.system(size: 11, weight: .semibold))
.frame(width: 30, height: 24)
.contentShape(Rectangle())
}
.buttonStyle(.borderless)
.help(help)
Button(action: action) { self.symbol(symbol) }
.buttonStyle(.borderless)
.help(help)
}

private func symbol(_ name: String) -> some View {
Image(systemName: name)
.font(.system(size: 11, weight: .semibold))
.frame(width: 30, height: 24)
.contentShape(Rectangle())
}
}

Expand Down
Loading