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
21 changes: 9 additions & 12 deletions ScreenTranslate.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
862C98FC2F32309800ABAC92 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
862C98FC2F32309800ABAC92 /* Exceptions for "ScreenTranslate" folder in "ScreenTranslate" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
"Supporting Files/Info.plist",
Expand All @@ -40,7 +40,7 @@
SC000002 /* ScreenTranslate */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
862C98FC2F32309800ABAC92 /* PBXFileSystemSynchronizedBuildFileExceptionSet */,
862C98FC2F32309800ABAC92 /* Exceptions for "ScreenTranslate" folder in "ScreenTranslate" target */,
);
path = ScreenTranslate;
sourceTree = "<group>";
Expand All @@ -62,6 +62,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
SC000025 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
Expand Down Expand Up @@ -236,16 +243,6 @@
};
/* End PBXSourcesBuildPhase section */

/* Begin PBXFrameworksBuildPhase section */
SC000025 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXTargetDependency section */
SC000033 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
Expand Down
18 changes: 18 additions & 0 deletions ScreenTranslate/App/Coordinators/CaptureCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ final class CaptureCoordinator {
Task {
defer { isCaptureInProgress = false }

guard await CaptureManager.shared.hasPermission else {
logger.info("Screen recording permission denied, triggering drag-to-authorize flow")
PermissionManager.shared.ensureScreenRecordingPermission()
return
}

do {
// Get available displays
let displays = try await CaptureManager.shared.availableDisplays()
Expand Down Expand Up @@ -105,6 +111,12 @@ final class CaptureCoordinator {
isCaptureInProgress = true

Task {
guard await CaptureManager.shared.hasPermission else {
logger.info("Screen recording permission denied, triggering drag-to-authorize flow")
PermissionManager.shared.ensureScreenRecordingPermission()
isCaptureInProgress = false
return
}
do {
// Present the selection overlay on all displays
let overlayController = SelectionOverlayController.shared
Expand Down Expand Up @@ -144,6 +156,12 @@ final class CaptureCoordinator {
isCaptureInProgress = true

Task {
guard await CaptureManager.shared.hasPermission else {
logger.info("Screen recording permission denied, triggering drag-to-authorize flow")
PermissionManager.shared.ensureScreenRecordingPermission()
isCaptureInProgress = false
return
}
do {
let overlayController = SelectionOverlayController.shared

Expand Down
12 changes: 2 additions & 10 deletions ScreenTranslate/App/Coordinators/TextTranslationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,8 @@ final class TextTranslationCoordinator {
permissionManager.refreshPermissionStatus()

if !permissionManager.hasAccessibilityPermission {
// Directly trigger system permission prompt
permissionManager.requestAccessibilityPermission()
permissionManager.refreshPermissionStatus()

if permissionManager.hasAccessibilityPermission {
return true
}

// Still not granted (user denied) — guide to System Settings
permissionManager.showPermissionDeniedError(for: .accessibility)
// Call the drag-to-authorize flow for accessibility
permissionManager.ensureAccessibilityPermissionFlow()
return false
}
return true
Expand Down
13 changes: 13 additions & 0 deletions ScreenTranslate/Extensions/URLExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

extension URL {
/// Automatically resolves localhost to 127.0.0.1 to avoid IPv6 resolution issues (connection refused ::1)
var resolvingLocalhost: URL {
guard let host = self.host, host.lowercased() == "localhost" else {
return self
}
var components = URLComponents(url: self, resolvingAgainstBaseURL: true)
components?.host = "127.0.0.1"
return components?.url ?? self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ struct CompatibleEngineConfigSheet: View {
@State private var testResult: String?
@State private var testSuccess = false

@State private var availableModels: [String] = []
@State private var isFetchingModels = false
@State private var showErrorAlert = false
@State private var errorMessage = ""

var body: some View {
VStack(spacing: 20) {
// Header
Expand Down Expand Up @@ -72,8 +77,40 @@ struct CompatibleEngineConfigSheet: View {
.font(.subheadline)
.foregroundStyle(.secondary)

TextField("gpt-4o-mini", text: $modelName)
.textFieldStyle(.roundedBorder)
HStack {
TextField("gpt-4o-mini", text: $modelName)
.textFieldStyle(.roundedBorder)

if !availableModels.isEmpty {
Menu {
ForEach(availableModels, id: \.self) { model in
Button(model) {
modelName = model
}
}
} label: {
Text("")
.frame(width: 8, height: 12)
}
.menuStyle(.borderlessButton)
.fixedSize()
}

Button {
Task {
await fetchModels()
}
} label: {
if isFetchingModels {
ProgressView()
.controlSize(.small)
} else {
Text(localized("engine.config.fetchModels"))
}
}
.buttonStyle(.bordered)
.disabled(isFetchingModels)
}
}

// API Key Toggle
Expand Down Expand Up @@ -162,6 +199,13 @@ struct CompatibleEngineConfigSheet: View {
.onAppear {
loadConfig()
}
.alert(isPresented: $showErrorAlert) {
Alert(
title: Text(localized("engine.config.fetchModels.failed")),
message: Text(errorMessage),
dismissButton: .default(Text(localized("button.ok")))
)
}
}

// MARK: - Computed Properties
Expand Down Expand Up @@ -242,13 +286,11 @@ struct CompatibleEngineConfigSheet: View {
keychain: KeychainService.shared
)

let success = await provider.checkConnection()
try await provider.verifyConnection()

await MainActor.run {
testSuccess = success
testResult = success
? localized("engine.config.test.success")
: localized("engine.config.test.failed")
testSuccess = true
testResult = localized("engine.config.test.success")
isTesting = false
}
} catch {
Expand All @@ -259,4 +301,26 @@ struct CompatibleEngineConfigSheet: View {
}
}
}

@MainActor
private func fetchModels() async {
isFetchingModels = true
errorMessage = ""

let requestURL = baseURL.isEmpty ? "http://localhost:8000/v1" : baseURL

do {
let models = try await ModelDiscoveryService.fetchModels(
baseURL: requestURL,
apiKey: hasAPIKey ? apiKey : nil,
engineType: "custom"
)
self.availableModels = models
self.isFetchingModels = false
} catch {
self.errorMessage = error.localizedDescription
self.showErrorAlert = true
self.isFetchingModels = false
}
}
}
78 changes: 71 additions & 7 deletions ScreenTranslate/Features/Settings/EngineConfigSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ struct EngineConfigSheet: View {
@State private var testSuccess = false
private let logger = Logger.settings

@State private var availableModels: [String] = []
@State private var isFetchingModels = false
@State private var showErrorAlert = false
@State private var errorMessage = ""

var body: some View {
VStack(spacing: 20) {
// Header
Expand Down Expand Up @@ -132,6 +137,13 @@ struct EngineConfigSheet: View {
.onAppear {
loadConfig()
}
.alert(isPresented: $showErrorAlert) {
Alert(
title: Text(localized("engine.config.fetchModels.failed")),
message: Text(errorMessage),
dismissButton: .default(Text(localized("button.ok")))
)
}
}

// MARK: - View Components
Expand Down Expand Up @@ -255,8 +267,40 @@ struct EngineConfigSheet: View {
.font(.subheadline)
.foregroundStyle(.secondary)

TextField(engine.defaultModelName ?? "", text: $modelName)
.textFieldStyle(.roundedBorder)
HStack {
TextField(engine.defaultModelName ?? "", text: $modelName)
.textFieldStyle(.roundedBorder)

if !availableModels.isEmpty {
Menu {
ForEach(availableModels, id: \.self) { model in
Button(model) {
modelName = model
}
}
} label: {
Text("")
.frame(width: 8, height: 12)
}
.menuStyle(.borderlessButton)
.fixedSize()
}

Button {
Task {
await fetchModels()
}
} label: {
if isFetchingModels {
ProgressView()
.controlSize(.small)
} else {
Text(localized("engine.config.fetchModels"))
}
}
.buttonStyle(.bordered)
.disabled(isFetchingModels)
}
}
}

Expand Down Expand Up @@ -368,13 +412,11 @@ struct EngineConfigSheet: View {
}

// Test connection
let success = await TranslationService.shared.testConnection(for: engine)
try await TranslationService.shared.verifyConnection(for: engine)

await MainActor.run {
testSuccess = success
testResult = success
? localized("engine.config.test.success")
: localized("engine.config.test.failed")
testSuccess = true
testResult = localized("engine.config.test.success")
isTesting = false
}
} catch {
Expand All @@ -385,4 +427,26 @@ struct EngineConfigSheet: View {
}
}
}

@MainActor
private func fetchModels() async {
isFetchingModels = true
errorMessage = ""

let requestURL = baseURL.isEmpty ? (engine.defaultBaseURL ?? "") : baseURL

do {
let models = try await ModelDiscoveryService.fetchModels(
baseURL: requestURL,
apiKey: engine.requiresAPIKey ? apiKey : nil,
engineType: engine.rawValue
)
self.availableModels = models
self.isFetchingModels = false
} catch {
self.errorMessage = error.localizedDescription
self.showErrorAlert = true
self.isFetchingModels = false
}
}
}
Loading