From 6282f12ce9c62123451cc397e72a5f455fa31c81 Mon Sep 17 00:00:00 2001 From: isark Date: Thu, 22 Feb 2024 21:22:46 +0100 Subject: [PATCH] Some more backend api improvements. TODO: Adapt frontend usage to this new interface --- src-tauri/src/main.rs | 92 ++++---------- src-tauri/src/overlay.rs | 1 - src-tauri/src/plan.rs | 60 +++++++-- src-tauri/src/poe_reader.rs | 7 +- src-tauri/src/storage.rs | 190 +++++++++++++++------------- src/app/_models/plan.ts | 4 + src/app/_services/config.service.ts | 2 +- src/app/_services/plan.service.ts | 2 +- 8 files changed, 184 insertions(+), 174 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 70bdc36..20715ee 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,7 +1,6 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use std::collections::HashMap; use std::sync::mpsc::Receiver; use std::{path::PathBuf, sync::Mutex}; @@ -10,7 +9,7 @@ use crossbeam::channel::Sender; use notify::PollWatcher; use overlay::{Event, Overlay}; -use plan::Plan; +use plan::{Plan, PlanMetadata}; use poe_data::world_area::WorldAreasMap; use poe_reader::filter_func; use poe_reader::{blocking_area_filtered_rx, poe_client_log_receiver}; @@ -33,6 +32,12 @@ mod plan; mod poe_reader; mod storage; +lazy_static! { + static ref WORLD_AREAS_MAP: WorldAreasMap = poe_data::world_area::load_world_areas_map( + include_str!("../../data/processed_world_areas.json") + ); +} + #[tauri::command] fn set_interactable(interactable: bool, state: tauri::State>) { if interactable { @@ -42,12 +47,6 @@ fn set_interactable(interactable: bool, state: tauri::State>) { } } -lazy_static! { - static ref WORLD_AREAS_MAP: WorldAreasMap = poe_data::world_area::load_world_areas_map( - include_str!("../../data/processed_world_areas.json") - ); -} - #[tauri::command] fn load_world_areas() -> WorldAreasMap { log::info!("Loading world areas"); @@ -60,35 +59,33 @@ fn load_config(state: tauri::State>) -> Option { } #[tauri::command] -fn set_config(config: Config, state: tauri::State>) { +fn update_config(config: Config, state: tauri::State>) { log::info!("Saved config: {:?}", config); if let Ok(mut storage) = state.lock() { storage.config = config.clone(); - storage.save(); + storage.save_config(); } } #[tauri::command] -fn load_plan(path: PathBuf, state: tauri::State>) -> Option { - if let Ok(mut storage) = state.lock() { - if let Some(path_string) = path.to_str() { - storage.last_plan = Some(path_string.to_string()); - let plan = Storage::load_plan(storage.last_plan.as_ref()?); - log::info!("got plan: {plan:?}"); - return plan; - } - } - None +fn enumerate_stored_plans() -> Vec { + Storage::enumerate_plans() } + #[tauri::command] -fn save_plan(path: PathBuf, plan: Plan) -> bool { - if let Some(path_string) = path.with_extension("json").to_str() { - log::trace!("Attempting to save plan at path {path_string}"); - return Storage::save_plan_at_path(path_string, plan).is_ok(); +fn load_plan_at_path(path: PathBuf, state: tauri::State>) -> Option { + if !path.exists() { + return None; } - false + let mut storage = match state.lock() { + Ok(storage) => storage, + Err(_) => return None, + }; + + storage.last_plan = Some(path.clone()); + Storage::load_plan_at_path(path) } #[tauri::command] @@ -100,40 +97,6 @@ fn save_plan_at_path(path: PathBuf, plan: Plan) -> bool { false } -#[tauri::command] -fn save_plan_by_name(name: String, plan: Plan) -> bool { - let path = Storage::create_path_for_name(name); - log::trace!("plan: {plan:?}"); - - if let Some(path) = path { - return save_plan(path.into(), plan); - } - - false -} - -#[tauri::command] -fn load_stored_plans() -> HashMap { - Storage::enumerate_plans() - .into_iter() - .map(|name| (name.clone(), Storage::load_by_name(name).unwrap())) - .collect() -} - -#[tauri::command] -fn load_previous(name: String) -> Option { - let plan = Storage::load_by_name(name); - log::info!("got plan: {plan:?}"); - return plan; -} - -#[tauri::command] -fn path_for_previous(prev: String) -> Option { - let path = Storage::path_for_name(prev); - log::info!("got path: {path:?}"); - return path; -} - #[tauri::command] fn base_plan() -> Plan { const BASE_PLAN_STRING: &str = include_str!("../../data/base_plan.json"); @@ -187,14 +150,11 @@ fn main() { set_interactable, load_world_areas, load_config, - set_config, - load_plan, - load_stored_plans, - save_plan, - save_plan_by_name, + update_config, + enumerate_stored_plans, + load_plan_at_path, + save_plan_at_path, base_plan, - load_previous, - path_for_previous, ]) .system_tray(system_tray) .on_system_tray_event(|app, event| match event { diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index 8beeedf..c875585 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -39,7 +39,6 @@ pub struct Overlay { pub struct OverlayData { // pub auto_hide: bool, } -pub type LockedOverlayData = Mutex; fn wrap_underlay_rx(rx: MpscReceiver) -> Receiver { let (cb_underlay_tx, cb_underlay_rx) = crossbeam::channel::unbounded(); diff --git a/src-tauri/src/plan.rs b/src-tauri/src/plan.rs index 41a421c..9b088b4 100644 --- a/src-tauri/src/plan.rs +++ b/src-tauri/src/plan.rs @@ -1,32 +1,65 @@ -use std::collections::HashMap; +use std::{collections::HashMap, path::PathBuf}; -use serde::{Deserialize, Serialize}; +use serde::{ser::SerializeStruct, Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Plan { plan: Vec, current: usize, - stored_path: Option, + #[serde(flatten)] + metadata: PlanMetadata, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct PlanMetadata { + #[serde(skip)] + stored_path: Option, update_url: Option, } + +impl From for Option { + fn from(metadata: PlanMetadata) -> Self { + Some(serde_json::from_slice(&std::fs::read(metadata.stored_path?).ok()?).ok()?) + } +} + +impl Serialize for PlanMetadata { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("PlanMetadata", 2)?; + + state.serialize_field("update_url", &self.update_url)?; + + if let Some(path) = &self.stored_path { + if let Some(name) = path.file_name() { + state.serialize_field("name", name)?; + } + } + + state.end() + } +} + impl Plan { - pub fn set_stored_path(&mut self, file: String) { - self.stored_path = Some(file); + pub fn set_stored_path(&mut self, file: PathBuf) { + self.metadata.stored_path = Some(file); } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct PlanElement { area_key: String, notes: String, - + #[serde(default = "PlanElement::generate_uuid")] uuid: uuid::Uuid, #[serde(default = "PlanElement::edited")] edited: bool, - anchor_act: Option + anchor_act: Option, } impl PlanElement { @@ -39,12 +72,14 @@ impl PlanElement { } } +#[allow(dead_code)] #[derive(Debug, Deserialize)] struct OldPlanElement { area: OldArea, note: String, } +#[allow(dead_code)] #[derive(Debug, Deserialize)] struct OldPlan { elements: Vec, @@ -56,7 +91,8 @@ struct OldArea { _rid: usize, } -pub fn convert_old(path: &str) -> Option { +#[allow(dead_code)] +pub fn convert_old(path: PathBuf) -> Option { let plan: OldPlan = serde_json::from_str(&std::fs::read_to_string(path).expect("Could not convert old")) .expect("could not convert old"); @@ -82,7 +118,9 @@ pub fn convert_old(path: &str) -> Option { anchor_act: None, }) .collect::>(), + metadata: PlanMetadata { stored_path: None, - update_url: None, + update_url: None, + }, }) } diff --git a/src-tauri/src/poe_reader.rs b/src-tauri/src/poe_reader.rs index af5a7a1..1d6baa5 100644 --- a/src-tauri/src/poe_reader.rs +++ b/src-tauri/src/poe_reader.rs @@ -1,12 +1,12 @@ use std::{ - fs::{File, OpenOptions}, + fs::OpenOptions, io::{Read, Seek, SeekFrom}, path::PathBuf, sync::mpsc::{channel, Receiver, Sender}, time::Duration, }; -use notify::{event, Config, PollWatcher, RecursiveMode, Watcher, Event}; +use notify::{Config, PollWatcher, RecursiveMode, Watcher, Event}; use regex::Regex; #[cfg(target_os = "windows")] @@ -31,7 +31,7 @@ fn share_mode(options: &mut OpenOptions) { } #[cfg(not(target_os = "windows"))] -fn share_mode(options: &mut OpenOptions) {} +fn share_mode(_options: &mut OpenOptions) {} impl LogFileReader { fn new(file: &PathBuf, tx: Sender) -> Result { @@ -117,6 +117,7 @@ fn entered_filtered_rx<'a>(rx: &'a Receiver) -> impl Iterator(rx: &'a Receiver) -> impl Iterator + 'a { // Generating level 68 area "1_SideArea5_6" with seed 4103532853 diff --git a/src-tauri/src/storage.rs b/src-tauri/src/storage.rs index 792aa6b..6629595 100644 --- a/src-tauri/src/storage.rs +++ b/src-tauri/src/storage.rs @@ -1,16 +1,16 @@ use std::error::Error; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use crate::config::Config; -use crate::plan::{convert_old, Plan}; +use crate::plan::{convert_old, Plan, PlanMetadata}; #[derive(Serialize, Deserialize, Debug)] pub struct Storage { pub config: Config, - pub last_plan: Option, + pub last_plan: Option, } const QUALIFIER: &'static str = "me"; @@ -19,23 +19,19 @@ const APPLICATION: &'static str = "Nothing"; const CONFIG_FILE: &'static str = "configuration.json"; const SAVED_PLANS: &'static str = "plans"; -fn proj_dir() -> Option { - ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) -} - -fn mkdir() { - if let Some(proj_dir) = proj_dir() { - let dir_structure = proj_dir.data_dir().join(SAVED_PLANS); +fn mkdir_for_storage() { + let dir_structure = Storage::plan_dir(); + if let Some(dir_structure) = dir_structure { std::fs::create_dir_all(dir_structure) - .map_err(|e| log::error!("Could not create directory for storing config and saves")) + .map_err(|_e| log::error!("Could not create directory for storing config and saves")) .ok(); } } impl Default for Storage { fn default() -> Self { - mkdir(); - let storage = Self::load(); + mkdir_for_storage(); + let storage = Self::load_storage(); match storage { Some(storage) => storage, None => Self { @@ -47,113 +43,125 @@ impl Default for Storage { } impl Storage { - fn load() -> Option { - let storage: Option = serde_json::from_str( - &std::fs::read_to_string(proj_dir()?.data_dir().join(CONFIG_FILE)).ok()?, - ) - .ok(); + pub fn proj_dir() -> Option { + ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) + } - log::info!("Loaded storage: {:?}", storage); + pub fn plan_dir() -> Option { + Some(Self::proj_dir()?.data_dir().join(SAVED_PLANS)) + } - storage + pub fn config_file() -> Option { + Some(Self::proj_dir()?.data_dir().join(CONFIG_FILE)) } - pub fn save(&self) { - if let Ok(content) = serde_json::to_string_pretty(&self) { - if let Some(dir) = proj_dir() { - match std::fs::write(dir.data_dir().join(CONFIG_FILE), content) { - Ok(_) => { - if let Some(c) = dir.data_dir().join(CONFIG_FILE).to_str() { - log::info!("Saved config to {}", c) - } - } - Err(_) => log::error!("Could not write config"), + pub fn save_config(&self) { + let content = match serde_json::to_string_pretty(&self) { + Ok(content) => content, + Err(_) => return, + }; + + let config_file = match Self::config_file() { + Some(config_file) => config_file, + None => return, + }; + + match std::fs::write(&config_file, content) { + Ok(_) => { + if let Some(c) = config_file.to_str() { + log::info!("Saved config to {c}"); } } + Err(_) => log::error!("Could not write config"), } } pub fn save_plan_at_path>(file: T, plan: Plan) -> Result<(), Box> { std::fs::write(&file.into(), serde_json::to_string(&plan)?)?; + Ok(()) } - pub fn load_plan>(file: T) -> Option { - let mut file = file.into(); - let plan_file = Path::new(&file); - //basically copy to our own dir to store if for reuse without contaminating or depending on the original file - if let Some(path) = plan_file.parent() { - if !path.ends_with(SAVED_PLANS) { - if let Some(proj_dir) = proj_dir() { - if let Some(file_name) = plan_file.file_name() { - let dest = proj_dir.data_dir().join(SAVED_PLANS).join(file_name); - let copy_result = std::fs::copy(&file, &dest); - match copy_result { - Ok(_) => { - file = dest.to_str()?.to_string(); - } - Err(e) => log::error!("Could not store plan file {e:?}"), - } - } - } - } + pub fn load_plan_at_path(path: PathBuf) -> Option { + log::trace!("Loading plan: {path:?}"); + let plan: Plan = match serde_json::from_str(&std::fs::read_to_string(&path).ok()?).ok() { + Some(plan) => plan, + None => convert_old(path.clone())?, + }; + + match Self::save_plan_at_store_path(path.file_name()?.to_str()?, plan.clone()) { + Ok(_) => (), + Err(_e) => { + log::error!("Could not save plan at store path during load"); + }, } + + Some(plan) + } - log::info!("Loading plan: {file:?}"); - let mut plan = if let Some(plan) = serde_json::from_str(&std::fs::read_to_string(&file).ok()?).ok() { - plan - } else { - log::info!("Attempting to convert old"); - let plan = convert_old(&file)?; - std::fs::write( - &file, - serde_json::to_string(&plan) - .map_err(|e| log::error!("Could not serialize converted plan to json {e:?}")) - .ok()?, - ) - .map_err(|e| "Could not write converted plan to storage") - .ok(); - Some(plan) + pub fn save_plan_at_store_path(file_name: &str, mut plan: Plan) -> Result<(), Box> { + let plan_dir = match Self::plan_dir() { + Some(dir) => dir, + None => return Err("No plan dir".into()), }; - if let Some(plan) = &mut plan { - plan.set_stored_path(file); + let file_path = plan_dir.with_file_name(file_name).with_extension("json"); + + //Disallow overwriting. + if file_path.exists() { + return Err("File already exists".into()); } - plan + plan.set_stored_path(file_path.clone()); + + std::fs::write(file_path, serde_json::to_string(&plan)?)?; + + Ok(()) } - pub fn load_by_name>(file: T) -> Option { - let file = file.into(); - let file = proj_dir()?.data_dir().join(SAVED_PLANS).join(file); + pub fn enumerate_plans() -> Vec { + let plan_dir = match Self::plan_dir() { + Some(dir) => dir, + None => return vec![], + }; - log::info!("Loading plan: {file:?}"); + let read_dir = match plan_dir.read_dir() { + Ok(read_dir) => read_dir, + Err(_) => return vec![], + }; - serde_json::from_str(&std::fs::read_to_string(&file).ok()?).ok() - } + read_dir + .filter_map(|entry| { + let path = entry.ok()?.path(); - pub fn enumerate_plans() -> Vec { - if let Some(proj_dir) = proj_dir() { - if let Ok(read_dir) = proj_dir.data_dir().join(SAVED_PLANS).read_dir() { - return read_dir - .filter_map(|entry| Some(entry.ok()?.path().file_name()?.to_str()?.to_string())) - .collect::>(); - } - } + if path.extension()? != "json" { + return None; + } - return vec![]; + Self::load_metadata_at_path(path) + }) + .collect() } - pub fn path_for_name>(file: T) -> Option { - let file = file.into(); - let file = proj_dir()?.data_dir().join(SAVED_PLANS).join(file); - Some(file.to_str()?.to_string()) + //TODO: Remove if this turns out to be unnecessary. + // pub fn create_path_for_local_plan(name: &str, plan: &Plan) -> Option { + // let file: PathBuf = Self::plan_dir()?.join(name).with_extension("json"); + + // Some(file.to_str()?.to_string()) + // } + + fn load_storage() -> Option { + let storage: Option = serde_json::from_str( + &std::fs::read_to_string(Self::proj_dir()?.data_dir().join(CONFIG_FILE)).ok()?, + ) + .ok(); + + log::trace!("Loaded storage: {:?}", storage); + + storage } - pub fn create_path_for_name>(file_name: T) -> Option { - let file_name = file_name.into(); - let file: PathBuf = proj_dir()?.data_dir().join(SAVED_PLANS).join(file_name).with_extension("json"); - Some(file.to_str()?.to_string()) + fn load_metadata_at_path(path: PathBuf) -> Option { + serde_json::from_str(&std::fs::read_to_string(&path).ok()?).ok() } } - diff --git a/src/app/_models/plan.ts b/src/app/_models/plan.ts index d01de44..ecd6a0a 100644 --- a/src/app/_models/plan.ts +++ b/src/app/_models/plan.ts @@ -9,6 +9,10 @@ export interface PlanInterface { name?: string; } +export interface PlanMetadata { + +} + export class Plan { plan: PlanElement[]; current: number; diff --git a/src/app/_services/config.service.ts b/src/app/_services/config.service.ts index c62159f..c341df4 100644 --- a/src/app/_services/config.service.ts +++ b/src/app/_services/config.service.ts @@ -28,7 +28,7 @@ export class ConfigService { console.error("Could not load config, should generally not happen :')"); return; } - + const _this = this; this.cfg = cfg; // Mostly to wrap setters so we can run the (debounced) sync function on any config changes! diff --git a/src/app/_services/plan.service.ts b/src/app/_services/plan.service.ts index f415cf8..6e88371 100644 --- a/src/app/_services/plan.service.ts +++ b/src/app/_services/plan.service.ts @@ -39,7 +39,7 @@ export class PlanService { } public loadPlanFromPath(path: string): Observable { - return from(invoke('load_plan', { path })); + return from(invoke('load_plan_at_path', { path })); } public loadFromUrl(url?: string, name?: string): Observable {