paiagram/interface/
about.rs

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