Skip to content

Commit 3b1ec7e

Browse files
committed
v1.0.1 Fix Dialog crash on repeated use and improve stability
1 parent 4265ee0 commit 3b1ec7e

File tree

6 files changed

+155
-84
lines changed

6 files changed

+155
-84
lines changed

.github/workflows/release.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
tag:
7+
description: 'Release tag (e.g., v1.0)'
8+
required: true
9+
message:
10+
description: 'Release message'
11+
required: false
12+
default: ''
13+
14+
permissions:
15+
contents: write
16+
17+
jobs:
18+
build:
19+
runs-on: macos-latest
20+
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
25+
- name: Build and Package
26+
run: |
27+
chmod +x release.sh
28+
chmod +x InstantLinguaDialog/build.sh
29+
./release.sh all
30+
31+
- name: Create Release
32+
uses: softprops/action-gh-release@v1
33+
with:
34+
tag_name: ${{ github.event.inputs.tag }}
35+
name: ${{ github.event.inputs.tag }}
36+
body: ${{ github.event.inputs.message }}
37+
files: |
38+
InstantLingua.popclipextz
39+
InstantLingua-Dev.popclipextz
40+
env:
41+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Build artifacts
22
*.popclipextz
3-
InstantLinguaDialog/InstantLingua Dialog.app/
3+
InstantLingua Dialog.app/
44

55
# macOS
66
.DS_Store

InstantLinguaDialog/InstantLinguaDialog/main.swift

Lines changed: 71 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import NaturalLanguage
66
class AppDelegate: NSObject, NSApplicationDelegate {
77
var window: NSWindow?
88
var eventMonitor: Any?
9-
var handledURL = false
9+
private var _handledURL = false
10+
private var isTransitioning = false
11+
private let lock = NSLock()
12+
13+
var handledURL: Bool {
14+
get { lock.lock(); defer { lock.unlock() }; return _handledURL }
15+
set { lock.lock(); defer { lock.unlock() }; _handledURL = newValue }
16+
}
1017

1118
func applicationWillFinishLaunching(_ notification: Notification) {
1219
// Register URL handler early to catch URL events before applicationDidFinishLaunching
@@ -27,7 +34,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
2734
}
2835

2936
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
30-
return true
37+
// Don't terminate if we're transitioning to a new window
38+
return !isTransitioning
3139
}
3240

3341
func applicationWillTerminate(_ notification: Notification) {
@@ -75,6 +83,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
7583
}
7684

7785
func showDialog(text: String? = nil) {
86+
// Prevent app termination during window transition
87+
isTransitioning = true
88+
defer { isTransitioning = false }
89+
7890
// Clean up previous event monitor
7991
if let monitor = eventMonitor {
8092
NSEvent.removeMonitor(monitor)
@@ -147,7 +159,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
147159

148160
// Position near mouse cursor
149161
let mouseLocation = NSEvent.mouseLocation
150-
let screenFrame = NSScreen.main?.visibleFrame ?? .zero
162+
guard let screen = NSScreen.main ?? NSScreen.screens.first else {
163+
// Fallback: center on screen if no screen available
164+
panel.center()
165+
panel.makeKeyAndOrderFront(nil)
166+
window = panel
167+
NSApplication.shared.activate(ignoringOtherApps: true)
168+
return
169+
}
170+
let screenFrame = screen.visibleFrame
151171
var windowX = mouseLocation.x - estimatedWidth / 2
152172

153173
// Default: above mouse, fallback: below if near top edge
@@ -180,7 +200,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
180200
NSApplication.shared.activate(ignoringOtherApps: true)
181201

182202
// Keyboard shortcuts
183-
eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
203+
eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
204+
guard self != nil else { return event }
184205
// Escape to close
185206
if event.keyCode == 53 {
186207
NSApplication.shared.terminate(nil)
@@ -231,19 +252,52 @@ func detectLanguage(_ text: String) -> String {
231252
return languageNames[language] ?? language.rawValue.capitalized
232253
}
233254

255+
// MARK: - Speech Manager
256+
class SpeechManager: NSObject, ObservableObject, NSSpeechSynthesizerDelegate {
257+
@Published var speakingOriginal = false
258+
@Published var speakingTranslation = false
259+
260+
private let synthesizer = NSSpeechSynthesizer()
261+
262+
override init() {
263+
super.init()
264+
synthesizer.delegate = self
265+
}
266+
267+
func speak(_ text: String, isOriginal: Bool) {
268+
if synthesizer.isSpeaking {
269+
synthesizer.stopSpeaking()
270+
speakingOriginal = false
271+
speakingTranslation = false
272+
return
273+
}
274+
275+
if isOriginal {
276+
speakingOriginal = true
277+
} else {
278+
speakingTranslation = true
279+
}
280+
synthesizer.startSpeaking(text)
281+
}
282+
283+
func speechSynthesizer(_ sender: NSSpeechSynthesizer, didFinishSpeaking finishedSpeaking: Bool) {
284+
DispatchQueue.main.async { [weak self] in
285+
self?.speakingOriginal = false
286+
self?.speakingTranslation = false
287+
}
288+
}
289+
}
290+
234291
// MARK: - Dialog View
235292
struct DialogView: View {
236293
let text: String
237294
let onClose: () -> Void
238295
@State private var copiedOriginal = false
239296
@State private var copiedTranslation = false
240-
@State private var speakingOriginal = false
241-
@State private var speakingTranslation = false
242297
@State private var showingHelp = false
298+
@StateObject private var speechManager = SpeechManager()
243299
@Environment(\.colorScheme) private var colorScheme
244300

245-
private let synthesizer = NSSpeechSynthesizer()
246-
247301
// Adaptive opacity based on color scheme
248302
private var originalCardOpacity: Double {
249303
colorScheme == .dark ? 0.15 : 0.06
@@ -285,10 +339,10 @@ struct DialogView: View {
285339
.foregroundStyle(.secondary)
286340
Spacer()
287341
HStack(spacing: 12) {
288-
Button(action: { speakText(parts.original) }) {
289-
Image(systemName: speakingOriginal ? "stop.fill" : "speaker.wave.2")
342+
Button(action: { speechManager.speak(parts.original, isOriginal: true) }) {
343+
Image(systemName: speechManager.speakingOriginal ? "stop.fill" : "speaker.wave.2")
290344
.font(.system(size: 12))
291-
.foregroundStyle(speakingOriginal ? .orange : .secondary)
345+
.foregroundStyle(speechManager.speakingOriginal ? .orange : .secondary)
292346
.frame(width: 14, height: 14)
293347
}
294348
.buttonStyle(.plain)
@@ -321,10 +375,10 @@ struct DialogView: View {
321375
.foregroundStyle(.secondary)
322376
Spacer()
323377
HStack(spacing: 12) {
324-
Button(action: { speakText(parts.translation) }) {
325-
Image(systemName: speakingTranslation ? "stop.fill" : "speaker.wave.2")
378+
Button(action: { speechManager.speak(parts.translation, isOriginal: false) }) {
379+
Image(systemName: speechManager.speakingTranslation ? "stop.fill" : "speaker.wave.2")
326380
.font(.system(size: 12))
327-
.foregroundStyle(speakingTranslation ? .orange : .secondary)
381+
.foregroundStyle(speechManager.speakingTranslation ? .orange : .secondary)
328382
.frame(width: 14, height: 14)
329383
}
330384
.buttonStyle(.plain)
@@ -361,10 +415,10 @@ struct DialogView: View {
361415
.foregroundStyle(.secondary)
362416
Spacer()
363417
HStack(spacing: 12) {
364-
Button(action: { speakText(text) }) {
365-
Image(systemName: speakingOriginal ? "stop.fill" : "speaker.wave.2")
418+
Button(action: { speechManager.speak(text, isOriginal: true) }) {
419+
Image(systemName: speechManager.speakingOriginal ? "stop.fill" : "speaker.wave.2")
366420
.font(.system(size: 12))
367-
.foregroundStyle(speakingOriginal ? .orange : .secondary)
421+
.foregroundStyle(speechManager.speakingOriginal ? .orange : .secondary)
368422
.frame(width: 14, height: 14)
369423
}
370424
.buttonStyle(.plain)
@@ -467,44 +521,6 @@ struct DialogView: View {
467521
.frame(minWidth: 360, minHeight: 160)
468522
}
469523

470-
private func speakText(_ textToSpeak: String) {
471-
// Determine if this is original or translation text
472-
let isOriginal: Bool
473-
if let parts = bilingualParts {
474-
isOriginal = textToSpeak == parts.original
475-
} else {
476-
isOriginal = true // Single text mode - always treat as original
477-
}
478-
479-
// If already speaking this text, stop it
480-
if synthesizer.isSpeaking {
481-
synthesizer.stopSpeaking()
482-
speakingOriginal = false
483-
speakingTranslation = false
484-
return // Just stop, don't restart
485-
}
486-
487-
// Start speaking
488-
if isOriginal {
489-
speakingOriginal = true
490-
} else {
491-
speakingTranslation = true
492-
}
493-
494-
synthesizer.startSpeaking(textToSpeak)
495-
496-
// Monitor when speech finishes
497-
DispatchQueue.global().async {
498-
while synthesizer.isSpeaking {
499-
Thread.sleep(forTimeInterval: 0.1)
500-
}
501-
DispatchQueue.main.async {
502-
speakingOriginal = false
503-
speakingTranslation = false
504-
}
505-
}
506-
}
507-
508524
private func copyText(_ text: String, isTranslation: Bool = false) {
509525
NSPasteboard.general.clearContents()
510526
NSPasteboard.general.setString(text, forType: .string)

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ With built-in support for **OpenAI, Claude, Grok, Gemini, and Ollama (local)**,
1717
- **Custom Prompt** – Define your own AI instructions for custom text processing
1818
- **Multi-Provider Support** – Use OpenAI, Claude, Grok, Gemini, or Ollama (local) models for language tasks
1919
- **Paste Mode** – Paste results directly to cursor position
20+
- **Dialog Window** – Display results in a floating panel for longer content
2021

2122
## Tasks Overview
2223

@@ -81,7 +82,9 @@ InstantLingua enhances your PopClip experience with fast, AI-driven language too
8182

8283
## Installation
8384

84-
1. Download the latest release from the repository
85+
1. Download from [Releases](https://github.com/laurensent/InstantLingua/releases):
86+
- **InstantLingua.popclipextz** – Standard version with preset models
87+
- **InstantLingua-Dev.popclipextz** – Dev version with custom API endpoint and model ID support
8588
2. Double-click the .popclipextz file to install
8689
3. Configure your preferred AI provider API key in PopClip settings
8790

release-dev.sh

Lines changed: 0 additions & 19 deletions
This file was deleted.

release.sh

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,49 @@
11
#!/bin/bash
22

33
# InstantLingua Release Script
4+
# Usage: ./release.sh [all|main|dev]
45

56
set -e
67

78
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
89
cd "$SCRIPT_DIR"
910

10-
echo "Building Dialog app..."
11-
"$SCRIPT_DIR/InstantLinguaDialog/build.sh" > /dev/null
11+
build_dialog() {
12+
echo "Building Dialog app..."
13+
"$SCRIPT_DIR/InstantLinguaDialog/build.sh" > /dev/null
14+
}
1215

13-
echo "Packaging extension..."
14-
rm -rf "$SCRIPT_DIR/InstantLingua.popclipext/InstantLingua Dialog.app"
15-
cp -R "$SCRIPT_DIR/InstantLinguaDialog/InstantLingua Dialog.app" "$SCRIPT_DIR/InstantLingua.popclipext/"
16-
rm -f "$SCRIPT_DIR/InstantLingua.popclipextz"
17-
zip -rq InstantLingua.popclipextz InstantLingua.popclipext -x "*.DS_Store"
16+
package_main() {
17+
echo "Packaging InstantLingua..."
18+
rm -rf "$SCRIPT_DIR/InstantLingua.popclipext/InstantLingua Dialog.app"
19+
cp -R "$SCRIPT_DIR/InstantLinguaDialog/InstantLingua Dialog.app" "$SCRIPT_DIR/InstantLingua.popclipext/"
20+
rm -f "$SCRIPT_DIR/InstantLingua.popclipextz"
21+
zip -rq InstantLingua.popclipextz InstantLingua.popclipext -x "*.DS_Store"
22+
echo "Created: InstantLingua.popclipextz ($(du -h InstantLingua.popclipextz | cut -f1))"
23+
}
1824

19-
echo "Done: InstantLingua.popclipextz ($(du -h InstantLingua.popclipextz | cut -f1))"
25+
package_dev() {
26+
echo "Packaging InstantLingua-Dev..."
27+
rm -rf "$SCRIPT_DIR/InstantLingua-Dev.popclipext/InstantLingua Dialog.app"
28+
cp -R "$SCRIPT_DIR/InstantLinguaDialog/InstantLingua Dialog.app" "$SCRIPT_DIR/InstantLingua-Dev.popclipext/"
29+
rm -f "$SCRIPT_DIR/InstantLingua-Dev.popclipextz"
30+
zip -rq InstantLingua-Dev.popclipextz InstantLingua-Dev.popclipext -x "*.DS_Store"
31+
echo "Created: InstantLingua-Dev.popclipextz ($(du -h InstantLingua-Dev.popclipextz | cut -f1))"
32+
}
33+
34+
case "${1:-all}" in
35+
main)
36+
build_dialog
37+
package_main
38+
;;
39+
dev)
40+
build_dialog
41+
package_dev
42+
;;
43+
all|*)
44+
build_dialog
45+
package_main
46+
package_dev
47+
echo "Done!"
48+
;;
49+
esac

0 commit comments

Comments
 (0)