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
43 changes: 27 additions & 16 deletions HeadsetControl-MacOSTray/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSWindowDele

private func activeHeadsetControlProvider() -> HeadsetControlProviding {
let testMode = UserDefaults.standard.integer(forKey: "testMode")
if testMode == 0 {
return headsetControlService
}
return MockHeadsetControlService(deviceIndex: testMode)
headsetControlService.setTestProfile(testMode)
return headsetControlService
}

private func runControlAction(_ action: @escaping (HeadsetControlProviding) -> Void) {
Expand Down Expand Up @@ -187,17 +185,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSWindowDele
let devicesResult = provider.fetchDevices()
var batteryLevelText: String? = nil
if let device = devicesResult.first,
let battery = device["battery"] as? [String: Any],
let level = battery["level"] as? Int {
batteryLevelText = "\(level)%"
let battery = device["battery"] as? [String: Any] {
batteryLevelText = self.batteryChargeText(from: battery)
let status = battery["status"] as? String ?? ""
let notifyOnLowBattery = UserDefaults.standard.bool(forKey: "notifyOnLowBattery")
if notifyOnLowBattery && ((status == "BATTERY_AVAILABLE" && level <= 25)) && !self.lowBatteryNotificationShown {
self.showLowBatteryNotification(level: level)
self.lowBatteryNotificationShown = true
}
if status == "BATTERY_AVAILABLE" && level > 25 && UserDefaults.standard.integer(forKey: "testMode") == 0 {
self.lowBatteryNotificationShown = false
if let level = battery["level"] as? Int {
let notifyOnLowBattery = UserDefaults.standard.bool(forKey: "notifyOnLowBattery")
if notifyOnLowBattery && ((status == "BATTERY_AVAILABLE" && level <= 25)) && !self.lowBatteryNotificationShown {
self.showLowBatteryNotification(level: level)
self.lowBatteryNotificationShown = true
}
if status == "BATTERY_AVAILABLE" && level > 25 && UserDefaults.standard.integer(forKey: "testMode") == 0 {
self.lowBatteryNotificationShown = false
}
}
}
DispatchQueue.main.async {
Expand All @@ -213,6 +212,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSWindowDele
}
}

private func batteryChargeText(from battery: [String: Any]) -> String? {
let status = battery["status"] as? String ?? ""
let isCharging = status == "BATTERY_CHARGING"
let prefix = isCharging ? "⚡︎ " : ""

if let level = battery["level"] as? Int, level >= 0 {
return prefix + "\(level)%"
}

return isCharging ? "⚡︎" : nil
}

// Helper to format time_to_empty_min into a submenu suffix like " (5h)" or " (<1h)".
// - Accepts Int/Double/String values from JSON and returns an optional suffix with a leading space.
private func formatTimeToEmpty(minutesAny: Any?) -> String? {
Expand Down Expand Up @@ -290,10 +301,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSWindowDele
menu.addItem(withTitle: String(format: "%@: %@", NSLocalizedString("Device", comment: "Device label"), deviceName), action: nil, keyEquivalent: "")
menu.addItem(withTitle: String(format: "%@: %@", NSLocalizedString("Vendor", comment: "Vendor label"), vendor), action: nil, keyEquivalent: "")
menu.addItem(withTitle: String(format: "%@: %@", NSLocalizedString("Product", comment: "Product label"), product), action: nil, keyEquivalent: "")
if let battery = device["battery"] as? [String: Any], let level = battery["level"] as? Int {
if let battery = device["battery"] as? [String: Any], let batteryText = batteryChargeText(from: battery) {
// Append time-to-empty in hours (submenu only) when available. Use floor rounding and "h" suffix; show "<1h" for under 60 minutes.
let suffix = formatTimeToEmpty(minutesAny: battery["time_to_empty_min"])
let title = String(format: "%@: %d%%%@", NSLocalizedString("Battery", comment: "Battery label"), level, suffix ?? "")
let title = String(format: "%@: %@%@", NSLocalizedString("Battery", comment: "Battery label"), batteryText, suffix ?? "")
menu.addItem(withTitle: title, action: nil, keyEquivalent: "")
}
if let chatmix = device["chatmix"] {
Expand Down
2 changes: 1 addition & 1 deletion HeadsetControl-MacOSTray/BuildNumber.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
CURRENT_PROJECT_VERSION = 260328.0834
CURRENT_PROJECT_VERSION = 260410.0948
28 changes: 23 additions & 5 deletions HeadsetControl-MacOSTray/HeadsetControlService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,35 @@ struct HeadsetCapability {
}

private func legacyBatteryStatusString(_ status: hsc_battery_status_t) -> String? {
switch status {
case HSC_BATTERY_AVAILABLE: return "BATTERY_AVAILABLE"
case HSC_BATTERY_CHARGING: return "BATTERY_CHARGING"
case HSC_BATTERY_UNAVAILABLE: return "BATTERY_UNAVAILABLE"
default: return nil
switch status.rawValue {
case HSC_BATTERY_AVAILABLE.rawValue:
return "BATTERY_AVAILABLE"
case HSC_BATTERY_CHARGING.rawValue, 1:
return "BATTERY_CHARGING"
case HSC_BATTERY_UNAVAILABLE.rawValue:
return "BATTERY_UNAVAILABLE"
case HSC_BATTERY_ERROR.rawValue:
return "BATTERY_ERROR"
case HSC_BATTERY_TIMEOUT.rawValue:
return "BATTERY_TIMEOUT"
default:
return nil
}
}

final class HeadsetControlService: HeadsetControlProviding {
private let libraryLock = NSLock()

func setTestProfile(_ profile: Int) {
let normalizedProfile = max(0, profile)

libraryLock.lock()
defer { libraryLock.unlock() }

hsc_set_test_profile(Int32(normalizedProfile))
hsc_enable_test_device(normalizedProfile != 0)
}

func fetchDevices() -> [[String: Any]] {
return withDiscoveredHeadsets { headsets in
var devices: [[String: Any]] = []
Expand Down
17 changes: 10 additions & 7 deletions HeadsetControl-MacOSTray/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,24 +146,27 @@ struct SettingsView: View {
HStack(alignment: .center) {
Text(NSLocalizedString("Test Mode:", comment: "Test mode label"))
Picker("", selection: $testMode) {
Text(NSLocalizedString("Device 1", comment: "Test mode device 1"))
Text(NSLocalizedString("1 - Error conditions", comment: "Test mode 1"))
.tag(1)
Text(NSLocalizedString("Device 2", comment: "Test mode device 2"))
Text(NSLocalizedString("2 - Charging battery", comment: "Test mode 2"))
.tag(2)
Text(NSLocalizedString("Device 3", comment: "Test mode device 3"))
Text(NSLocalizedString("3 - Basic battery", comment: "Test mode 3"))
.tag(3)
Text(NSLocalizedString("Device 4", comment: "Test mode device 4"))
Text(NSLocalizedString("4 - Battery unavailable", comment: "Test mode 4"))
.tag(4)
Text(NSLocalizedString("Device 5", comment: "Test mode device 5"))
Text(NSLocalizedString("5 - Timeout", comment: "Test mode 5"))
.tag(5)
Text(NSLocalizedString("Device 6", comment: "Test mode device 6"))
Text(NSLocalizedString("6 - Full battery", comment: "Test mode 6"))
.tag(6)
Text(NSLocalizedString("Device 7", comment: "Test mode device 7"))
Text(NSLocalizedString("7 - Low battery", comment: "Test mode 7"))
.tag(7)
Text(NSLocalizedString("Disabled", comment: "Test mode disabled"))
.tag(0)
}
.pickerStyle(.menu)
.onChange(of: testMode) { _, _ in
NotificationCenter.default.post(name: .refreshHeadsetStatus, object: nil)
}
}

HStack(alignment: .center, spacing: 8) {
Expand Down
120 changes: 60 additions & 60 deletions HeadsetControl-MacOSTray/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
@@ -1,75 +1,75 @@
"HeadsetControl-MacOSTray" = "HeadsetControl-MacOSTray";
"Headset" = "Headset";
"General Settings" = "Allgemeine Einstellungen";
"Test Mode:" = "Testmodus:";
"Disabled" = "Deaktiviert";
"Update Interval (seconds):" = "Aktualisierungsintervall (Sekunden):";
"%d s" = "%d s";
"Sidetone" = "Mithörton";
"Sidetone Level Values" = "Mithörton-Werte";
"Valid range: -1...128. Use -1 to hide a menu entry." = "Gültiger Bereich: -1...128. Mit -1 wird ein Menüeintrag ausgeblendet.";
"Off:" = "Aus:";
"Off" = "Aus";
"Low:" = "Niedrig:";
"Low" = "Niedrig";
"Medium:" = "Mittel:";
"Medium" = "Mittel";
"High:" = "Hoch:";
"High" = "Hoch";
"Maximum:" = "Maximal:";
"Maximum" = "Maximal";
"Refresh" = "Aktualisieren";
"Close" = "Schließen";
"No devices found" = "Keine Geräte gefunden";
"Settings..." = "Einstellungen...";
"Quit" = "Beenden";
"Device" = "Gerät";
"Unknown Device" = "Unbekanntes Gerät";
"Vendor" = "Hersteller";
"Unknown Vendor" = "Unbekannter Hersteller";
"Product" = "Produkt";
"Unknown Product" = "Unbekanntes Produkt";
"Battery" = "Batterie";
"Chatmix" = "Chatmix";
"Lights" = "LED";
"Inactive Time" = "Inaktivitätszeit";
"Voice Prompts" = "Sprachansagen";
"Rotate to Mute" = "Drehen zum Stummschalten";
"Equalizer Preset" = "Equalizer-Voreinstellung";
"Equalizer" = "Equalizer";
"On" = "An";
"%dh" = "%d Std.";
"(Off is always included.)" = "(Aus ist immer enthalten.)";
"1 - Error conditions" = "1 - Fehlerzustände";
"1 Minute" = "1 Minute";
"2 Minutes" = "2 Minuten";
"5 Minutes" = "5 Minuten";
"10 Minutes" = "10 Minuten";
"15 Minutes" = "15 Minuten";
"2 - Charging battery" = "2 - Akku wird geladen";
"2 Minutes" = "2 Minuten";
"3 - Basic battery" = "3 - Einfacher Batteriestatus";
"30 Minutes" = "30 Minuten";
"4 - Battery unavailable" = "4 - Batterie nicht verfügbar";
"45 Minutes" = "45 Minuten";
"5 - Timeout" = "5 - Zeitüberschreitung";
"5 Minutes" = "5 Minuten";
"6 - Full battery" = "6 - Voller Akku";
"60 Minutes" = "60 Minuten";
"7 - Low battery" = "7 - Niedriger Batteriestand";
"75 Minutes" = "75 Minuten";
"90 Minutes" = "90 Minuten";
"<1h" = "<1 Std.";
"About" = "Über";
"Battery" = "Batterie";
"Build" = "Build";
"Chatmix" = "Chatmix";
"Close" = "Schließen";
"Comma-separated list of preset names." = "Kommagetrennte Liste von Voreinstellungsnamen.";
"Device" = "Gerät";
"Disabled" = "Deaktiviert";
"Equalizer" = "Equalizer";
"Equalizer Preset" = "Equalizer-Voreinstellung";
"Equalizer Presets" = "Equalizer-Voreinstellungen";
"General" = "Allgemein";
"General Settings" = "Allgemeine Einstellungen";
"GitHub Repository" = "GitHub-Repository";
"Headset" = "Headset";
"HeadsetControl-MacOSTray" = "HeadsetControl-MacOSTray";
"High" = "Hoch";
"High:" = "Hoch:";
"Inactive Time" = "Inaktivitätszeit";
"Lights" = "LED";
"Low" = "Niedrig";
"Low battery notification message" = "Batteriestand ist niedrig (%d%%). Bitte laden Sie Ihr Headset auf.";
"Low:" = "Niedrig:";
"Maximum" = "Maximal";
"Maximum:" = "Maximal:";
"Medium" = "Mittel";
"Medium:" = "Mittel:";
"No devices found" = "Keine Geräte gefunden";
"Notification on low battery" = "Benachrichtigung bei niedrigem Batteriestand";
"Off" = "Aus";
"Off:" = "Aus:";
"On" = "An";
"Preset 1" = "Voreinstellung 1";
"Preset 2" = "Voreinstellung 2";
"Preset 3" = "Voreinstellung 3";
"Preset 4" = "Voreinstellung 4";
"Preset names" = "Voreinstellungsnamen";
"Comma-separated list of preset names." = "Kommagetrennte Liste von Voreinstellungsnamen.";
"Notification on low battery" = "Benachrichtigung bei niedrigem Batteriestand";
"Low battery notification message" = "Batteriestand ist niedrig (%d%%). Bitte laden Sie Ihr Headset auf.";
"Device 1" = "Gerät 1";
"Device 2" = "Gerät 2";
"Device 3" = "Gerät 3";
"Device 4" = "Gerät 4";
"Device 5" = "Gerät 5";
"Device 6" = "Gerät 6";
"Device 7" = "Gerät 7";
"Equalizer Presets" = "Equalizer-Voreinstellungen";
"<1h" = "<1 Std.";
"%dh" = "%d Std.";
"General" = "Allgemein";
"About" = "Über";
"(Off is always included.)" = "(Aus ist immer enthalten.)";
"GitHub Repository" = "GitHub-Repository";
"Version" = "Version";
"Build" = "Build";
"Product" = "Produkt";
"Quit" = "Beenden";
"Refresh" = "Aktualisieren";
"Rotate to Mute" = "Drehen zum Stummschalten";
"Settings or main app window" = "Einstellungs- oder Hauptfenster der App";
"Settings..." = "Einstellungen...";
"Sidetone" = "Mithörton";
"Sidetone Level Values" = "Mithörton-Werte";
"Test Mode:" = "Testmodus:";
"Unknown Device" = "Unbekanntes Gerät";
"Unknown Product" = "Unbekanntes Produkt";
"Unknown Vendor" = "Unbekannter Hersteller";
"Update Interval (seconds):" = "Aktualisierungsintervall (Sekunden):";
"Valid range: -1...128. Use -1 to hide a menu entry." = "Gültiger Bereich: -1...128. Mit -1 wird ein Menüeintrag ausgeblendet.";
"Vendor" = "Hersteller";
"Version" = "Version";
"Voice Prompts" = "Sprachansagen";
Loading