310 lines
11 KiB
Rust
Executable File
310 lines
11 KiB
Rust
Executable File
use std::fs::File;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::thread::JoinHandle;
|
|
|
|
use anyhow::{Context, Result};
|
|
use clipboard::ClipboardProvider;
|
|
use druid::{AppDelegate, AppLauncher, Command, Data, DelegateCtx, Env, FileDialogOptions, FileSpec, Handled, Lens, Menu, MenuItem, RawMods, Selector, Target, Widget, WindowDesc, WindowId};
|
|
use druid::widget::{Align, Padding};
|
|
use num_format::Locale;
|
|
|
|
use crate::{GameState, Notes, NotesState};
|
|
use crate::widget::notes_viewer_scroll::NotesViewerScroll;
|
|
|
|
#[derive(Clone, Data, Lens)]
|
|
pub struct App {
|
|
pub notes: Option<(Notes, String)>,
|
|
pub game_state: Option<GameState>,
|
|
pub notes_state: Option<NotesState>,
|
|
#[data(same_fn = "PartialEq::eq")]
|
|
locale: Locale,
|
|
change_counter: u64,
|
|
update_thread: Option<Arc<JoinHandle<()>>>,
|
|
update_thread_continue: Arc<AtomicBool>,
|
|
}
|
|
|
|
impl App {
|
|
fn changed(&mut self) {
|
|
self.change_counter = self.change_counter.wrapping_add(1);
|
|
}
|
|
}
|
|
|
|
impl Default for App {
|
|
fn default() -> Self {
|
|
Self {
|
|
notes: None,
|
|
game_state: None,
|
|
notes_state: None,
|
|
locale: Locale::en_GB,
|
|
change_counter: 0,
|
|
update_thread: None,
|
|
update_thread_continue: Arc::new(AtomicBool::new(true)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl App {
|
|
const CHANGED: Selector<()> = Selector::new("changed");
|
|
const CONNECT: Selector<()> = Selector::new("connect");
|
|
const DISCONNECT: Selector<()> = Selector::new("disconnect");
|
|
const REFRESH: Selector<()> = Selector::new("refresh");
|
|
|
|
pub fn launch(mut self) -> Result<()> {
|
|
let main_window = App::build_window();
|
|
let launcher = AppLauncher::with_window(main_window);
|
|
let event_sink = launcher.get_external_handle();
|
|
|
|
let should_continue = Arc::clone(&self.update_thread_continue);
|
|
self.update_thread = Some(Arc::new(std::thread::spawn(move || {
|
|
while should_continue.load(Ordering::SeqCst) {
|
|
event_sink.submit_command(
|
|
Self::REFRESH,
|
|
(),
|
|
Target::Auto,
|
|
).ok();
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(16));
|
|
}
|
|
})));
|
|
|
|
launcher
|
|
.delegate(Delegate)
|
|
.launch(self)
|
|
.context("could not launch app")
|
|
}
|
|
|
|
fn build_window() -> WindowDesc<App> {
|
|
WindowDesc::new(Self::build_root_widget())
|
|
.title("FFXII: TZA Auto Notes")
|
|
.menu(Self::build_menu)
|
|
.window_size((400.0, 800.0))
|
|
}
|
|
|
|
fn build_root_widget() -> impl Widget<App> {
|
|
// let location = Label::new(|data: &Option<GameState>, _: &Env| {
|
|
// match &data {
|
|
// Some(game_state) => format!(
|
|
// "{} ({})",
|
|
// game_state.location_name(),
|
|
// game_state.location,
|
|
// ),
|
|
// None => "Not connected".into(),
|
|
// }
|
|
// })
|
|
// .with_line_break_mode(LineBreaking::WordWrap)
|
|
// .lens(App::game_state);
|
|
//
|
|
// let stage = Label::new(|data: &Option<GameState>, _: &Env| {
|
|
// match &data {
|
|
// Some(game_state) => game_state.stage.to_string(),
|
|
// None => "Not connected".into(),
|
|
// }
|
|
// })
|
|
// .with_line_break_mode(LineBreaking::WordWrap)
|
|
// .lens(App::game_state);
|
|
//
|
|
// let notes_state = Label::new(|data: &Option<NotesState>, _: &Env| {
|
|
// match data {
|
|
// Some(notes_state) => format!("Step {:?}, area {}", notes_state.step_idx, notes_state.area_idx),
|
|
// None => "Not connected".into(),
|
|
// }
|
|
// })
|
|
// .with_line_break_mode(LineBreaking::WordWrap)
|
|
// .lens(App::notes_state);
|
|
//
|
|
// let buttons = Flex::row()
|
|
// .with_flex_child(
|
|
// Button::new("Stage")
|
|
// .on_click(|ctx, data: &mut Option<GameState>, env: &Env| {
|
|
// let game_state = match data {
|
|
// Some(game_state) => game_state.clone(),
|
|
// None => return,
|
|
// };
|
|
//
|
|
// if let Ok(mut clipboard) = clipboard::ClipboardContext::new() {
|
|
// clipboard.set_contents(format!(
|
|
// " - stage: {stage}
|
|
// areas:
|
|
// # {area_id}: {area_name}
|
|
// - area: {area_id}
|
|
// steps: |-
|
|
// - ",
|
|
// stage = game_state.stage,
|
|
// area_id = game_state.location,
|
|
// area_name = game_state.location_name(),
|
|
// )).ok();
|
|
// }
|
|
// })
|
|
// .lens(App::game_state),
|
|
// 1.0,
|
|
// )
|
|
// .with_flex_child(
|
|
// Button::new("Area")
|
|
// .on_click(|ctx, data: &mut Option<GameState>, env: &Env| {
|
|
// let game_state = match data {
|
|
// Some(game_state) => game_state.clone(),
|
|
// None => return,
|
|
// };
|
|
//
|
|
// if let Ok(mut clipboard) = clipboard::ClipboardContext::new() {
|
|
// clipboard.set_contents(format!(
|
|
// " # {area_id}: {area_name}
|
|
// - area: {area_id}
|
|
// steps: |-
|
|
// - ",
|
|
// area_id = game_state.location,
|
|
// area_name = game_state.location_name(),
|
|
// )).ok();
|
|
// }
|
|
// })
|
|
// .lens(App::game_state),
|
|
// 1.0,
|
|
// );
|
|
|
|
// let info = Flex::column()
|
|
// .with_child(location)
|
|
// .with_child(Separator::new().with_orientation(Orientation::Horizontal))
|
|
// .with_child(stage)
|
|
// .with_child(Separator::new().with_orientation(Orientation::Horizontal))
|
|
// .with_child(notes_state)
|
|
// .with_child(Separator::new().with_orientation(Orientation::Horizontal))
|
|
// .with_child(buttons);
|
|
|
|
// let layout = Split::columns(NotesViewerScroll::new(), info)
|
|
// .draggable(true)
|
|
// .split_point(0.75);
|
|
|
|
Align::centered(Padding::new((8.0, 8.0, 8.0, 0.0), NotesViewerScroll::new()))
|
|
}
|
|
|
|
fn build_menu(_: Option<WindowId>, _data: &App, _env: &Env) -> Menu<App> {
|
|
Menu::empty()
|
|
.entry(Self::build_file_menu())
|
|
}
|
|
|
|
fn build_file_menu() -> Menu<App> {
|
|
let connect: MenuItem<App> = MenuItem::new("Connect")
|
|
.command(Command::new(Self::CONNECT, (), Target::Auto))
|
|
.enabled_if(|data: &App, _: &Env| data.game_state.is_none());
|
|
|
|
let disconnect: MenuItem<App> = MenuItem::new("Disconnect")
|
|
.command(Command::new(Self::DISCONNECT, (), Target::Auto))
|
|
.enabled_if(|data: &App, _: &Env| data.game_state.is_some());
|
|
|
|
let yaml = FileSpec::new("Auto notes", &["yml", "yaml"]);
|
|
let open_options = FileDialogOptions::new()
|
|
.allowed_types(vec![yaml])
|
|
.default_type(yaml)
|
|
.name_label("Notes")
|
|
.title("Open auto notes")
|
|
.button_text("Open");
|
|
let open_notes: MenuItem<App> = MenuItem::new("Open notes")
|
|
.command(Command::new(
|
|
druid::commands::SHOW_OPEN_PANEL,
|
|
open_options,
|
|
Target::Auto))
|
|
.hotkey(RawMods::Ctrl, "o");
|
|
|
|
let exit: MenuItem<App> = MenuItem::new("Exit")
|
|
.command(druid::commands::CLOSE_ALL_WINDOWS);
|
|
|
|
Menu::new("File")
|
|
.entry(connect)
|
|
.entry(disconnect)
|
|
.separator()
|
|
.entry(open_notes)
|
|
.separator()
|
|
.entry(exit)
|
|
}
|
|
|
|
fn set_up_notes_state(&mut self) {
|
|
let notes = match self.notes.clone() {
|
|
Some((notes, _)) => notes,
|
|
None => return,
|
|
};
|
|
|
|
if let Some(game_state) = &mut self.game_state {
|
|
match NotesState::new(notes, game_state) {
|
|
Ok(notes_state) => self.notes_state = Some(notes_state),
|
|
Err(e) => eprintln!("could not set up notes state: {:#?}", e),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Delegate;
|
|
|
|
impl AppDelegate<App> for Delegate {
|
|
fn command(&mut self, _ctx: &mut DelegateCtx, _target: Target, cmd: &Command, data: &mut App, _env: &Env) -> Handled {
|
|
if let Some(()) = cmd.get(App::CHANGED) {
|
|
data.changed();
|
|
}
|
|
|
|
if let Some(()) = cmd.get(App::CONNECT) {
|
|
match GameState::new() {
|
|
Ok(game_state) => {
|
|
data.game_state = Some(game_state);
|
|
data.set_up_notes_state();
|
|
data.changed();
|
|
}
|
|
Err(e) => eprintln!("could not connect: {:#?}", e),
|
|
}
|
|
|
|
return Handled::Yes;
|
|
}
|
|
|
|
if let Some(()) = cmd.get(App::DISCONNECT) {
|
|
data.game_state = None;
|
|
data.notes_state = None;
|
|
}
|
|
|
|
if let Some(()) = cmd.get(App::REFRESH) {
|
|
if let Some(game_state) = &mut data.game_state {
|
|
match &mut data.notes_state {
|
|
Some(notes_state) => {
|
|
// this will refresh gamestate
|
|
if let Err(e) = notes_state.tick(game_state) {
|
|
eprintln!("could not refresh notes: {:#?}", e);
|
|
}
|
|
}
|
|
None => if let Err(e) = game_state.refresh() {
|
|
eprintln!("could not refresh game state: {:#?}", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(info) = cmd.get(druid::commands::OPEN_FILE) {
|
|
match File::open(info.path())
|
|
.context("could not open notes file")
|
|
.and_then(|file| serde_yaml::from_reader::<_, Notes>(file).context("could not parse notes file"))
|
|
{
|
|
Ok(notes) => {
|
|
data.notes = Some((notes.clone(), info.path().to_string_lossy().to_string()));
|
|
data.set_up_notes_state();
|
|
data.changed();
|
|
}
|
|
Err(err) => {
|
|
eprintln!("{:#?}", err);
|
|
}
|
|
}
|
|
return Handled::Yes;
|
|
}
|
|
|
|
Handled::No
|
|
}
|
|
|
|
fn window_removed(&mut self, _id: WindowId, data: &mut App, _env: &Env, _ctx: &mut DelegateCtx) {
|
|
let thread = match data.update_thread.take() {
|
|
Some(thread) => thread,
|
|
None => return,
|
|
};
|
|
|
|
data.update_thread_continue.store(false, Ordering::SeqCst);
|
|
if let Ok(thread) = Arc::try_unwrap(thread) {
|
|
thread.join().ok();
|
|
}
|
|
}
|
|
}
|