Add copy device information feature

This commit is contained in:
Beingpax 2025-10-05 17:40:55 +05:45
parent 4361597ee9
commit 3eed3e2f6b
4 changed files with 281 additions and 68 deletions

View File

@ -5,35 +5,29 @@ import AppKit
struct EmailSupport {
static func generateSupportEmailURL() -> URL? {
let subject = "VoiceInk Support Request"
let systemInfo = """
App Version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown")
macOS Version: \(ProcessInfo.processInfo.operatingSystemVersionString)
Device: \(getMacModel())
CPU: \(getCPUInfo())
Memory: \(getMemoryInfo())
"""
let systemInfo = SystemInfoService.shared.getSystemInfoString()
let body = """
------------------------
**SCREEN RECORDING HIGHLY RECOMMENDED**
Create a quick screen recording showing the issue!
It helps me understand and fix the problem much faster.
📝 ISSUE DETAILS:
- What steps did you take before the issue occurred?
- What did you expect to happen?
- What actually happened instead?
## 📋 COMMON ISSUES:
Check out our Common Issues page before sending an email: https://tryvoiceink.com/common-issues
------------------------
System Information:
\(systemInfo)
"""
let encodedSubject = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
@ -48,25 +42,5 @@ struct EmailSupport {
}
}
private static func getMacModel() -> String {
var size = 0
sysctlbyname("hw.model", nil, &size, nil, 0)
var machine = [CChar](repeating: 0, count: size)
sysctlbyname("hw.model", &machine, &size, nil, 0)
return String(cString: machine)
}
private static func getCPUInfo() -> String {
var size = 0
sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0)
var buffer = [CChar](repeating: 0, count: size)
sysctlbyname("machdep.cpu.brand_string", &buffer, &size, nil, 0)
return String(cString: buffer)
}
private static func getMemoryInfo() -> String {
let totalMemory = ProcessInfo.processInfo.physicalMemory
return ByteCountFormatter.string(fromByteCount: Int64(totalMemory), countStyle: .memory)
}
}

View File

@ -0,0 +1,194 @@
import Foundation
import AppKit
import AVFoundation
class SystemInfoService {
static let shared = SystemInfoService()
private init() {}
func getSystemInfoString() -> String {
let info = """
=== VOICEINK SYSTEM INFORMATION ===
Generated: \(Date().formatted(date: .long, time: .standard))
APP INFORMATION:
App Version: \(getAppVersion())
Build Version: \(getBuildVersion())
OPERATING SYSTEM:
macOS Version: \(ProcessInfo.processInfo.operatingSystemVersionString)
HARDWARE INFORMATION:
Device Model: \(getMacModel())
CPU: \(getCPUInfo())
Memory: \(getMemoryInfo())
Architecture: \(getArchitecture())
AUDIO SETTINGS:
Input Mode: \(getAudioInputMode())
Current Audio Device: \(getCurrentAudioDevice())
Available Audio Devices: \(getAvailableAudioDevices())
HOTKEY SETTINGS:
Primary Hotkey: \(getPrimaryHotkey())
Secondary Hotkey: \(getSecondaryHotkey())
TRANSCRIPTION SETTINGS:
Current Model: \(getCurrentTranscriptionModel())
AI Enhancement: \(getAIEnhancementStatus())
AI Provider: \(getAIProvider())
AI Model: \(getAIModel())
PERMISSIONS:
Accessibility: \(getAccessibilityStatus())
Screen Recording: \(getScreenRecordingStatus())
Microphone: \(getMicrophoneStatus())
"""
return info
}
func copySystemInfoToClipboard() {
let info = getSystemInfoString()
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(info, forType: .string)
}
private func getAppVersion() -> String {
return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
}
private func getBuildVersion() -> String {
return Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
}
private func getMacModel() -> String {
var size = 0
sysctlbyname("hw.model", nil, &size, nil, 0)
var machine = [CChar](repeating: 0, count: size)
sysctlbyname("hw.model", &machine, &size, nil, 0)
return String(cString: machine)
}
private func getCPUInfo() -> String {
var size = 0
sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0)
var buffer = [CChar](repeating: 0, count: size)
sysctlbyname("machdep.cpu.brand_string", &buffer, &size, nil, 0)
return String(cString: buffer)
}
private func getMemoryInfo() -> String {
let totalMemory = ProcessInfo.processInfo.physicalMemory
return ByteCountFormatter.string(fromByteCount: Int64(totalMemory), countStyle: .memory)
}
private func getArchitecture() -> String {
#if arch(x86_64)
return "Intel x86_64"
#elseif arch(arm64)
return "Apple Silicon (ARM64)"
#else
return "Unknown"
#endif
}
private func getAudioInputMode() -> String {
if let mode = UserDefaults.standard.audioInputModeRawValue,
let audioMode = AudioInputMode(rawValue: mode) {
return audioMode.rawValue
}
return "System Default"
}
private func getCurrentAudioDevice() -> String {
let audioManager = AudioDeviceManager.shared
if let deviceID = audioManager.selectedDeviceID ?? audioManager.fallbackDeviceID,
let deviceName = audioManager.getDeviceName(deviceID: deviceID) {
return deviceName
}
return "System Default"
}
private func getAvailableAudioDevices() -> String {
let devices = AudioDeviceManager.shared.availableDevices
if devices.isEmpty {
return "None detected"
}
return devices.map { $0.name }.joined(separator: ", ")
}
private func getPrimaryHotkey() -> String {
if let hotkeyRaw = UserDefaults.standard.string(forKey: "selectedHotkey1"),
let hotkey = HotkeyManager.HotkeyOption(rawValue: hotkeyRaw) {
return hotkey.displayName
}
return "Right Command"
}
private func getSecondaryHotkey() -> String {
if let hotkeyRaw = UserDefaults.standard.string(forKey: "selectedHotkey2"),
let hotkey = HotkeyManager.HotkeyOption(rawValue: hotkeyRaw) {
return hotkey.displayName
}
return "None"
}
private func getCurrentTranscriptionModel() -> String {
if let modelName = UserDefaults.standard.string(forKey: "CurrentTranscriptionModel") {
if let model = PredefinedModels.models.first(where: { $0.name == modelName }) {
return model.displayName
}
return modelName
}
return "No model selected"
}
private func getAIEnhancementStatus() -> String {
let enhancementEnabled = UserDefaults.standard.bool(forKey: "isAIEnhancementEnabled")
return enhancementEnabled ? "Enabled" : "Disabled"
}
private func getAIProvider() -> String {
if let providerRaw = UserDefaults.standard.string(forKey: "selectedAIProvider") {
return providerRaw
}
return "None selected"
}
private func getAIModel() -> String {
if let providerRaw = UserDefaults.standard.string(forKey: "selectedAIProvider") {
let modelKey = "\(providerRaw)SelectedModel"
if let savedModel = UserDefaults.standard.string(forKey: modelKey), !savedModel.isEmpty {
return savedModel
}
return "Default (\(providerRaw))"
}
return "None selected"
}
private func getAccessibilityStatus() -> String {
return AXIsProcessTrusted() ? "Granted" : "Not Granted"
}
private func getScreenRecordingStatus() -> String {
return CGPreflightScreenCaptureAccess() ? "Granted" : "Not Granted"
}
private func getMicrophoneStatus() -> String {
switch AVCaptureDevice.authorizationStatus(for: .audio) {
case .authorized:
return "Granted"
case .denied:
return "Denied"
case .restricted:
return "Restricted"
case .notDetermined:
return "Not Determined"
@unknown default:
return "Unknown"
}
}
}

View File

@ -48,13 +48,13 @@ struct LicenseManagementView: View {
}
}
Text(licenseViewModel.licenseState == .licensed ?
Text(licenseViewModel.licenseState == .licensed ?
"Thank you for supporting VoiceInk" :
"Transcribe what you say to text instantly with AI")
.font(.title3)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
if case .licensed = licenseViewModel.licenseState {
HStack(spacing: 40) {
Button {
@ -190,13 +190,13 @@ struct LicenseManagementView: View {
VStack(spacing: 20) {
Text("Already purchased?")
.font(.headline)
HStack(spacing: 12) {
Text("Manage your license and device activations")
.font(.subheadline)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
Button(action: {
if let url = URL(string: "https://polar.sh/beingpax/portal/request") {
NSWorkspace.shared.open(url)
@ -253,7 +253,7 @@ struct LicenseManagementView: View {
VStack(alignment: .leading, spacing: 16) {
Text("License Management")
.font(.headline)
Button(role: .destructive, action: {
licenseViewModel.removeLicense()
}) {

View File

@ -113,37 +113,40 @@ struct TimeEfficiencyView: View {
}
private var reportIssueButton: some View {
Button(action: {
EmailSupport.openSupportEmail()
}) {
HStack(alignment: .center, spacing: 12) {
// Left icon
Image(systemName: "exclamationmark.bubble.fill")
.font(.system(size: 20, weight: .medium))
.foregroundStyle(.white)
// Center text
Text("Feedback or Issues?")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.white)
Spacer(minLength: 8)
// Right button
Text("Report")
.font(.system(size: 12, weight: .medium))
.foregroundColor(Color.accentColor)
.padding(.horizontal, 12)
.padding(.vertical, 4)
.background(Capsule().fill(.white))
ZStack {
Button(action: {
EmailSupport.openSupportEmail()
}) {
HStack(alignment: .center, spacing: 12) {
// Left icon
Image(systemName: "exclamationmark.bubble.fill")
.font(.system(size: 20, weight: .medium))
.foregroundStyle(.white)
// Center text
Text("Feedback or Issues?")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.white)
Spacer(minLength: 8)
}
.padding(.vertical, 10)
.padding(.horizontal, 12)
.background(accentGradient)
.cornerRadius(10)
}
.padding(.vertical, 10)
.padding(.horizontal, 12)
.background(accentGradient)
.cornerRadius(10)
.buttonStyle(.plain)
.shadow(color: Color.accentColor.opacity(0.2), radius: 3, y: 1)
.frame(maxWidth: 280)
// Copy system info button overlaid and centered vertically
HStack {
Spacer()
CopySystemInfoButton()
.padding(.trailing, 8)
}
.frame(maxWidth: 280)
}
.buttonStyle(.plain)
.shadow(color: Color.accentColor.opacity(0.2), radius: 3, y: 1)
.frame(maxWidth: 280)
}
@ -220,3 +223,45 @@ struct TimeBlockView: View {
)
}
}
// MARK: - Copy System Info Button
private struct CopySystemInfoButton: View {
@State private var isCopied: Bool = false
var body: some View {
Button(action: {
copySystemInfo()
}) {
Image(systemName: isCopied ? "checkmark" : "doc.on.doc")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white.opacity(0.9))
.padding(8)
.background(
Circle()
.fill(Color.white.opacity(0.2))
.overlay(
Circle()
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
)
.rotationEffect(.degrees(isCopied ? 360 : 0))
}
.buttonStyle(.plain)
.scaleEffect(isCopied ? 1.1 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: isCopied)
}
private func copySystemInfo() {
SystemInfoService.shared.copySystemInfoToClipboard()
withAnimation {
isCopied = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation {
isCopied = false
}
}
}
}