paiagram/interface/
about.rs

1use crate::interface::UiCommand;
2use crate::interface::tabs::all_tabs::MinesweeperTab;
3use crate::rw_data::ModifyData;
4use bevy::ecs::system::Single;
5use bevy::ecs::{message::MessageWriter, system::InMut};
6use egui::{Id, Modal, OpenUrl};
7use rfd::AsyncFileDialog;
8use std::collections::VecDeque;
9use std::sync::{Arc, Mutex, OnceLock};
10
11#[cfg(not(target_arch = "wasm32"))]
12use bevy::tasks::IoTaskPool;
13
14#[cfg(target_arch = "wasm32")]
15use wasm_bindgen_futures::spawn_local;
16
17static FILE_IMPORT_QUEUE: OnceLock<Arc<Mutex<VecDeque<ModifyData>>>> = OnceLock::new();
18
19fn shared_queue() -> Arc<Mutex<VecDeque<ModifyData>>> {
20    FILE_IMPORT_QUEUE
21        .get_or_init(|| Arc::new(Mutex::new(VecDeque::new())))
22        .clone()
23}
24
25fn show_import_button(
26    ui: &mut egui::Ui,
27    label: &'static str,
28    filter_label: &'static str,
29    extensions: &'static [&'static str],
30    constructor: fn(String) -> ModifyData,
31) {
32    if ui.button(label).clicked() {
33        start_file_import(filter_label, extensions, constructor);
34    }
35}
36
37fn start_file_import(
38    filter_label: &'static str,
39    extensions: &'static [&'static str],
40    constructor: fn(String) -> ModifyData,
41) {
42    let queue = shared_queue();
43    let task = async move {
44        if let Some(content) = pick_file_contents(filter_label, extensions).await {
45            if let Ok(mut guard) = queue.lock() {
46                guard.push_back(constructor(content));
47            }
48        }
49    };
50
51    #[cfg(target_arch = "wasm32")]
52    spawn_local(task);
53
54    #[cfg(not(target_arch = "wasm32"))]
55    IoTaskPool::get().spawn(task).detach();
56}
57
58async fn pick_file_contents(
59    filter_label: &'static str,
60    extensions: &'static [&'static str],
61) -> Option<String> {
62    let file = AsyncFileDialog::new()
63        .add_filter(filter_label, extensions)
64        .pick_file()
65        .await?;
66    let bytes = file.read().await;
67    String::from_utf8(bytes).ok()
68}
69
70pub fn show_about(
71    (InMut(ui), InMut(modal_open)): (InMut<egui::Ui>, InMut<bool>),
72    mut msg_read_file: MessageWriter<ModifyData>,
73    mut msg_open_tab: MessageWriter<UiCommand>,
74) {
75    let queue = shared_queue();
76    let pending: Vec<_> = {
77        let mut guard = queue.lock().unwrap();
78        guard.drain(..).collect()
79    };
80    drop(queue);
81    for message in pending {
82        msg_read_file.write(message);
83    }
84
85    if ui.button("⚙").clicked() {
86        msg_open_tab.write(UiCommand::OpenOrFocusTab(super::AppTab::Settings(
87            super::tabs::settings::SettingsTab,
88        )));
89    }
90
91    if ui.button("G").clicked() {
92        msg_open_tab.write(UiCommand::OpenOrFocusTab(super::AppTab::Graph(
93            super::tabs::graph::GraphTab::default(),
94        )));
95    }
96
97    egui::MenuBar::new().ui(ui, |ui| {
98        ui.menu_button("File", |ui| {
99            if ui.button("Read...").clicked() {
100                // TODO
101            }
102            if ui.button("Save As...").clicked() {
103                // TODO
104            }
105            ui.separator();
106            show_import_button(
107                ui,
108                "Import qETRC...",
109                "qETRC Files",
110                &["pyetgr", "json"],
111                ModifyData::LoadQETRC,
112            );
113            show_import_button(
114                ui,
115                "Import OuDiaSecond...",
116                "OuDiaSecond Files",
117                &["oud2"],
118                ModifyData::LoadOuDiaSecond,
119            );
120        });
121        ui.menu_button("Help", |ui| {
122            if ui.button("Minesweeper").clicked() {
123                msg_open_tab.write(UiCommand::OpenOrFocusTab(super::AppTab::Minesweeper(
124                    MinesweeperTab,
125                )));
126            }
127            if ui.button("Check for Updates").clicked() {
128                // TODO
129            }
130            if ui.button("Report Issue").clicked() {
131                ui.ctx().open_url(OpenUrl {
132                    url: "https://github.com/WenSimEHRP/Paiagram/issues".into(),
133                    new_tab: true,
134                });
135            };
136            if ui.button("Help Translate").clicked() {
137                ui.ctx().open_url(OpenUrl {
138                    // TODO: add a real URL here
139                    url: "https://github.com/WenSimEHRP/Paiagram/issues".into(),
140                    new_tab: true,
141                });
142            };
143            ui.separator();
144            if ui.button("Documentation").clicked() {
145                ui.ctx().open_url(OpenUrl {
146                    // TODO: add a real URL here
147                    url: "https://github.com/WenSimEHRP/Paiagram/issues".into(),
148                    new_tab: true,
149                });
150            };
151            if ui.button("About Paiagram").clicked() {
152                *modal_open = true;
153            };
154        });
155    });
156    if *modal_open {
157        let modal = Modal::new(Id::new("Modal B")).show(ui.ctx(), |ui| {
158            ui.set_width(500.0);
159            ui.heading("About");
160            ui.label(format!("Paiagram ({})", git_version::git_version!()));
161            ui.label("© 2025 Jeremy Gao");
162            ui.label("Paiagram is a free and open-source application licensed under the AGPL v3.0 license.");
163            ui.separator();
164            egui::ScrollArea::vertical().show(ui, |ui| {
165                ui.set_width(ui.available_width().max(0.0));
166                ui.monospace(include_str!("../../LICENSE.md"));
167            });
168        });
169
170        if modal.should_close() {
171            *modal_open = false;
172        }
173    }
174}