1use std::{cell::RefCell, rc::Rc, time::Duration};
6
7use copypasta::{ClipboardContext, ClipboardProvider};
8use slint::ComponentHandle;
9use wowlab_node::{init_ui_logging, log_dir};
10
11use crate::{
12 AppWindow,
13 app::{App, AppInitError},
14 tray, update,
15};
16
17const POLL_INTERVAL: Duration = Duration::from_millis(100);
18const CLIPBOARD_FEEDBACK_DURATION: Duration = Duration::from_secs(2);
19
20#[derive(Debug, thiserror::Error)]
21pub(crate) enum UiError {
22 #[error(transparent)]
23 Application(#[from] AppInitError),
24 #[error("application window failed: {0}")]
25 Window(#[from] slint::PlatformError),
26}
27
28pub(crate) fn run(no_update: bool) -> Result<(), UiError> {
29 let (_logging_guard, log_rx) = init_ui_logging();
30
31 let window = AppWindow::new()?;
32 let app = Rc::new(RefCell::new(App::new(log_rx)?));
33
34 initialize_window(&window, &app.borrow());
35 wire_node_callbacks(&window, &app);
36 wire_utility_callbacks(&window, &app);
37 wire_settings_callbacks(&window, &app);
38
39 if update::should_check_on_startup(no_update) {
40 update::check_in_background(&window.as_weak());
41 }
42
43 let tray_state = tray::create_tray();
44 let timer = start_poll_timer(&window, &app, tray_state);
45
46 window.run()?;
47 drop(timer);
48
49 Ok(())
50}
51
52fn initialize_window(window: &AppWindow, app: &App) {
53 window.set_version_text(format!("Version {}", crate::VERSION).into());
54 window.set_cpu_cores(app.cpu_cores().to_string().into());
55 window.set_node_name(app.node_name().into());
56 window.set_public_key(app.public_key().into());
57 window.set_app_state(app.state_name().into());
58 window.set_connection_status(app.connection_status_name().into());
59}
60
61fn wire_node_callbacks(window: &AppWindow, app: &Rc<RefCell<App>>) {
62 let window_weak = window.as_weak();
63
64 window.on_token_changed(move |text| {
65 let (error, valid) = crate::app::validate_token(&text);
66
67 if let Some(window) = window_weak.upgrade() {
68 window.set_token_error(error.into());
69 window.set_token_valid(valid);
70 }
71 });
72
73 let app_ref = Rc::clone(app);
74
75 window.on_register(move |token| {
76 app_ref.borrow_mut().register(token.to_string());
77 });
78
79 let app_ref = Rc::clone(app);
80 let window_weak = window.as_weak();
81
82 window.on_unlink(move || {
83 if let Some(window) = window_weak.upgrade() {
84 window.set_unlink_busy(true);
85 window.set_unlink_error("".into());
86 }
87
88 app_ref.borrow_mut().request_unlink();
89 });
90
91 let app_ref = Rc::clone(app);
92 let window_weak = window.as_weak();
93
94 window.on_toggle_node(move || {
95 if let Some(window) = window_weak.upgrade() {
96 let enabled = window.get_node_enabled();
97
98 if enabled {
99 app_ref.borrow_mut().disconnect();
100 window.set_node_enabled(false);
101 } else {
102 app_ref.borrow_mut().reconnect();
103 window.set_node_enabled(true);
104 }
105 }
106 });
107}
108
109fn wire_utility_callbacks(window: &AppWindow, app: &Rc<RefCell<App>>) {
110 window.on_open_portal(|| {
111 let _ = open::that("https://wowlab.gg/account/nodes?showToken");
112 });
113
114 window.on_open_logs(|| {
115 if let Some(directory) = log_dir() {
116 let _ = open::that(directory);
117 }
118 });
119
120 let app_ref = Rc::clone(app);
121 let window_weak = window.as_weak();
122
123 window.on_copy_public_key(move || {
124 let public_key = app_ref.borrow().public_key().to_string();
125
126 copy_to_clipboard_with_feedback(&public_key, &window_weak);
127 });
128
129 let window_weak = window.as_weak();
130
131 window.on_copy_log_entry(move |message| {
132 copy_to_clipboard_with_feedback(&message, &window_weak);
133 });
134
135 let app_ref = Rc::clone(app);
136 let window_weak = window.as_weak();
137
138 window.on_set_filter(move |filter| {
139 app_ref.borrow_mut().set_filter(filter);
140
141 if let Some(window) = window_weak.upgrade() {
142 app_ref.borrow().update_logs(&window);
143 }
144 });
145
146 let app_ref = Rc::clone(app);
147 let window_weak = window.as_weak();
148
149 window.on_search_changed(move |text| {
150 app_ref.borrow_mut().set_search_text(text.to_string());
151
152 if let Some(window) = window_weak.upgrade() {
153 app_ref.borrow().update_logs(&window);
154 }
155 });
156}
157
158fn wire_settings_callbacks(window: &AppWindow, app: &Rc<RefCell<App>>) {
159 let window_weak = window.as_weak();
160
161 window.on_check_for_updates(move || {
162 update::check_in_background(&window_weak);
163 });
164
165 let window_weak = window.as_weak();
166
167 window.on_toggle_minimize_to_tray(move || {
168 if let Some(window) = window_weak.upgrade() {
169 window.set_minimize_to_tray(!window.get_minimize_to_tray());
170 }
171 });
172
173 let window_weak = window.as_weak();
174 let app_ref = Rc::clone(app);
175
176 window.window().on_close_requested(move || {
177 if let Some(window) = window_weak.upgrade() {
178 if app_ref.borrow().unlink_in_progress() {
179 return slint::CloseRequestResponse::KeepWindowShown;
180 }
181
182 if window.get_minimize_to_tray() {
183 let _ = window.hide();
184
185 return slint::CloseRequestResponse::KeepWindowShown;
186 }
187 }
188
189 slint::CloseRequestResponse::HideWindow
190 });
191}
192
193fn start_poll_timer(
194 window: &AppWindow,
195 app: &Rc<RefCell<App>>,
196 tray_state: Option<tray::TrayState>,
197) -> slint::Timer {
198 let timer = slint::Timer::default();
199 let app_ref = Rc::clone(app);
200 let window_weak = window.as_weak();
201
202 timer.start(slint::TimerMode::Repeated, POLL_INTERVAL, move || {
203 if let Some(window) = window_weak.upgrade() {
204 let close_after_unlink = app_ref.borrow_mut().poll(&window);
205
206 if close_after_unlink {
207 let _ = slint::quit_event_loop();
208
209 return;
210 }
211 }
212
213 if let Some(ref tray) = tray_state {
214 match tray::poll_tray_events(tray) {
215 tray::TrayAction::Show => {
216 if let Some(window) = window_weak.upgrade() {
217 let _ = window.show();
218 }
219 }
220 tray::TrayAction::Quit => {
221 if !app_ref.borrow().unlink_in_progress() {
222 let _ = slint::quit_event_loop();
223 }
224 }
225 tray::TrayAction::None => {}
226 }
227 }
228 });
229
230 timer
231}
232
233fn copy_to_clipboard_with_feedback(text: &str, window: &slint::Weak<AppWindow>) {
234 if let Ok(mut clipboard) = ClipboardContext::new() {
235 if clipboard.set_contents(text.to_string()).is_ok() {
236 if let Some(window) = window.upgrade() {
237 window.set_show_copied_feedback(true);
238 let feedback_window = window.as_weak();
239
240 slint::Timer::single_shot(CLIPBOARD_FEEDBACK_DURATION, move || {
241 if let Some(window) = feedback_window.upgrade() {
242 window.set_show_copied_feedback(false);
243 }
244 });
245 }
246 }
247 }
248}