| 1 |
|
| 2 |
|
| 3 |
use audiofiles_browser::ui::theme; |
| 4 |
use eframe::egui; |
| 5 |
|
| 6 |
use super::{AudioFilesApp, AppScreen, SYNC_SERVER_URL}; |
| 7 |
|
| 8 |
impl AudioFilesApp { |
| 9 |
|
| 10 |
pub(crate) fn draw_activation_screen(&mut self, ui: &mut egui::Ui) { |
| 11 |
|
| 12 |
let activation = self.activation_result.lock().take(); |
| 13 |
if let Some(result) = activation { |
| 14 |
match result { |
| 15 |
Ok(()) => { |
| 16 |
let cache = super::license::LicenseCache { |
| 17 |
key_code: self.license_key_input.trim().to_string(), |
| 18 |
machine_id: self.machine_id.clone(), |
| 19 |
activated_at: chrono::Utc::now().to_rfc3339(), |
| 20 |
}; |
| 21 |
if let Err(e) = super::license::save_license(&self.config_dir, &cache) { |
| 22 |
tracing::error!("Failed to save license: {e}"); |
| 23 |
} |
| 24 |
self.license_cache = Some(cache); |
| 25 |
self.activating = false; |
| 26 |
self.activation_error = None; |
| 27 |
|
| 28 |
|
| 29 |
if self.vault_registry.is_some() { |
| 30 |
self.activate_browser(); |
| 31 |
} else { |
| 32 |
self.screen = AppScreen::VaultSetup; |
| 33 |
} |
| 34 |
return; |
| 35 |
} |
| 36 |
Err(e) => { |
| 37 |
self.activation_error = Some(e); |
| 38 |
self.activating = false; |
| 39 |
} |
| 40 |
} |
| 41 |
} |
| 42 |
|
| 43 |
egui::CentralPanel::default().show_inside(ui, |ui| { |
| 44 |
let available = ui.available_size(); |
| 45 |
|
| 46 |
ui.add_space((available.y * 0.35).max(40.0)); |
| 47 |
|
| 48 |
ui.vertical_centered(|ui| { |
| 49 |
ui.heading("audiofiles"); |
| 50 |
ui.add_space(theme::space::MD); |
| 51 |
ui.label("Start a free trial, or activate a license key."); |
| 52 |
ui.add_space(theme::space::SECTION); |
| 53 |
|
| 54 |
|
| 55 |
let trial_expired = matches!( |
| 56 |
self.trial_state, |
| 57 |
Some(ref t) if super::license::trial_days_remaining(t) <= 0 |
| 58 |
); |
| 59 |
let trial_label = match self.trial_state { |
| 60 |
Some(ref trial) => { |
| 61 |
let days = super::license::trial_days_remaining(trial); |
| 62 |
if days > 0 { |
| 63 |
format!("Continue trial ({days} days left)") |
| 64 |
} else { |
| 65 |
"Trial expired".to_string() |
| 66 |
} |
| 67 |
} |
| 68 |
None => "Start free trial — 30 days, no card".to_string(), |
| 69 |
}; |
| 70 |
let trial_btn = egui::Button::new(egui::RichText::new(trial_label).strong()); |
| 71 |
if ui.add_enabled(!trial_expired, trial_btn).clicked() { |
| 72 |
self.start_trial(); |
| 73 |
} |
| 74 |
if trial_expired { |
| 75 |
ui.add_space(theme::space::SM); |
| 76 |
ui.label( |
| 77 |
egui::RichText::new("Activate a license below to continue.") |
| 78 |
.small() |
| 79 |
.color(theme::text_secondary()), |
| 80 |
); |
| 81 |
} |
| 82 |
|
| 83 |
ui.add_space(theme::space::XL); |
| 84 |
ui.separator(); |
| 85 |
ui.add_space(theme::space::MD); |
| 86 |
|
| 87 |
|
| 88 |
ui.label( |
| 89 |
egui::RichText::new("Already have a license?") |
| 90 |
.color(theme::text_secondary()), |
| 91 |
); |
| 92 |
ui.add_space(theme::space::SM); |
| 93 |
|
| 94 |
let input_width = 360.0_f32.min(available.x - 40.0); |
| 95 |
ui.allocate_ui(egui::vec2(input_width, 28.0), |ui| { |
| 96 |
let response = ui.add_sized( |
| 97 |
ui.available_size(), |
| 98 |
egui::TextEdit::singleline(&mut self.license_key_input) |
| 99 |
.hint_text("five-word-license-key-example"), |
| 100 |
); |
| 101 |
|
| 102 |
if response.changed() { |
| 103 |
self.activation_error = None; |
| 104 |
} |
| 105 |
|
| 106 |
if response.lost_focus() |
| 107 |
&& ui.input(|i| i.key_pressed(egui::Key::Enter)) |
| 108 |
&& !self.activating |
| 109 |
&& !self.license_key_input.trim().is_empty() |
| 110 |
{ |
| 111 |
self.start_activation(); |
| 112 |
} |
| 113 |
}); |
| 114 |
|
| 115 |
ui.add_space(theme::space::MD); |
| 116 |
|
| 117 |
let can_activate = !self.activating && !self.license_key_input.trim().is_empty(); |
| 118 |
ui.horizontal(|ui| { |
| 119 |
let button_text = if self.activating { "Activating\u{2026}" } else { "Activate" }; |
| 120 |
if ui.add_enabled(can_activate, egui::Button::new(button_text)).clicked() { |
| 121 |
self.start_activation(); |
| 122 |
} |
| 123 |
if self.activating { |
| 124 |
ui.spinner(); |
| 125 |
} |
| 126 |
}); |
| 127 |
|
| 128 |
if let Some(err) = self.activation_error.clone() { |
| 129 |
ui.add_space(theme::space::MD); |
| 130 |
ui.colored_label(theme::accent_red(), err.to_string()); |
| 131 |
ui.add_space(theme::space::SM); |
| 132 |
|
| 133 |
match err { |
| 134 |
super::license::ActivationError::Network |
| 135 |
| super::license::ActivationError::Server(_) |
| 136 |
| super::license::ActivationError::Other(_) => { |
| 137 |
if ui.button("Try again").clicked() && !self.activating { |
| 138 |
self.start_activation(); |
| 139 |
} |
| 140 |
} |
| 141 |
super::license::ActivationError::InvalidKey => { |
| 142 |
ui.hyperlink_to( |
| 143 |
"Get a new license key", |
| 144 |
"https://makenot.work/store/audiofiles", |
| 145 |
); |
| 146 |
} |
| 147 |
super::license::ActivationError::MachineLimit => { |
| 148 |
ui.hyperlink_to( |
| 149 |
"Contact support", |
| 150 |
"mailto:info@makenot.work?subject=License%20activation%20issue", |
| 151 |
); |
| 152 |
} |
| 153 |
} |
| 154 |
} |
| 155 |
|
| 156 |
ui.add_space(theme::space::MD); |
| 157 |
ui.hyperlink_to( |
| 158 |
"Get a license key", |
| 159 |
"https://makenot.work/store/audiofiles", |
| 160 |
); |
| 161 |
|
| 162 |
ui.add_space(theme::space::XL); |
| 163 |
ui.horizontal(|ui| { |
| 164 |
ui.add_space((ui.available_width() / 2.0 - 60.0).max(0.0)); |
| 165 |
if ui.small_button("About audiofiles").clicked() { |
| 166 |
self.show_about = true; |
| 167 |
} |
| 168 |
}); |
| 169 |
}); |
| 170 |
}); |
| 171 |
} |
| 172 |
|
| 173 |
|
| 174 |
pub(crate) fn start_trial(&mut self) { |
| 175 |
if self.trial_state.is_none() { |
| 176 |
let now = chrono::Utc::now().to_rfc3339(); |
| 177 |
let trial = super::license::TrialState { |
| 178 |
first_launch_date: now.clone(), |
| 179 |
last_seen_date: Some(now), |
| 180 |
}; |
| 181 |
if let Err(e) = super::license::save_trial(&self.config_dir, &trial) { |
| 182 |
tracing::error!("Failed to save trial state: {e}"); |
| 183 |
} |
| 184 |
self.trial_state = Some(trial); |
| 185 |
} |
| 186 |
if self.vault_registry.is_some() { |
| 187 |
self.activate_browser(); |
| 188 |
} else { |
| 189 |
self.screen = AppScreen::VaultSetup; |
| 190 |
} |
| 191 |
} |
| 192 |
|
| 193 |
|
| 194 |
fn start_activation(&mut self) { |
| 195 |
self.activating = true; |
| 196 |
self.activation_error = None; |
| 197 |
let slot = self.activation_result.clone(); |
| 198 |
let server_url = SYNC_SERVER_URL.to_string(); |
| 199 |
let key = self.license_key_input.trim().to_string(); |
| 200 |
let mid = self.machine_id.clone(); |
| 201 |
self._runtime.spawn(async move { |
| 202 |
let result = super::license::activate_key(&server_url, &key, &mid).await; |
| 203 |
*slot.lock() = Some(result); |
| 204 |
}); |
| 205 |
} |
| 206 |
|
| 207 |
|
| 208 |
pub(crate) fn sync_license_to_browser(&mut self) { |
| 209 |
if let Some(ref mut browser) = self.browser { |
| 210 |
if let Some(ref cache) = self.license_cache { |
| 211 |
browser.settings.license_key_masked = Some(mask_key(&cache.key_code)); |
| 212 |
browser.settings.trial_days_remaining = None; |
| 213 |
} else if let Some(ref trial) = self.trial_state { |
| 214 |
browser.settings.trial_days_remaining = Some(super::license::trial_days_remaining(trial)); |
| 215 |
} |
| 216 |
let mid = &self.machine_id; |
| 217 |
browser.settings.machine_id = Some( |
| 218 |
if mid.len() > 12 { |
| 219 |
format!("{}...{}", &mid[..8], &mid[mid.len()-4..]) |
| 220 |
} else { |
| 221 |
mid.clone() |
| 222 |
} |
| 223 |
); |
| 224 |
} |
| 225 |
} |
| 226 |
|
| 227 |
|
| 228 |
|
| 229 |
pub(crate) fn deactivate(&mut self) { |
| 230 |
if let Some(ref cache) = self.license_cache { |
| 231 |
let server_url = SYNC_SERVER_URL.to_string(); |
| 232 |
let key = cache.key_code.clone(); |
| 233 |
let mid = self.machine_id.clone(); |
| 234 |
self._runtime.spawn(async move { |
| 235 |
if let Err(e) = super::license::deactivate_key(&server_url, &key, &mid).await { |
| 236 |
tracing::warn!("Deactivation request failed (best-effort): {e}"); |
| 237 |
} |
| 238 |
}); |
| 239 |
} |
| 240 |
let _ = super::license::remove_license(&self.config_dir); |
| 241 |
self.license_cache = None; |
| 242 |
self.browser = None; |
| 243 |
self.sync_manager = None; |
| 244 |
self.screen = AppScreen::Activation; |
| 245 |
self.license_key_input.clear(); |
| 246 |
self.activation_error = None; |
| 247 |
} |
| 248 |
} |
| 249 |
|
| 250 |
|
| 251 |
pub(crate) fn mask_key(key: &str) -> String { |
| 252 |
let words: Vec<&str> = key.split('-').collect(); |
| 253 |
if words.len() >= 2 { |
| 254 |
format!("{}-...-{}", words[0], words[words.len() - 1]) |
| 255 |
} else { |
| 256 |
"***".to_string() |
| 257 |
} |
| 258 |
} |
| 259 |
|