Skip to content

Commit 8bb780d

Browse files
authored
feat: Add small popup translation (#20)
* feat: update settings and UI for shortcut window type - Mark Windows support as complete in README. - Refactor settings to replace double-click functionality with shortcut window type selection. - Implement shortcut window type preference in settings and update related components. - Enhance mouse event handling to manage clicks outside the selection icon. - Introduce global shortcuts for quick translation with debounce logic. - Improve window management for translate popup and selection icon. * chore: update FE dependencies to latest versions * chore: bump version to 0.1.0 in tauri configuration * fix: improve publish workflow to dynamically set Rust targets based on platform and architecture
1 parent 8709a48 commit 8bb780d

File tree

20 files changed

+2325
-1914
lines changed

20 files changed

+2325
-1914
lines changed

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ jobs:
3939
- name: install Rust stable
4040
uses: dtolnay/rust-toolchain@stable
4141
with:
42-
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
43-
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
42+
# Add targets based on the platform and target architecture
43+
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || (contains(matrix.args, 'aarch64-pc-windows-msvc') && 'aarch64-pc-windows-msvc') || '' }}
4444

4545
- name: install frontend dependencies
4646
run: pnpm install # change this to npm, pnpm or bun depending on which one you use.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Features:
99
- [x] Providers: OpenAI, Ollama, Claude (WIP).
1010
- [x] Hot key: Quickly translate a selected text via shortcut key (Currently is: `Cmd + E`)
1111
- [] Linux support.
12-
- [] Windows support.
12+
- [x] Windows support.
1313
- [WIP] Custom prompt: Adjust the translation or text refining in different style.
1414

1515
***Note***: This app is still in development

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,16 @@
3131
"@tauri-apps/plugin-store": "2.3.0",
3232
"clipboard": "link:@tauri-apps\\api\\clipboard",
3333
"framer-motion": "^11.18.2",
34-
"next": "14.2.15",
35-
"react": "^18.3.1",
36-
"react-dom": "^18.3.1",
34+
"next": "15.3.5",
35+
"react": "^19.0.0",
36+
"react-dom": "^19.1.0",
3737
"react-icons": "^5.5.0"
3838
},
3939
"devDependencies": {
4040
"@tauri-apps/cli": "2.6.2",
4141
"@types/node": "^20.19.4",
42-
"@types/react": "^18.3.23",
43-
"@types/react-dom": "^18.3.7",
42+
"@types/react": "^19.0.10",
43+
"@types/react-dom": "^19.0.4",
4444
"eslint": "^9.30.1",
4545
"eslint-config-next": "14.2.3",
4646
"postcss": "^8.5.6",

pnpm-lock.yaml

Lines changed: 1683 additions & 1470 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/src/commands.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,22 +144,22 @@ pub async fn refine(
144144
}
145145

146146
#[tauri::command]
147-
pub async fn get_double_click_enabled(app_handle: tauri::AppHandle) -> Result<bool, String> {
147+
pub async fn get_shortcut_window_type(app_handle: tauri::AppHandle) -> Result<String, String> {
148148
let store = app_handle.store("store.bin").map_err(|e| format!("Failed to get store: {}", e))?;
149-
match store.get("DOUBLE_CLICK_ENABLED") {
149+
match store.get("SHORTCUT_WINDOW_TYPE") {
150150
Some(value) => {
151-
if let Some(enabled) = value.as_bool() {
152-
Ok(enabled)
151+
if let Some(window_type) = value.as_str() {
152+
Ok(window_type.to_string())
153153
} else {
154-
Ok(false) // Default to false if value is not a boolean
154+
Ok("main".to_string()) // Default to main if value is not a string
155155
}
156156
},
157-
None => Ok(false), // Default to false if key doesn't exist
157+
None => Ok("main".to_string()), // Default to main if key doesn't exist
158158
}
159159
}
160160

161161
#[tauri::command]
162-
pub async fn save_settings(app_handle: tauri::AppHandle, api_key: Option<String>, double_click_enabled: bool) -> Result<(), String> {
162+
pub async fn save_settings(app_handle: tauri::AppHandle, api_key: Option<String>, shortcut_window_type: Option<String>) -> Result<(), String> {
163163
let store = app_handle.store("store.bin").map_err(|e| format!("Failed to get store: {}", e))?;
164164

165165
if let Some(key) = api_key {
@@ -168,7 +168,9 @@ pub async fn save_settings(app_handle: tauri::AppHandle, api_key: Option<String>
168168
}
169169
}
170170

171-
store.set("DOUBLE_CLICK_ENABLED", double_click_enabled);
171+
if let Some(window_type) = shortcut_window_type {
172+
store.set("SHORTCUT_WINDOW_TYPE", window_type);
173+
}
172174

173175
store.save().map_err(|e| format!("Failed to save store: {}", e))?;
174176

src-tauri/src/event_handlers.rs

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
use std::sync::Arc;
2+
use tauri::{AppHandle, Emitter, Listener, Manager};
3+
use device_query::{DeviceQuery, DeviceState};
4+
use crate::mouse_service::{MouseService, MouseEvent};
5+
use crate::window_management::{
6+
create_selection_icon, create_or_focus_compact_window,
7+
get_current_active_app, restore_focus_to_app
8+
};
9+
10+
#[cfg(target_os = "windows")]
11+
use crate::window_management::{get_current_active_window, restore_focus_to_window};
12+
13+
pub fn setup_selection_icon_events(
14+
app_handle: &AppHandle,
15+
mouse_service: &Arc<MouseService>
16+
) {
17+
let selection_app_handle = app_handle.clone();
18+
let mouse_service_for_icon = mouse_service.clone();
19+
20+
app_handle.listen("create-selection-icon", move |event| {
21+
println!("Received create-selection-icon event from mouse service");
22+
if let Ok(payload) = serde_json::from_str::<serde_json::Value>(event.payload()) {
23+
if let (Some(text), Some(x), Some(y)) = (
24+
payload.get("text").and_then(|v| v.as_str()).map(|s| s.to_string()),
25+
payload.get("x").and_then(|v| v.as_i64()),
26+
payload.get("y").and_then(|v| v.as_i64())
27+
) {
28+
// Get the current active app/window BEFORE showing the selection icon
29+
#[cfg(target_os = "macos")]
30+
let current_app = get_current_active_app();
31+
32+
#[cfg(target_os = "windows")]
33+
let current_window = get_current_active_window();
34+
35+
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
36+
let _current_app: Option<String> = None;
37+
38+
let app_handle = selection_app_handle.clone();
39+
let mouse_service_ref = mouse_service_for_icon.clone();
40+
tauri::async_runtime::spawn(async move {
41+
match create_selection_icon(&app_handle, x as i32, y as i32).await {
42+
Ok(window) => {
43+
// Store the selected text in the window's state
44+
if let Err(e) = window.emit("set-selected-text", &text) {
45+
println!("Failed to emit set-selected-text event: {}", e);
46+
}
47+
if let Err(e) = window.show() {
48+
println!("Failed to show selection icon window: {}", e);
49+
}
50+
51+
// Track the selection icon bounds and visibility
52+
let icon_size = 24; // Icon size from create_selection_icon
53+
mouse_service_ref.set_selection_icon_bounds(
54+
x as i32,
55+
y as i32,
56+
icon_size,
57+
icon_size
58+
);
59+
mouse_service_ref.set_selection_icon_visible(true);
60+
println!("Selection icon bounds set: ({}, {}, {}, {})", x, y, icon_size, icon_size);
61+
62+
// After showing the icon, restore focus to the original app/window
63+
std::thread::sleep(std::time::Duration::from_millis(100));
64+
65+
#[cfg(target_os = "macos")]
66+
if let Some(app_name) = current_app {
67+
restore_focus_to_app(&app_name);
68+
println!("Restored focus to application: {}", app_name);
69+
}
70+
71+
#[cfg(target_os = "windows")]
72+
if let Some(hwnd) = current_window {
73+
restore_focus_to_window(hwnd);
74+
println!("Restored focus to previous window");
75+
}
76+
},
77+
Err(e) => {
78+
println!("Failed to create selection icon window: {}", e);
79+
}
80+
}
81+
});
82+
} else {
83+
println!("Invalid payload for create-selection-icon event");
84+
}
85+
} else {
86+
println!("Failed to parse create-selection-icon event payload");
87+
}
88+
});
89+
}
90+
91+
pub fn setup_icon_click_events(
92+
app_handle: &AppHandle,
93+
mouse_service: &Arc<MouseService>
94+
) {
95+
let icon_app_handle = app_handle.clone();
96+
let mouse_service_for_click = mouse_service.clone();
97+
98+
app_handle.listen("icon-clicked", move |event| {
99+
if let Some(_icon_window) = icon_app_handle.get_webview_window("selection-icon") {
100+
// Get the current mouse position for the translate popup
101+
let device_state = DeviceState::new();
102+
let (x, y) = device_state.get_mouse().coords;
103+
104+
// Get the selected text from the event payload
105+
let selected_text = event.payload().to_owned();
106+
107+
// Show the translate popup in a separate thread to avoid blocking
108+
let app_handle = icon_app_handle.clone();
109+
let mouse_service_ref = mouse_service_for_click.clone();
110+
tauri::async_runtime::spawn(async move {
111+
match create_or_focus_compact_window(&app_handle, x, y).await {
112+
Ok(translate_window) => {
113+
if let Err(e) = translate_window.emit("shortcut-quickTranslate", serde_json::json!({
114+
"text": selected_text
115+
})) {
116+
println!("Failed to emit shortcut-quickTranslate event: {}", e);
117+
}
118+
if let Err(e) = translate_window.show() {
119+
println!("Failed to show translate window: {}", e);
120+
}
121+
if let Err(e) = translate_window.set_focus() {
122+
println!("Failed to set focus on translate window: {}", e);
123+
}
124+
125+
// Hide the selection icon after showing the translate popup
126+
if let Some(icon_window) = app_handle.get_webview_window("selection-icon") {
127+
if let Err(e) = icon_window.hide() {
128+
println!("Failed to hide selection icon after showing translate popup: {}", e);
129+
} else {
130+
mouse_service_ref.set_selection_icon_visible(false);
131+
println!("Selection icon hidden after showing translate popup");
132+
}
133+
}
134+
},
135+
Err(e) => {
136+
println!("Failed to create translate window: {}", e);
137+
}
138+
}
139+
});
140+
}
141+
});
142+
}
143+
144+
pub fn setup_mouse_events(
145+
app_handle: &AppHandle,
146+
mouse_service: &Arc<MouseService>
147+
) {
148+
let app_handle_for_service = app_handle.clone();
149+
let mouse_service_for_events = mouse_service.clone();
150+
151+
mouse_service.start(app_handle.clone(), move |event| {
152+
let app_handle = app_handle_for_service.clone();
153+
match event {
154+
MouseEvent::TextSelected(text) => {
155+
let device_state = DeviceState::new();
156+
let (x, y) = device_state.get_mouse().coords;
157+
158+
// Emit event to main thread to handle window creation (thread safety)
159+
println!("Text selected from mouse service, emitting event to main thread");
160+
if let Err(e) = app_handle.emit("create-selection-icon", serde_json::json!({
161+
"text": text,
162+
"x": x + 5,
163+
"y": y - 5
164+
})) {
165+
println!("Failed to emit create-selection-icon event: {}", e);
166+
}
167+
},
168+
MouseEvent::ClickOutsideIcon(x, y) => {
169+
println!("Click outside icon detected at ({}, {})", x, y);
170+
if let Some(icon_window) = app_handle.get_webview_window("selection-icon") {
171+
if let Err(e) = icon_window.hide() {
172+
println!("Failed to hide selection icon: {}", e);
173+
} else {
174+
println!("Selection icon hidden due to outside click");
175+
// Update the mouse service state
176+
mouse_service_for_events.set_selection_icon_visible(false);
177+
}
178+
}
179+
},
180+
_ => {}
181+
}
182+
});
183+
}
184+
185+
pub fn setup_cleanup_events(app_handle: &AppHandle) {
186+
let cleanup_handle = app_handle.clone();
187+
app_handle.listen("app-shutdown", move |_| {
188+
println!("Application shutting down, cleaning up windows...");
189+
190+
// Close all windows properly to prevent window class issues
191+
if let Some(window) = cleanup_handle.get_webview_window("translate-popup") {
192+
println!("Closing translate-popup window");
193+
let _ = window.close();
194+
}
195+
196+
if let Some(window) = cleanup_handle.get_webview_window("selection-icon") {
197+
println!("Closing selection-icon window");
198+
let _ = window.close();
199+
}
200+
201+
if let Some(window) = cleanup_handle.get_webview_window("main") {
202+
println!("Closing main window");
203+
let _ = window.close();
204+
}
205+
206+
println!("Window cleanup completed");
207+
});
208+
}

0 commit comments

Comments
 (0)