| 20 |
20 |
|
mod audio;
|
| 21 |
21 |
|
mod license;
|
| 22 |
22 |
|
mod midi;
|
|
23 |
+ |
mod preferences;
|
| 23 |
24 |
|
mod tray;
|
| 24 |
25 |
|
pub mod updater;
|
| 25 |
26 |
|
mod vault_setup;
|
| 70 |
71 |
|
.build()
|
| 71 |
72 |
|
.expect("failed to start tokio runtime");
|
| 72 |
73 |
|
|
| 73 |
|
- |
// OTA update checker (runs in background on the tokio runtime)
|
| 74 |
|
- |
let update_checker = updater::UpdateChecker::new(runtime.handle());
|
|
74 |
+ |
// Load user preferences (controls the network-touching update checker).
|
|
75 |
+ |
let prefs = preferences::Preferences::load(&config_dir);
|
|
76 |
+ |
|
|
77 |
+ |
// OTA update checker (runs in background on the tokio runtime). Only
|
|
78 |
+ |
// spawned if the user hasn't opted out — preserves the "no silent
|
|
79 |
+ |
// network without consent" rule.
|
|
80 |
+ |
let update_checker = if prefs.check_for_updates {
|
|
81 |
+ |
updater::UpdateChecker::new(runtime.handle())
|
|
82 |
+ |
} else {
|
|
83 |
+ |
tracing::info!("Update checks disabled by preferences");
|
|
84 |
+ |
updater::UpdateChecker::disabled()
|
|
85 |
+ |
};
|
| 75 |
86 |
|
|
| 76 |
87 |
|
let shared = Arc::new(SharedState::new());
|
| 77 |
88 |
|
|
| 119 |
130 |
|
Box::new(move |cc| {
|
| 120 |
131 |
|
audiofiles_browser::ui::theme::setup_fonts(&cc.egui_ctx);
|
| 121 |
132 |
|
Ok(Box::new(AudioFilesApp::new(
|
| 122 |
|
- |
config_dir, shared, app_tray, update_checker, runtime, gtk_ok,
|
|
133 |
+ |
config_dir, shared, app_tray, update_checker, prefs, runtime, gtk_ok,
|
| 123 |
134 |
|
)))
|
| 124 |
135 |
|
}),
|
| 125 |
136 |
|
)
|
| 194 |
205 |
|
let db_path = data_dir.join("audiofiles.db");
|
| 195 |
206 |
|
let content_dir = data_dir.join("samples");
|
| 196 |
207 |
|
let manager = SyncManager::new(config, db_path, content_dir, runtime.clone());
|
| 197 |
|
- |
manager.fetch_tiers();
|
|
208 |
+ |
manager.fetch_pricing();
|
| 198 |
209 |
|
manager.try_restore_session();
|
| 199 |
210 |
|
manager.start_scheduler();
|
| 200 |
211 |
|
Some(manager)
|
| 243 |
254 |
|
tray: Option<tray::AppTray>,
|
| 244 |
255 |
|
sync_manager: Option<SyncManager>,
|
| 245 |
256 |
|
update_checker: updater::UpdateChecker,
|
|
257 |
+ |
prefs: preferences::Preferences,
|
|
258 |
+ |
/// Whether the About modal is currently visible. Toggled by Cmd/Ctrl+I or
|
|
259 |
+ |
/// the About button on activation / vault setup / DB-error screens.
|
|
260 |
+ |
show_about: bool,
|
| 246 |
261 |
|
/// Active MIDI input connection (dropped to disconnect).
|
| 247 |
262 |
|
midi_connection: Option<midi::MidiConnection>,
|
| 248 |
263 |
|
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
| 270 |
285 |
|
shared: Arc<SharedState>,
|
| 271 |
286 |
|
tray: Option<tray::AppTray>,
|
| 272 |
287 |
|
update_checker: updater::UpdateChecker,
|
|
288 |
+ |
prefs: preferences::Preferences,
|
| 273 |
289 |
|
runtime: tokio::runtime::Runtime,
|
| 274 |
290 |
|
gtk_ok: bool,
|
| 275 |
291 |
|
) -> Self {
|
| 308 |
324 |
|
let (browser, error) = init_browser(&data_dir, shared.clone(), &vault_setup::vault_name_for_path(reg, &data_dir));
|
| 309 |
325 |
|
(data_dir, browser, error, sync_manager, Some(cache.clone()))
|
| 310 |
326 |
|
}
|
| 311 |
|
- |
// Registry exists, unlicensed but in trial → open the active vault (no sync)
|
|
327 |
+ |
// Registry exists, unlicensed but in trial → open the active vault
|
| 312 |
328 |
|
(Some(reg), license::LicenseStatus::Unlicensed) if has_active_trial => {
|
| 313 |
329 |
|
let data_dir = reg.active.clone();
|
| 314 |
330 |
|
let _ = std::fs::create_dir_all(&data_dir);
|
|
331 |
+ |
let sync_manager = create_sync_manager(&data_dir, runtime.handle());
|
| 315 |
332 |
|
let (browser, error) = init_browser(&data_dir, shared.clone(), &vault_setup::vault_name_for_path(reg, &data_dir));
|
| 316 |
|
- |
(data_dir, browser, error, None, None)
|
|
333 |
+ |
(data_dir, browser, error, sync_manager, None)
|
| 317 |
334 |
|
}
|
| 318 |
335 |
|
// Registry exists but unlicensed (deactivated and reactivated)
|
| 319 |
336 |
|
(Some(reg), license::LicenseStatus::Unlicensed) => {
|
| 346 |
363 |
|
tray,
|
| 347 |
364 |
|
sync_manager,
|
| 348 |
365 |
|
update_checker,
|
|
366 |
+ |
prefs,
|
|
367 |
+ |
show_about: false,
|
| 349 |
368 |
|
midi_connection: None,
|
| 350 |
369 |
|
gtk_ok,
|
| 351 |
370 |
|
_runtime: runtime,
|
| 437 |
456 |
|
}
|
| 438 |
457 |
|
}
|
| 439 |
458 |
|
|
|
459 |
+ |
// Cmd/Ctrl+I toggles the About modal. Works on every screen so a
|
|
460 |
+ |
// confused user always has one keystroke to "who made this".
|
|
461 |
+ |
ctx.input_mut(|i| {
|
|
462 |
+ |
if i.consume_shortcut(&egui::KeyboardShortcut::new(
|
|
463 |
+ |
egui::Modifiers::COMMAND,
|
|
464 |
+ |
egui::Key::I,
|
|
465 |
+ |
)) {
|
|
466 |
+ |
self.show_about = !self.show_about;
|
|
467 |
+ |
}
|
|
468 |
+ |
});
|
|
469 |
+ |
|
| 440 |
470 |
|
match self.screen {
|
| 441 |
471 |
|
AppScreen::Activation => {
|
| 442 |
472 |
|
self.draw_activation_screen(ctx);
|
| 449 |
479 |
|
}
|
| 450 |
480 |
|
}
|
| 451 |
481 |
|
|
|
482 |
+ |
// About modal — drawn last so it overlays the screen content.
|
|
483 |
+ |
self.draw_about_modal(ctx);
|
|
484 |
+ |
|
| 452 |
485 |
|
// Show update notification overlay (bottom-right) — user must consent
|
| 453 |
486 |
|
if self.update_checker.should_show() {
|
| 454 |
487 |
|
let (version, notes, download_url) = {
|
| 721 |
754 |
|
);
|
| 722 |
755 |
|
}
|
| 723 |
756 |
|
} else {
|
| 724 |
|
- |
egui::CentralPanel::default().show(ctx, |ui| {
|
| 725 |
|
- |
ui.heading("audiofiles");
|
| 726 |
|
- |
if let Some(ref err) = self.error {
|
| 727 |
|
- |
ui.label(format!("Error: could not initialize database.\n{err}"));
|
| 728 |
|
- |
}
|
| 729 |
|
- |
});
|
|
757 |
+ |
self.draw_db_error_screen(ctx);
|
| 730 |
758 |
|
}
|
| 731 |
759 |
|
}
|
| 732 |
760 |
|
}
|
| 733 |
761 |
|
|
|
762 |
+ |
impl AudioFilesApp {
|
|
763 |
+ |
/// Render the "vault failed to open" recovery surface. Replaces the prior
|
|
764 |
+ |
/// dead-end label with explicit recovery actions: Retry the same path,
|
|
765 |
+ |
/// Choose a different vault, or open the data folder for manual triage.
|
|
766 |
+ |
fn draw_db_error_screen(&mut self, ctx: &egui::Context) {
|
|
767 |
+ |
egui::CentralPanel::default().show(ctx, |ui| {
|
|
768 |
+ |
ui.add_space(48.0);
|
|
769 |
+ |
ui.vertical_centered(|ui| {
|
|
770 |
+ |
ui.heading("audiofiles");
|
|
771 |
+ |
ui.add_space(8.0);
|
|
772 |
+ |
ui.label("Couldn't open this vault.");
|
|
773 |
+ |
if let Some(ref err) = self.error {
|
|
774 |
+ |
ui.add_space(4.0);
|
|
775 |
+ |
ui.label(
|
|
776 |
+ |
egui::RichText::new(err)
|
|
777 |
+ |
.small()
|
|
778 |
+ |
.color(audiofiles_browser::ui::theme::text_muted()),
|
|
779 |
+ |
);
|
|
780 |
+ |
}
|
|
781 |
+ |
ui.add_space(16.0);
|
|
782 |
+ |
ui.label(
|
|
783 |
+ |
egui::RichText::new(format!("Vault location: {}", self.data_dir.display()))
|
|
784 |
+ |
.small()
|
|
785 |
+ |
.color(audiofiles_browser::ui::theme::text_muted()),
|
|
786 |
+ |
);
|
|
787 |
+ |
ui.add_space(16.0);
|
|
788 |
+ |
|
|
789 |
+ |
ui.horizontal(|ui| {
|
|
790 |
+ |
ui.add_space(ui.available_width() / 2.0 - 200.0);
|
|
791 |
+ |
if ui.button("Try again").clicked() {
|
|
792 |
+ |
let vault_name = self
|
|
793 |
+ |
.vault_registry
|
|
794 |
+ |
.as_ref()
|
|
795 |
+ |
.map(|r| vault_setup::vault_name_for_path(r, &self.data_dir))
|
|
796 |
+ |
.unwrap_or_else(|| "Library".to_string());
|
|
797 |
+ |
let (browser, error) =
|
|
798 |
+ |
init_browser(&self.data_dir, self.shared.clone(), &vault_name);
|
|
799 |
+ |
self.browser = browser;
|
|
800 |
+ |
self.error = error;
|
|
801 |
+ |
}
|
|
802 |
+ |
if ui.button("Choose a different location").clicked() {
|
|
803 |
+ |
self.screen = AppScreen::VaultSetup;
|
|
804 |
+ |
self.error = None;
|
|
805 |
+ |
}
|
|
806 |
+ |
if ui.button(reveal_folder_label()).clicked() {
|
|
807 |
+ |
reveal_in_file_manager(&self.data_dir);
|
|
808 |
+ |
}
|
|
809 |
+ |
});
|
|
810 |
+ |
ui.add_space(24.0);
|
|
811 |
+ |
if ui.small_button("About audiofiles").clicked() {
|
|
812 |
+ |
self.show_about = true;
|
|
813 |
+ |
}
|
|
814 |
+ |
});
|
|
815 |
+ |
});
|
|
816 |
+ |
}
|
|
817 |
+ |
|
|
818 |
+ |
/// Render the About modal: version, attribution, contact, license, and the
|
|
819 |
+ |
/// network-touching update-check toggle (the only user-visible network
|
|
820 |
+ |
/// surface besides license activation).
|
|
821 |
+ |
fn draw_about_modal(&mut self, ctx: &egui::Context) {
|
|
822 |
+ |
if !self.show_about {
|
|
823 |
+ |
return;
|
|
824 |
+ |
}
|
|
825 |
+ |
let mut open = true;
|
|
826 |
+ |
let mut updated_check_pref = self.prefs.check_for_updates;
|
|
827 |
+ |
egui::Window::new("About audiofiles")
|
|
828 |
+ |
.open(&mut open)
|
|
829 |
+ |
.resizable(false)
|
|
830 |
+ |
.collapsible(false)
|
|
831 |
+ |
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
|
|
832 |
+ |
.show(ctx, |ui| {
|
|
833 |
+ |
ui.set_max_width(360.0);
|
|
834 |
+ |
ui.vertical_centered(|ui| {
|
|
835 |
+ |
ui.heading("audiofiles");
|
|
836 |
+ |
ui.label(format!("Version {}", env!("CARGO_PKG_VERSION")));
|
|
837 |
+ |
});
|
|
838 |
+ |
ui.add_space(8.0);
|
|
839 |
+ |
ui.label("Made by Make Creative, LLC.");
|
|
840 |
+ |
ui.horizontal(|ui| {
|
|
841 |
+ |
ui.label("Contact:");
|
|
842 |
+ |
ui.hyperlink_to("info@makenot.work", "mailto:info@makenot.work");
|
|
843 |
+ |
});
|
|
844 |
+ |
ui.horizontal(|ui| {
|
|
845 |
+ |
ui.label("Web:");
|
|
846 |
+ |
ui.hyperlink_to("makenot.work", "https://makenot.work");
|
|
847 |
+ |
});
|
|
848 |
+ |
ui.label("License: PolyForm Noncommercial 1.0.0.");
|
|
849 |
+ |
ui.add_space(12.0);
|
|
850 |
+ |
ui.separator();
|
|
851 |
+ |
ui.add_space(8.0);
|
|
852 |
+ |
ui.strong("Network");
|
|
853 |
+ |
ui.checkbox(
|
|
854 |
+ |
&mut updated_check_pref,
|
|
855 |
+ |
"Check makenot.work for updates",
|
|
856 |
+ |
);
|
|
857 |
+ |
ui.label(
|
|
858 |
+ |
egui::RichText::new(
|
|
859 |
+ |
"When enabled, the app contacts makenot.work on launch and every 6 hours \
|
|
860 |
+ |
to check for a newer version. It sends only the current version, OS, and \
|
|
861 |
+ |
architecture. License activation is always user-initiated.",
|
|
862 |
+ |
)
|
|
863 |
+ |
.small()
|
|
864 |
+ |
.color(audiofiles_browser::ui::theme::text_muted()),
|
|
865 |
+ |
);
|
|
866 |
+ |
ui.add_space(8.0);
|
|
867 |
+ |
ui.label(
|
|
868 |
+ |
egui::RichText::new(format!(
|
|
869 |
+ |
"Preferences file: {}",
|
|
870 |
+ |
self.config_dir.join("preferences.json").display()
|
|
871 |
+ |
))
|
|
872 |
+ |
.small()
|
|
873 |
+ |
.color(audiofiles_browser::ui::theme::text_muted()),
|
|
874 |
+ |
);
|
|
875 |
+ |
ui.add_space(12.0);
|
|
876 |
+ |
ui.vertical_centered(|ui| {
|
|
877 |
+ |
if ui.button("Close").clicked() {
|
|
878 |
+ |
self.show_about = false;
|
|
879 |
+ |
}
|
|
880 |
+ |
});
|
|
881 |
+ |
});
|
|
882 |
+ |
if updated_check_pref != self.prefs.check_for_updates {
|
|
883 |
+ |
self.prefs.check_for_updates = updated_check_pref;
|
|
884 |
+ |
self.prefs.save(&self.config_dir);
|
|
885 |
+ |
// The change takes effect on next launch — we don't tear down the
|
|
886 |
+ |
// already-spawned tokio task at runtime.
|
|
887 |
+ |
}
|
|
888 |
+ |
if !open {
|
|
889 |
+ |
self.show_about = false;
|
|
890 |
+ |
}
|
|
891 |
+ |
}
|
|
892 |
+ |
}
|
|
893 |
+ |
|
|
894 |
+ |
/// Platform-specific label for the "open this folder in the OS file manager"
|
|
895 |
+ |
/// action. Mirrors the convention used in `file_list_menus.rs::reveal_label`.
|
|
896 |
+ |
fn reveal_folder_label() -> &'static str {
|
|
897 |
+ |
#[cfg(target_os = "macos")]
|
|
898 |
+ |
{
|
|
899 |
+ |
"Show in Finder"
|
|
900 |
+ |
}
|
|
901 |
+ |
#[cfg(target_os = "windows")]
|
|
902 |
+ |
{
|
|
903 |
+ |
"Show in Explorer"
|
|
904 |
+ |
}
|
|
905 |
+ |
#[cfg(target_os = "linux")]
|
|
906 |
+ |
{
|
|
907 |
+ |
"Open folder"
|
|
908 |
+ |
}
|
|
909 |
+ |
}
|
|
910 |
+ |
|
|
911 |
+ |
/// Open `path` in the native file manager. Errors are silently dropped — the
|
|
912 |
+ |
/// user can fall back to the displayed path string.
|
|
913 |
+ |
fn reveal_in_file_manager(path: &Path) {
|
|
914 |
+ |
#[cfg(target_os = "macos")]
|
|
915 |
+ |
let _ = std::process::Command::new("open").arg(path).spawn();
|
|
916 |
+ |
#[cfg(target_os = "windows")]
|
|
917 |
+ |
let _ = std::process::Command::new("explorer").arg(path).spawn();
|
|
918 |
+ |
#[cfg(target_os = "linux")]
|
|
919 |
+ |
let _ = std::process::Command::new("xdg-open").arg(path).spawn();
|
|
920 |
+ |
}
|
|
921 |
+ |
|
| 734 |
922 |
|
#[cfg(test)]
|
| 735 |
923 |
|
mod tests {
|
| 736 |
924 |
|
use super::*;
|