Compare commits

..

No commits in common. 'main' and 'cleanup' have entirely different histories.

@ -1,47 +0,0 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended",
"plugin:@angular-eslint/template/accessibility"
],
"rules": {}
}
]
}

1
.gitignore vendored

@ -45,7 +45,6 @@ Thumbs.db
package-lock.json
processed_world_areas.json
processed_world_areas_full.json
releases
.env
releaser_key

@ -31,7 +31,8 @@
"styles": [
"src/styles.scss"
],
"scripts": []
"scripts": [
]
},
"configurations": {
"production": {
@ -71,15 +72,6 @@
}
},
"defaultConfiguration": "development"
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
@ -90,9 +82,6 @@
}
},
"cli": {
"analytics": false,
"schematicCollections": [
"@angular-eslint/schematics"
]
"analytics": false
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -6,8 +6,7 @@
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"lint": "ng lint"
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
@ -23,9 +22,7 @@
"@tauri-apps/api": "^1.2.0",
"@types/markdown-it": "^13.0.0",
"@types/natural-compare": "^1.4.1",
"@types/uuid": "^9.0.8",
"angular-resize-event": "^3.2.0",
"angular-svg-icon": "^17.0.0",
"bootstrap": "^5.3.2",
"fuzzr": "https://github.com/isark2/fuzzr#v0.3.1",
"markdown-it": "^13.0.1",
@ -35,24 +32,15 @@
"ngx-moveable": "^0.48.1",
"rxjs": "~7.8.1",
"tslib": "^2.6.0",
"uuid": "^9.0.1",
"vanilla-picker": "^2.12.1",
"zone.js": "^0.13.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "^16.1.4",
"@angular-eslint/builder": "17.3.0",
"@angular-eslint/eslint-plugin": "17.3.0",
"@angular-eslint/eslint-plugin-template": "17.3.0",
"@angular-eslint/schematics": "17.3.0",
"@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "~16.1.4",
"@angular/compiler-cli": "^16.1.4",
"@tauri-apps/cli": "^1.4.0",
"@types/jasmine": "~4.3.0",
"@typescript-eslint/eslint-plugin": "7.2.0",
"@typescript-eslint/parser": "7.2.0",
"eslint": "^8.57.0",
"jasmine-core": "~5.0.1",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
@ -61,4 +49,4 @@
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.1.6"
}
}
}

10
src-tauri/Cargo.lock generated

@ -1972,7 +1972,6 @@ dependencies = [
"regex",
"serde",
"serde_json",
"serde_with",
"simple_logger",
"statig",
"steamlocate",
@ -2944,9 +2943,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.7.0"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a"
checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49"
dependencies = [
"base64 0.21.2",
"chrono",
@ -2954,7 +2953,6 @@ dependencies = [
"indexmap 1.9.3",
"indexmap 2.0.0",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"time",
@ -2962,9 +2960,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.7.0"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655"
checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7"
dependencies = [
"darling",
"proc-macro2",

@ -20,7 +20,7 @@ ts-rs = "6.2.1"
[dependencies]
steamlocate = "1.2.1"
tauri = { version = "1.2", features = [ "dialog-message", "http-request", "dialog-open", "global-shortcut-all", "dialog-save", "updater", "system-tray"] }
tauri = { version = "1.2", features = [ "http-request", "dialog-open", "global-shortcut-all", "dialog-save", "updater", "system-tray"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Underlayer = { git = "https://git.isark.me/isark/Underlay.git" }
@ -39,7 +39,6 @@ notify = "6.0.1"
regex = "1.9.3"
lazy_static = "1.4.0"
uuid = { version = "1.6.1", features = ["v4", "serde"] }
serde_with = "3.7.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem

@ -37,6 +37,9 @@ struct UnprocessedArea {
#[serde(rename = "IsVaalArea")]
pub is_vaal_area: bool,
#[serde(rename = "Unknown64")]
pub unknown64: bool,
#[serde(rename = "Unknown9")]
pub unknown9: i64,
}
@ -58,7 +61,8 @@ pub type AreaId = usize;
pub type WorldAreasMap = HashMap<String, WorldArea>;
#[allow(dead_code)]
pub fn load_world_areas_map(content: &'static str) -> WorldAreasMap {
serde_json::from_str::<WorldAreasMap>(content).expect("Could not load world areas json")
serde_json::from_str::<WorldAreasMap>(content)
.expect("Could not load world areas json")
}
#[macro_export]
@ -202,6 +206,7 @@ pub fn repack(content: &str, out_path: &str) {
if w_a.act < 11
&& !w_a.is_vaal_area
&& !w_a.connections_world_areas_keys.is_empty()
&& !w_a.unknown64
&& !w_a.named_id.starts_with("Map")
&& !w_a.named_id.starts_with("Descent")
&& !w_a.named_id.starts_with("EndlessLedge")
@ -229,31 +234,3 @@ pub fn repack(content: &str, out_path: &str) {
std::fs::write(out_path, &new_json).expect("Could not write to file");
}
#[allow(dead_code)]
pub fn repack_full(content: &str, out_path: &str) {
let areas_json = serde_json::from_str::<UnprocessedAreas>(content);
let area_map = areas_json
.expect("Could not read world areas")
.into_iter()
.map(|w_a| {
(
w_a.named_id.clone(),
WorldArea {
name: w_a.name,
named_id: w_a.named_id,
act: w_a.act,
is_town: w_a.is_town,
has_waypoint: w_a.has_waypoint,
connections_world_areas_keys: w_a.connections_world_areas_keys,
key_id: w_a.key_id,
},
)
})
.collect::<WorldAreasMap>();
let new_json = serde_json::to_string(&area_map).unwrap();
std::fs::write(out_path, &new_json).expect("Could not write to file");
}

@ -6,10 +6,8 @@ fn main() {
tauri_build::build();
const OUT_PROCESSED: &'static str = "../data/processed_world_areas.json";
const OUT_PROCESSED_FULL: &'static str = "../data/processed_world_areas_full.json";
println!("cargo:rerun-if-changed=../data/WorldAreas.json");
poe_data::world_area::repack(&include_str!("../../data/WorldAreas.json"), OUT_PROCESSED);
poe_data::world_area::repack_full(&include_str!("../../data/WorldAreas.json"), OUT_PROCESSED_FULL);
//Let's go with uppercase names, allows automatic import to work...
config::Rect::export_to("../src/app/_models/generated/Rect.ts").expect("Could not generate config struct");

@ -26,10 +26,6 @@ pub struct Rect {
pub struct Config {
#[serde(default = "Config::default_initial_plan_window_position")]
pub initial_plan_window_position: Rect,
#[serde(default = "Config::default_initial_agg_window_position")]
pub initial_agg_window_position: Rect,
#[serde(default = "Config::default_initial_notes_window_position")]
pub initial_notes_window_position: Rect,
#[serde(default = "Config::default_hide_on_unfocus")]
pub hide_on_unfocus: bool,
#[serde(default = "Config::default_toggle_overlay")]
@ -51,29 +47,12 @@ pub struct Config {
pub num_visible: u16,
#[serde(default = "Config::default_plan_offset")]
pub offset: i16,
#[serde(default = "Config::default_enable_stopwatch")]
pub enable_stopwatch: bool,
#[serde(default = "Config::default_run_compare_history")]
pub run_compare_history: Option<String>,
#[serde(default = "Config::default_detach_notes")]
detach_notes: bool,
#[serde(default = "Config::default_show_livesplit")]
show_livesplit: bool,
#[serde(default = "Config::default_show_live_aggregate")]
show_live_aggregate: bool,
#[serde(default = "Config::default_shorten_zone_names")]
shorten_zone_names: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
initial_plan_window_position: Self::default_initial_plan_window_position(),
initial_agg_window_position: Self::default_initial_agg_window_position(),
initial_notes_window_position: Self::default_initial_agg_window_position(),
hide_on_unfocus: Self::default_hide_on_unfocus(),
toggle_overlay: Self::default_toggle_overlay(),
prev: Self::default_prev(),
@ -87,12 +66,6 @@ impl Default for Config {
note_default_fg: Self::default_note_default_fg(),
num_visible: Self::default_plan_num_visible(),
offset: Self::default_plan_offset(),
run_compare_history: Self::default_run_compare_history(),
detach_notes: Self::default_detach_notes(),
enable_stopwatch: Self::default_enable_stopwatch(),
show_livesplit: Self::default_show_livesplit(),
show_live_aggregate: Self::default_show_live_aggregate(),
shorten_zone_names: Self::default_shorten_zone_names(),
}
}
}
@ -102,14 +75,6 @@ impl Config {
Default::default()
}
fn default_initial_agg_window_position() -> Rect {
Default::default()
}
fn default_initial_notes_window_position() -> Rect {
Default::default()
}
fn default_hide_on_unfocus() -> bool {
true
}
@ -153,23 +118,4 @@ impl Config {
fn default_plan_offset() -> i16 {
1
}
fn default_enable_stopwatch() -> bool {
false
}
fn default_run_compare_history() -> Option<String> {
None
}
fn default_show_livesplit() -> bool {
false
}
fn default_show_live_aggregate() -> bool {
false
}
fn default_shorten_zone_names() -> bool {
false
}
fn default_detach_notes() -> bool {
false
}
}

@ -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};
@ -26,14 +25,12 @@ use tauri::SystemTrayMenuItem;
use tauri::Window;
use lazy_static::lazy_static;
use time::{RunHistory, RunHistoryMetadata};
mod config;
mod overlay;
mod plan;
mod poe_reader;
mod storage;
mod time;
lazy_static! {
static ref WORLD_AREAS_MAP: WorldAreasMap = poe_data::world_area::load_world_areas_map(
@ -41,12 +38,6 @@ lazy_static! {
);
}
lazy_static! {
static ref FULL_WORLD_AREAS_MAP: WorldAreasMap = poe_data::world_area::load_world_areas_map(
include_str!("../../data/processed_world_areas_full.json")
);
}
#[tauri::command]
fn set_interactable(interactable: bool, state: tauri::State<Sender<Event>>) {
if interactable {
@ -62,13 +53,6 @@ fn load_world_areas() -> WorldAreasMap {
WORLD_AREAS_MAP.clone()
}
#[tauri::command]
fn load_full_world_areas() -> WorldAreasMap {
log::info!("Loading world areas");
FULL_WORLD_AREAS_MAP.clone()
}
#[tauri::command]
fn load_config(state: tauri::State<Mutex<Storage>>) -> Option<Config> {
Some(state.lock().ok()?.config.clone())
@ -88,21 +72,6 @@ fn enumerate_stored_plans() -> Vec<PlanMetadata> {
Storage::enumerate_plans()
}
#[tauri::command]
fn save_history(current_run_history: RunHistory) {
Storage::save_history(current_run_history);
}
#[tauri::command]
fn load_history_at_uuid(uuid: String) -> Option<RunHistory> {
Storage::load_history_at_uuid(uuid)
}
#[tauri::command]
fn load_cache() -> Option<HashMap<String, RunHistoryMetadata>> {
Storage::load_cache()
}
#[tauri::command]
fn load_plan_at_path(
path: PathBuf,
@ -134,8 +103,8 @@ fn save_plan_at_path(path: PathBuf, plan: Plan) -> bool {
}
#[tauri::command]
fn save_plan_at_store(name: String, plan: Plan, allow_overwrite: bool) -> Option<PathBuf> {
Storage::save_plan_at_store_path(&name, plan, allow_overwrite).ok()
fn save_plan_at_store(name: String, plan: Plan) -> Option<PathBuf> {
Storage::save_plan_at_store_path(&name, plan).ok()
}
#[tauri::command]
@ -153,13 +122,11 @@ fn main() {
let settings = CustomMenuItem::new("settings".to_string(), "Settings");
let editor = CustomMenuItem::new("editor".to_string(), "Plan Editor");
let runstats = CustomMenuItem::new("runstats".to_string(), "Run stats");
let force_show = CustomMenuItem::new("force_show".to_string(), "Force show");
let exit = CustomMenuItem::new("exit".to_string(), "Exit");
let tray_menu = SystemTrayMenu::new()
.add_item(settings)
.add_item(editor)
.add_item(runstats)
.add_item(force_show)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(exit);
@ -182,7 +149,7 @@ fn main() {
.expect("Could not get main overlay window"),
);
}
// app.get_window("Overlay")
// .expect("Could not get main overlay window")
// .open_devtools();
@ -192,7 +159,6 @@ fn main() {
.invoke_handler(tauri::generate_handler![
set_interactable,
load_world_areas,
load_full_world_areas,
load_config,
update_config,
enumerate_stored_plans,
@ -200,9 +166,6 @@ fn main() {
save_plan_at_path,
save_plan_at_store,
base_plan,
save_history,
load_history_at_uuid,
load_cache,
])
.system_tray(system_tray)
.on_system_tray_event(|app, event| match event {
@ -210,7 +173,7 @@ fn main() {
"exit" => {
std::process::exit(0);
}
"editor" | "settings" | "runstats" => {
"editor" | "settings" => {
if let Some(window) = app.get_window("Normal") {
window.show().ok();
window.emit_to("Normal", "loadTab", id).ok();
@ -240,11 +203,12 @@ fn listen_for_zone_changes(poe_client_log_path: Option<PathBuf>, window: Window)
std::thread::spawn(move || {
// need _watcher
if let Some((enter_area_receiver, _watcher)) = receiver_from_path(&poe_client_log_path) {
let world_areas: WorldAreasMap = load_world_areas();
for area in blocking_area_filtered_rx(&enter_area_receiver) {
if let Some(area) = filter_func(area) {
// if let Some(entered) = world_areas.get(&area) {
window.emit_to("Overlay", "entered", &area).ok();
// }
if let Some(entered) = world_areas.get(&area) {
window.emit_to("Overlay", "entered", &entered.named_id).ok();
}
}
}
}

@ -58,7 +58,7 @@ impl Overlay {
previous: State::hidden(),
};
window.manage(Mutex::new(OverlayData {}));
window.manage(Mutex::new(OverlayData { }));
let mut fsm = Overlay::uninitialized_state_machine(overlay).init();
@ -126,22 +126,19 @@ impl Overlay {
UnderlayEvent::MoveResize(bounds) => fsm.handle(&Event::Bounds(bounds)),
UnderlayEvent::Detach => fsm.handle(&State::hidden().into()),
UnderlayEvent::Focus => {
fsm.window.emit("overlay_or_target_focus", true).ok();
if let State::Hidden {} = fsm.state() {
fsm.handle(&State::Visible {}.into());
}
}
UnderlayEvent::Blur => {
if !fsm.window.is_focused().unwrap() {
fsm.window.emit("overlay_or_target_focus", false).ok();
if fsm
if !fsm.window.is_focused().unwrap()
&& fsm
.window
.state::<Mutex<Storage>>()
.lock()
.is_ok_and(|s| s.config.hide_on_unfocus)
{
fsm.handle(&State::Hidden {}.into())
}
{
fsm.handle(&State::Hidden {}.into())
}
}
UnderlayEvent::X11FullscreenEvent { is_fullscreen: _ } => {}

@ -1,10 +1,10 @@
use std::{collections::HashMap, path::PathBuf};
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize};
use serde_json::Value;
use serde::{ser::SerializeStruct, Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Plan {
pub struct
Plan {
plan: Vec<PlanElement>,
current: usize,
#[serde(flatten)]
@ -15,11 +15,6 @@ pub struct Plan {
pub struct PlanMetadata {
stored_path: Option<PathBuf>,
update_url: Option<String>,
latest_server_etag: Option<String>,
identifier: Option<String>,
#[serde(default)]
#[serde(deserialize_with = "deserialize_option_string")]
last_stored_time: Option<String>,
}
impl PlanMetadata {
@ -31,39 +26,23 @@ impl PlanMetadata {
impl Serialize for PlanMetadata {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("PlanMetadata", 6)?;
let mut state = serializer.serialize_struct("PlanMetadata", 3)?;
state.serialize_field("update_url", &self.update_url)?;
state.serialize_field("stored_path", &self.stored_path)?;
state.serialize_field("latest_server_etag", &self.latest_server_etag)?;
state.serialize_field("identifier", &self.identifier)?;
state.serialize_field("last_stored_time", &self.last_stored_time)?;
if let Some(path) = &self.stored_path {
if let Some(name) = path.file_name() {
state.serialize_field("name", &name.to_str())?;
}
}
state.end()
}
}
// Custom deserializer that accepts both strings and legacy number for the stored_time field
fn deserialize_option_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let v: Option<Value> = Option::deserialize(deserializer)?;
Ok(match v {
Some(Value::String(s)) => Some(s),
Some(Value::Number(n)) => Some(n.to_string()),
_ => None,
})
}
impl From<PlanMetadata> for Option<Plan> {
fn from(metadata: PlanMetadata) -> Self {
Some(serde_json::from_slice(&std::fs::read(metadata.stored_path?).ok()?).ok()?)
@ -88,13 +67,6 @@ pub struct PlanElement {
edited: bool,
anchor_act: Option<u8>,
#[serde(default, skip_serializing_if = "is_false")]
checkpoint: bool,
}
fn is_false(flag: &bool) -> bool {
!flag
}
impl PlanElement {
@ -151,15 +123,11 @@ pub fn convert_old(path: PathBuf) -> Option<Plan> {
uuid: PlanElement::generate_uuid(),
edited: PlanElement::edited(),
anchor_act: None,
checkpoint: false,
})
.collect::<Vec<PlanElement>>(),
metadata: PlanMetadata {
stored_path: None,
update_url: None,
latest_server_etag: None,
identifier: None,
last_stored_time: None,
},
})
}

@ -1,14 +1,12 @@
use std::collections::HashMap;
use std::error::Error;
use std::fs::DirEntry;
use std::path::PathBuf;
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::plan::{convert_old, Plan, PlanMetadata};
use crate::time::{RunHistory, RunHistoryMetadata};
#[derive(Serialize, Deserialize, Debug)]
pub struct Storage {
@ -20,9 +18,7 @@ const QUALIFIER: &'static str = "me";
const ORGANIZATION: &'static str = "isark.poe";
const APPLICATION: &'static str = "Nothing";
const CONFIG_FILE: &'static str = "configuration.json";
const HISTORY_CACHE_FILE: &'static str = "history_cache.json";
const SAVED_PLANS: &'static str = "plans";
const SAVED_HISTORIES: &'static str = "histories";
fn mkdir_for_storage() {
let dir_structure = Storage::plan_dir();
@ -31,13 +27,6 @@ fn mkdir_for_storage() {
.map_err(|_e| log::error!("Could not create directory for storing config and saves"))
.ok();
}
let dir_structure: Option<PathBuf> = Storage::history_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"))
.ok();
}
}
impl Default for Storage {
@ -63,18 +52,10 @@ impl Storage {
Some(Self::proj_dir()?.data_dir().join(SAVED_PLANS))
}
pub fn history_dir() -> Option<PathBuf> {
Some(Self::proj_dir()?.data_dir().join(SAVED_HISTORIES))
}
pub fn config_file() -> Option<PathBuf> {
Some(Self::proj_dir()?.data_dir().join(CONFIG_FILE))
}
pub fn history_cache_file() -> Option<PathBuf> {
Some(Self::proj_dir()?.data_dir().join(HISTORY_CACHE_FILE))
}
pub fn save_config(&self) {
let content = match serde_json::to_string_pretty(&self) {
Ok(content) => content,
@ -103,36 +84,25 @@ impl Storage {
}
pub fn load_plan_at_path(path: PathBuf, save_local: bool) -> Option<Plan> {
let mut plan: Plan = match serde_json::from_str(&std::fs::read_to_string(&path).ok()?).ok() {
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())?,
};
if save_local {
match Self::save_plan_at_store_path(path.file_name()?.to_str()?, plan.clone(), false) {
Ok(path) => plan.set_stored_path(Some(path)),
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");
}
}
}
//QoL touch file to put recent plans at top! :D
match std::fs::File::open(&path) {
Ok(file) => {
file.set_modified(std::time::SystemTime::now()).ok();
}
Err(_) => (),
}
Some(plan)
}
pub fn save_plan_at_store_path(
file_name: &str,
mut plan: Plan,
allow_overwrite: bool,
) -> Result<PathBuf, Box<dyn Error>> {
pub fn save_plan_at_store_path(file_name: &str, mut plan: Plan) -> Result<PathBuf, Box<dyn Error>> {
let plan_dir = match Self::plan_dir() {
Some(dir) => dir,
None => return Err("No plan dir".into()),
@ -141,13 +111,12 @@ impl Storage {
let file_path = plan_dir.join(file_name).with_extension("json");
//Disallow overwriting.
if !allow_overwrite && file_path.exists() {
if file_path.exists() {
return Err("File already exists".into());
}
if allow_overwrite && file_path.exists() {
log::info!("Overwriting plan : {file_path:?}");
}
//TODO: Determine necessity
plan.set_stored_path(Some(file_path.clone()));
std::fs::write(&file_path, serde_json::to_string(&plan)?)?;
@ -160,21 +129,14 @@ impl Storage {
None => return vec![],
};
let mut read_dir: Vec<DirEntry> = match plan_dir.read_dir() {
Ok(read_dir) => read_dir.filter_map(|v| {
log::trace!("Read dir: {:?}", v);
v.ok()
}).collect(),
let read_dir = match plan_dir.read_dir() {
Ok(read_dir) => read_dir,
Err(_) => return vec![],
};
read_dir.sort_by_key(|v| v.metadata().ok()?.modified().ok());
read_dir.reverse();
read_dir
.iter()
.filter_map(|entry| {
let path = entry.path();
let path = entry.ok()?.path();
if path.extension()? != "json" {
return None;
@ -204,80 +166,9 @@ impl Storage {
}
fn load_metadata_at_path(path: PathBuf) -> Option<PlanMetadata> {
let mut plan: PlanMetadata = match serde_json::from_str(&std::fs::read_to_string(&path).ok()?) {
Ok(plan) => plan,
Err(e) => {
log::error!("Could not load metadata at path: {path:?} : {e}");
return None;
}
};
let mut plan: PlanMetadata =
serde_json::from_str(&std::fs::read_to_string(&path).ok()?).ok()?;
plan.set_stored_path(Some(path));
Some(plan)
}
pub fn save_history(history: RunHistory) {
Self::store_history_at_path(&history);
Self::store_history_at_cache(history);
}
pub fn load_history_at_uuid(uuid: String) -> Option<RunHistory> {
serde_json::from_str(&std::fs::read_to_string(Self::history_uuid_to_path(uuid)?).ok()?).ok()
}
pub fn load_cache() -> Option<HashMap<String, RunHistoryMetadata>> {
let path = match Self::history_cache_file() {
Some(path) => path,
None => { log::error!("Could not get path for history cache"); return None; },
};
serde_json::from_str(&std::fs::read_to_string(path).ok()?).ok()
}
fn history_uuid_to_path(uuid: String) -> Option<PathBuf> {
let history_dir = match Self::history_dir() {
Some(dir) => dir,
None => return None,
};
Some(history_dir.join(uuid.to_string()).with_extension("json"))
}
fn store_history_at_path(history: &RunHistory) {
let path = match Self::history_uuid_to_path(history.uuid()) {
Some(path) => path,
None => { log::error!("Could not get path for history"); return; },
};
let serialization_result = match serde_json::to_string(&history) {
Ok(serialization_result) => serialization_result,
Err(e) => { log::error!("Could not serialize history: {}", e); return;},
};
std::fs::write(
path,
serialization_result,
).ok();
}
fn store_history_at_cache(history: RunHistory) {
let mut cache = match Self::load_cache() {
Some(cache) => cache,
None => HashMap::new(),
};
cache.insert(history.uuid(), history.into());
let path = match Self::history_cache_file() {
Some(path) => path,
None => { log::error!("Could not get path for history cache"); return; },
};
let serialization = match serde_json::to_string(&cache) {
Ok(serialization) => serialization,
Err(e) => { log::error!("Could not serialize history cache: {}", e); return; },
};
std::fs::write(path, serialization).ok();
}
}

@ -1,52 +0,0 @@
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RunHistory {
entries: Vec<TrackEntry>,
#[serde(flatten)]
metadata: RunHistoryMetadata,
}
impl RunHistory {
pub fn uuid(&self) -> String {
self.metadata.uuid.clone()
}
}
impl From<RunHistory> for RunHistoryMetadata {
fn from(history: RunHistory) -> Self {
history.metadata
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum EntryType {
PlanForceNext,
PlanForcePrev,
ZoneEnter,
CheckpointReached
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TrackEntry {
#[serde(rename = "type")]
entry_type: EntryType,
zone: String,
current_elapsed_millis: u64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RunHistoryMetadata {
uuid: String,
current_elapsed_millis: u64,
associated_name: String,
#[serde(default = "default_last_updated_date_now")]
last_updated: u64,
}
fn default_last_updated_date_now() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64
}

@ -8,7 +8,7 @@
},
"package": {
"productName": "Nothing",
"version": "1.8.9"
"version": "1.4.0"
},
"tauri": {
"systemTray": {
@ -18,8 +18,7 @@
"allowlist": {
"dialog": {
"open": true,
"save": true,
"message": true
"save": true
},
"globalShortcut": {
"all": true

@ -7,18 +7,12 @@ export interface PlanInterface {
stored_path?: string;
update_url?: string;
name?: string;
latest_server_etag?: string;
identifier?: string,
last_stored_time?: string;
}
export interface PlanMetadata {
stored_path?: string;
update_url?: string;
name: string;
latest_server_etag?: string;
identifier?: string;
last_stored_time?: string;
}
export class Plan {
@ -26,31 +20,16 @@ export class Plan {
current: number;
update_url?: string;
name?: string;
latest_server_etag?: string;
identifier?: string;
last_stored_time?: string;
public path?: string;
private path?: string;
private selfSaveSubject: Subject<void> = new Subject<void>();
constructor(plan?: PlanInterface) {
if(!plan) {
this.plan = [];
this.current = 0;
return;
};
constructor(plan: PlanInterface) {
this.plan = plan.plan;
this.current = plan.current;
if (plan.stored_path) {
this.path = plan.stored_path;
}
this.update_url = plan.update_url;
this.name = plan.name;
this.latest_server_etag = plan.latest_server_etag;
this.identifier = plan.identifier;
this.last_stored_time = plan.last_stored_time;
this.selfSaveSubject.pipe(debounceTime(500)).subscribe(() => this.directSelfSave());
}
@ -58,10 +37,6 @@ export class Plan {
this.path = path;
}
isNext(zoneId: string, current = this.current) {
return current + 1 < this.plan.length && zoneId === this.plan[current + 1].area_key;
}
next() {
if (this.current + 1 < this.plan!.length) {
this.current++;
@ -82,20 +57,17 @@ export class Plan {
current: this.current,
stored_path: this.path,
update_url: this.update_url,
latest_server_etag: this.latest_server_etag,
identifier: this.identifier,
last_stored_time: this.last_stored_time,
};
}
public requestSelfSave() {
private requestSelfSave() {
if (this.path) {
this.selfSaveSubject.next();
}
}
private directSelfSave() {
invoke('save_plan_at_path', { path: this.path, plan: this.toInterface() });
invoke('save_plan_at_path', {path: this.path, plan: this.toInterface()});
}
}
@ -106,7 +78,4 @@ export interface PlanElement {
uuid?: string;
edited: boolean;
anchor_act?: number;
checkpoint?: boolean;
checkpoint_millis?: number;
checkpoint_your_millis?: number;
}

@ -32,18 +32,12 @@ export class OverlayService {
this.isOverlay = appWindow.label === "Overlay";
}
registerInitialBinds(retry: boolean = true) {
this.shortcuts.register(this.configService.config.toggleOverlay).subscribe({
next: (_shortcut) => {
this.onToggleOverlay()
},
error: (err) => {
console.error("error binding overlay toggle", err);
this.shortcuts.force_unbind(this.configService.config.toggleOverlay).subscribe(() => this.registerInitialBinds(false))
}
registerInitialBinds() {
this.shortcuts.register(this.configService.config.toggleOverlay).subscribe((_shortcut) => {
this.onToggleOverlay()
});
}
onOverlayStateChange(event: Event<StateEvent>) {
this.interactable = event.payload.Interactable != null;
if (event.payload.Hidden) { this.visible = false } else { this.visible = true };

@ -1,20 +1,10 @@
import { Injectable } from '@angular/core';
import { invoke } from '@tauri-apps/api';
import { EMPTY, Observable, ReplaySubject, Subject, from, map, switchMap, tap } from 'rxjs';
import { Observable, ReplaySubject, Subject, from, map, switchMap, tap } from 'rxjs';
import { Plan, PlanInterface, PlanMetadata } from '../_models/plan';
import { MatDialog } from '@angular/material/dialog';
import { UrlDialog } from '../plan-display/url-dialog.component';
import { fetch } from '@tauri-apps/api/http';
import { Response } from '@tauri-apps/api/http';
export class UrlError extends Error {
status: number;
constructor(status: number, message: string) {
super(message);
this.status = status;
}
}
@Injectable({
providedIn: 'root'
@ -23,11 +13,9 @@ export class PlanService {
private _currentPlanSubject: Subject<Plan> = new ReplaySubject<Plan>(1);
private _basePlanSubject: Subject<Plan> = new ReplaySubject<Plan>(1);
private _storedPlansSubject: Subject<PlanMetadata[]> = new ReplaySubject<PlanMetadata[]>(1);
constructor(private dialog: MatDialog) {
this.loadBasePlan();
this.loadStoredPlans();
}
public getBasePlan(): Observable<Plan> {
@ -42,19 +30,15 @@ export class PlanService {
this._currentPlanSubject.next(plan);
}
public getStoredPlans(): Observable<PlanMetadata[]> {
return this._storedPlansSubject.asObservable();
public enumerateStoredPlans(): Observable<PlanMetadata[]> {
return from(invoke<PlanMetadata[]>('enumerate_stored_plans'));
}
public loadPlanFromPath(path: string, save_local: boolean = true): Observable<Plan> {
return from(invoke<PlanInterface>('load_plan_at_path', { path, saveLocal: save_local })).pipe(map(plan => {
return new Plan(plan)
})).pipe(tap(() => {
this.loadStoredPlans();
}));
return from(invoke<PlanInterface>('load_plan_at_path', { path, saveLocal: save_local })).pipe(map(plan => { return new Plan(plan) }));
}
public loadFromUrl(url?: string, name?: string, save_local: boolean = false): Observable<Plan> {
public loadFromUrl(url?: string, name?: string): Observable<Plan> {
if (!url || !name) {
const dialogRef = this.dialog.open(UrlDialog, {
data: {
@ -65,14 +49,14 @@ export class PlanService {
return dialogRef.afterClosed().pipe(switchMap(data => {
if (data.url) {
return this._loadFromUrl(data.url, data.name, save_local);
return this._loadFromUrl(data.url, data.name);
}
return new Observable<Plan>((s) => s.complete());
}));
} else {
return this._loadFromUrl(url, name, save_local);
return this._loadFromUrl(url, name);
}
}
@ -84,32 +68,11 @@ export class PlanService {
return from(invoke('save_plan_at_path', { path, plan: plan.toInterface() }));
}
public checkForPlanUpdate(plan: Plan | PlanMetadata): Observable<Plan> {
if (!plan.update_url) return EMPTY;
return from(fetch(
plan.update_url,
{
method: 'HEAD',
timeout: 10
})).pipe(switchMap(response => {
this.validateResponse(response);
if (response.headers['etag'] === plan.latest_server_etag) {
return EMPTY;
}
return this._loadFromUrl(plan.update_url!, plan.name!, false);
}));
public savePlanAtStore(name: string, plan: Plan) {
return from(invoke<string>('save_plan_at_store', { name, plan: plan.toInterface() }));
}
public savePlanAtStore(name: string, plan: Plan, allowOverwrite: boolean = false) {
return from(invoke<string>('save_plan_at_store', { name, plan: plan.toInterface(), allowOverwrite })).pipe(tap(() => {
this.loadStoredPlans();
}));
}
private _loadFromUrl(url: string, name: string, save_local: boolean): Observable<Plan> {
private _loadFromUrl(url: string, name: string): Observable<Plan> {
//Tauri fetch
return from(fetch(
url,
@ -117,46 +80,17 @@ export class PlanService {
method: 'GET',
timeout: 10
})).pipe(map(response => {
this.validateResponse(response);
const plan = new Plan(response.data as PlanInterface);
const etag = response.headers['etag'];
if (etag) {
plan.latest_server_etag = etag;
console.log("got etag: ", etag);
}
return plan;
return new Plan(response.data as PlanInterface);
})).pipe(tap(plan => {
plan.update_url = url;
plan.name = name;
if (save_local) {
this.savePlanAtStore(name, plan).subscribe();
}
this.loadStoredPlans();
}));
}
private validateResponse(response: Response<unknown>) {
if (!response.ok) {
throw new UrlError(response.status, "Error fetching plan from URL, status " + response.status + ", URL: " + response.url + " with body:" + response.data);
}
}
private loadBasePlan() {
from(invoke<PlanInterface>('base_plan')).subscribe(plan => {
plan.plan.forEach(elem => { elem.edited = false; });
plan.current = 0;
plan.name = "Base Plan";
this._basePlanSubject?.next(new Plan(plan));
});
}
private loadStoredPlans() {
from(invoke<PlanMetadata[]>('enumerate_stored_plans')).subscribe(plans => {
this._storedPlansSubject.next(plans);
})
}
}

@ -1,111 +0,0 @@
import { Injectable } from '@angular/core';
import { EntryType, RunHistory, TrackEntry } from './time-tracker.service';
import { Plan } from '../_models/plan';
export interface RunStat {
zoneName: string;
entryTime: string;
estimatedExit: string;
estimatedTimeSpent: string;
}
export interface AggregateRunStat {
zoneName: string;
aggregateFirstEntry: string;
aggregateLastExit: string;
aggregateTimeSpent: string;
aggregateNumEntries: string;
}
export interface UnformattedAggregateRunStat {
zoneId: string;
aggregateFirstEntry: number;
aggregateLastExit: number;
aggregateTimeSpent: number;
aggregateNumEntries: number;
}
export interface UnformattedAggregationData {
aggregation: UnformattedAggregateRunStat[];
aggregateNAId: string;
}
export interface UnformattedRunStat {
zoneId: string;
entryTime: number;
estimatedExit?: number;
estimatedTimeSpent?: number;
entryType: EntryType;
}
export type RunStatType = RunStat | AggregateRunStat;
@Injectable({
providedIn: 'root'
})
export class RunStatService {
constructor() {
}
calcAggregated(data: RunHistory): UnformattedAggregationData {
const aggregation = new Map<string, UnformattedAggregateRunStat>();
data.entries.forEach((entry, index) => {
const hasExit = !(data.entries.length - 1 === index);
let aggregate: UnformattedAggregateRunStat = {
zoneId: entry.zone,
aggregateFirstEntry: entry.current_elapsed_millis,
aggregateLastExit: hasExit ? data.entries[index + 1].current_elapsed_millis : 0,
aggregateTimeSpent: hasExit ? (data.entries[index + 1].current_elapsed_millis - data.entries[index].current_elapsed_millis) : 0,
aggregateNumEntries: 1,
}
const existing = aggregation.get(entry.zone);
if (existing) {
existing.aggregateLastExit = aggregate.aggregateLastExit;
existing.aggregateTimeSpent += aggregate.aggregateTimeSpent;
existing.aggregateNumEntries++;
}
aggregation.set(entry.zone, existing ?? aggregate);
});
return {
aggregation: Array.from(aggregation.values()),
aggregateNAId: data.entries.length > 0 ? data.entries[data.entries.length - 1].zone : "",
};
}
calcDirect(data: RunHistory): UnformattedRunStat[] {
return data.entries.map((entry, index) => {
const hasExit = !(data.entries.length - 1 === index);
return {
zoneId: entry.zone,
entryTime: entry.current_elapsed_millis,
estimatedExit: hasExit ? data.entries[index + 1].current_elapsed_millis : undefined,
estimatedTimeSpent: hasExit ? (data.entries[index + 1].current_elapsed_millis - data.entries[index].current_elapsed_millis) : undefined,
entryType: entry.type,
}
})
}
insertTimesAtCheckpoints(history: RunHistory, plan: Plan) {
const data = this.calcDirect(history);
const checkPointEntries = new Map(data.filter(entry => entry.entryType === EntryType.CheckpointReached).map(entry => [entry.zoneId, entry.entryTime]));
plan.plan.forEach(elem => {
if (checkPointEntries.has(elem.uuid!)) {
elem.checkpoint_millis = checkPointEntries.get(elem.uuid!);
}
});
}
}

@ -1,6 +1,6 @@
import { Injectable, NgZone } from '@angular/core';
import { ShortcutHandler, register, unregister } from '@tauri-apps/api/globalShortcut';
import { Observable, Subscriber, from } from 'rxjs';
import { Observable, Subscriber } from 'rxjs';
@Injectable({
providedIn: 'root'
@ -8,7 +8,7 @@ import { Observable, Subscriber, from } from 'rxjs';
export class ShortcutService {
private internalHandlers: Map<string, [ShortcutHandler, Subscriber<string>, () => void]> = new Map<string, [ShortcutHandler, Subscriber<string>, () => void]>();
constructor(private zone: NgZone) { }
constructor(private zone: NgZone) {}
register(shortcut: string) {
return new Observable<string>((subscriber) => {
@ -21,12 +21,12 @@ export class ShortcutService {
};
this.internalHandlers.set(shortcut, [originalHandler, subscriber, teardown]);
register(shortcut, originalHandler).catch(e => subscriber.error(e));
register(shortcut, originalHandler)
return teardown;
});
}
rebind_from_to(previousShortcut: string, nextShortcut: string) {
let [oldHandler, subscriber, teardown] = this.internalHandlers.get(previousShortcut)!;
@ -43,10 +43,4 @@ export class ShortcutService {
this.internalHandlers.set(nextShortcut, [oldHandler, subscriber, teardown]);
}
///No safety checks
force_unbind(shortcut: string) {
return from(unregister(shortcut));
}
}

@ -1,382 +0,0 @@
import { Injectable, NgZone } from '@angular/core';
import { ConfigService } from './config.service';
import { Observable, ReplaySubject, Subject, Subscribable, Subscription, from, map, tap, timer } from 'rxjs';
import { Plan } from '../_models/plan';
import { MatDialog } from '@angular/material/dialog';
import { Resume, ResumeDialog } from '../plan-display/resume-dialog.component';
import { v4 as uuidv4 } from 'uuid';
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
export enum EntryType {
PlanForceNext = "PlanForceNext",
PlanForcePrev = "PlanForcePrev",
ZoneEnter = "ZoneEnter",
CheckpointReached = "CheckpointReached",
}
export interface TrackEntry {
type: EntryType;
zone: string;
current_elapsed_millis: number;
}
export interface RunHistoryMetadata {
uuid: string;
currentElapsedMillis: number;
associatedName: string;
last_updated: number;
}
interface RunHistoryInterface {
uuid: string;
current_elapsed_millis: number;
associated_name: string;
last_updated: number;
entries: TrackEntry[];
}
export class RunHistory {
uuid: string;
currentElapsedMillis: number;
entries: TrackEntry[];
associatedName: string;
last_updated: number;
plan?: Plan;
constructor(data: RunHistoryInterface) {
this.uuid = data.uuid;
this.currentElapsedMillis = data.current_elapsed_millis;
this.entries = data.entries;
this.last_updated = data.last_updated;
this.associatedName = data.associated_name;
}
toInterface(): RunHistoryInterface {
return {
uuid: this.uuid,
current_elapsed_millis: this.currentElapsedMillis,
last_updated: this.last_updated,
entries: this.entries,
associated_name: this.associatedName,
}
}
}
@Injectable({
providedIn: 'root'
})
export class TimeTrackerService {
private currentRunHistory?: RunHistory;
private timerSubscription?: Subscription;
private debouncedSaveStopwatch?: Subscription;
resumeOnNext: boolean = false;
private start?: Date;
private latest?: Date;
private active: boolean = false;
private storedHistoriesSubject: Subject<Map<string, RunHistoryMetadata>> = new ReplaySubject<Map<string, RunHistoryMetadata>>(1);
private pushSubject: Subject<TrackEntry> = new ReplaySubject<TrackEntry>(1);
private newCurrentRunHistorySubject: Subject<RunHistory> = new ReplaySubject<RunHistory>(1);
constructor(private configService: ConfigService, public dialog: MatDialog, private zone: NgZone) {
this.loadCache();
appWindow.listen("entered", (entered) => {
if (entered.payload && typeof entered.payload === 'string')
this.onZoneEnter(entered.payload);
});
this.pushSubject.subscribe((entry) => {
this.currentRunHistory?.entries.push(entry);
});
}
public getCurrentRunHistory(): Observable<RunHistory> {
return this.newCurrentRunHistorySubject.asObservable();
}
getLatestEntry() {
return this.pushSubject.asObservable();
}
private setCurrentRunHistory(history: RunHistory) {
this.currentRunHistory = history;
this.newCurrentRunHistorySubject.next(history);
}
get elapsedTimeMillis() {
this.latest = new Date();
return this.latest!.valueOf() - this.start!.valueOf();
}
get isActive() {
return this.active;
}
get hasRunLoaded() {
return !!this.currentRunHistory;
}
public get storedHistories(): Observable<Map<string, RunHistoryMetadata>> {
return this.storedHistoriesSubject;
}
onNewRun(plan: Plan) {
if (this.timerSubscription && !this.timerSubscription.closed) this.timerSubscription.unsubscribe();
if (this.debouncedSaveStopwatch && !this.debouncedSaveStopwatch.closed) this.debouncedSaveStopwatch.unsubscribe();
this.start = undefined;
this.latest = undefined;
this.active = false;
if (plan.last_stored_time) {
this.loadHistory(plan.last_stored_time).subscribe(history => {
if (history) {
this.setCurrentRunHistory(history);
} else {
//Legacy or missing history, attempt to preserve elapsed time
this.setCurrentRunHistory(this.createNew(plan.name));
plan.last_stored_time = this.currentRunHistory!.uuid;
const old_time = parseInt(plan.last_stored_time, 10);
if (!isNaN(old_time) && old_time > 0) {
this.currentRunHistory!.currentElapsedMillis = old_time;
}
plan.requestSelfSave();
}
this.currentRunHistory!.plan = plan;
this.askResume(plan);
});
} else {
this.setCurrentRunHistory(this.createNew(plan.name));
this.currentRunHistory!.plan = plan;
this.resumeOnNext = true;
}
}
///Assumes currentPlan is set...
private startStopwatch() {
this.stop(); // Make sure we stop before starting again
if (this.currentRunHistory?.currentElapsedMillis) {
this.start = new Date(Date.now() - this.currentRunHistory.currentElapsedMillis);
} else {
this.start = new Date();
}
this.latest = new Date();
this.active = true;
//Make sure this is always cleared if e.g. force started! should be fine but just in case!!
this.resumeOnNext = false;
this.timerSubscription = timer(0, 1000).subscribe(() => {
this.zone.run(() => {
this.latest = new Date();
this.currentRunHistory!.currentElapsedMillis = this.elapsedTimeMillis;
});
});
this.debouncedSaveStopwatch = timer(0, 5000).subscribe(() => {
this.underlyingSaveStopwatch();
})
}
private underlyingSaveStopwatch() {
if (this.currentRunHistory && this.active && this.start && this.latest) {
this.currentRunHistory!.currentElapsedMillis = this.elapsedTimeMillis;
this.currentRunHistory!.last_updated = Date.now();
this.saveHistory(this.currentRunHistory!).subscribe(() => { });
this.loadCache();
if (this.currentRunHistory.plan) {
this.currentRunHistory.plan.last_stored_time = this.currentRunHistory.uuid;
this.currentRunHistory.plan.requestSelfSave();
}
}
}
public saveHistory(currentRunHistory: RunHistory) {
return from(invoke('save_history', { currentRunHistory: currentRunHistory.toInterface() })).pipe(tap(() => this.loadCache()));
}
public loadHistory(uuid: string) {
return from(invoke<RunHistoryInterface>('load_history_at_uuid', { uuid: uuid })).pipe(map(history => {
if (history) {
return new RunHistory(history);
}
return undefined;
}));
}
public onForceNext(forced_area: string) {
if (this.configService.config.enableStopwatch) {
if (this.isActive) {
this.pushSubject.next({
type: EntryType.PlanForceNext,
zone: forced_area,
current_elapsed_millis: this.elapsedTimeMillis
});
}
}
}
public onForcePrev(forced_area: string) {
if (this.configService.config.enableStopwatch) {
if (this.isActive) {
this.pushSubject.next({
type: EntryType.PlanForcePrev,
zone: forced_area,
current_elapsed_millis: this.elapsedTimeMillis
});
}
}
}
//Not perfect but good enough..
public reportCheckpoint(checkpoint: string) {
this.pushSubject.next({
type: EntryType.CheckpointReached,
zone: checkpoint,
current_elapsed_millis: this.elapsedTimeMillis
})
}
public stop() {
if (this.timerSubscription && !this.timerSubscription.closed) this.timerSubscription.unsubscribe();
if (this.debouncedSaveStopwatch && !this.debouncedSaveStopwatch.closed) this.debouncedSaveStopwatch.unsubscribe();
//Do a nice little save here as well!
this.underlyingSaveStopwatch();
this.start = undefined;
this.latest = undefined;
this.active = false;
}
public startLoaded() {
if (this.currentRunHistory) {
this.startStopwatch();
}
}
public hmsTimestamp(elapsed?: number): string {
return hmsTimestamp(elapsed);
}
public loadCache() {
from(invoke<Map<string, RunHistoryMetadata>>('load_cache')).subscribe(data => {
this.zone.run(() => {
const cache = new Map<string, RunHistoryMetadata>();
Object.values(data).forEach((value) => {
cache.set(value.uuid, {
uuid: value.uuid,
currentElapsedMillis: value.current_elapsed_millis,
associatedName: value.associated_name,
last_updated: value.last_updated,
});
});
console.log("sending new cache!");
this.storedHistoriesSubject.next(cache);
});
});
}
private onZoneEnter(zone: string) {
if (!this.currentRunHistory) return;
if (this.configService.config.enableStopwatch) {
if(!this.isActive && this.resumeOnNext) {
this.resumeOnNext = false;
this.startStopwatch();
}
if(!this.isActive && !this.resumeOnNext) {
//Don't start timer if not meant to auto resumes on next.
return;
}
this.pushSubject.next({
type: EntryType.ZoneEnter,
zone: zone,
current_elapsed_millis: this.elapsedTimeMillis
});
}
}
private askResume(plan: Plan) {
const dialogRef = this.dialog.open(ResumeDialog, { disableClose: true });
dialogRef.afterClosed().subscribe(resume => {
switch (resume) {
case Resume.Instant:
this.startStopwatch();
this.loadReachedCheckpoints();
break;
case Resume.Next:
this.resumeOnNext = true;
this.loadReachedCheckpoints();
break;
case Resume.Discard:
this.setCurrentRunHistory(this.createNew(plan.name));
this.loadReachedCheckpoints();
plan.last_stored_time = this.currentRunHistory!.uuid;
this.resumeOnNext = true;
plan.requestSelfSave();
break;
}
})
}
loadReachedCheckpoints() {
if (!this.currentRunHistory || !this.currentRunHistory.plan || !this.configService.config.runCompareHistory) return;
this.loadHistory(this.configService.config.runCompareHistory).subscribe(history => {
if (!history) return;
const checkpoints = new Map(history.entries.filter(entry => entry.type === EntryType.CheckpointReached).map(entry => [entry.zone, entry.current_elapsed_millis]));
const ourCheckpointValidZones = new Map(this.currentRunHistory?.plan?.plan.filter(entry => checkpoints.has(entry.uuid!)).map(entry => [entry.uuid!, entry]));
this.currentRunHistory?.entries.filter(entry => entry.type === EntryType.CheckpointReached).forEach(entry => {
if (ourCheckpointValidZones.has(entry.zone)) {
ourCheckpointValidZones.get(entry.zone)!.checkpoint_your_millis = entry.current_elapsed_millis;
}
})
});
}
private createNew(associatedName?: string) {
const uuid = uuidv4();
return new RunHistory({
uuid,
associated_name: associatedName || 'Unnamed',
current_elapsed_millis: 0,
last_updated: Date.now(),
entries: [],
});
}
}
export function hmsTimestamp(elapsed?: number): string {
if (elapsed == null || elapsed == undefined) return "N/A";
const h = String(Math.floor(elapsed / 3600000)).padStart(2, '0');
const m = String(Math.floor((elapsed % 3600000) / 60000)).padStart(2, '0');
const s = String(Math.floor((elapsed % 60000) / 1000)).padStart(2, '0');
return `${h}:${m}:${s}`;
}

@ -13,7 +13,6 @@ export class WorldAreaService {
public matcher?: Fuzzr;
private worldAreasSubject = new ReplaySubject<Map<string, WorldArea>>();
private fullWorldAreasSubject = new ReplaySubject<Map<string, WorldArea>>();
constructor(private zone: NgZone) {
from(invoke<Map<string, WorldArea>>('load_world_areas')).subscribe((data) => {
@ -24,12 +23,6 @@ export class WorldAreaService {
toString: (e: [string, WorldArea]) => e[1].name
})
});
from(invoke<Map<string, WorldArea>>('load_full_world_areas')).subscribe((data) => {
const entries = Object.entries(data).sort((a, b) => a[1].key_id - b[1].key_id);
this.worldAreas = new Map(entries);
this.zone.run(() => this.fullWorldAreasSubject.next(this.worldAreas!));
});
}
getWorldAreas(): Observable<Map<string, WorldArea>> {
@ -38,12 +31,6 @@ export class WorldAreaService {
);
}
getFullWorldAreas(): Observable<Map<string, WorldArea>> {
return this.fullWorldAreasSubject.asObservable().pipe(
filter(worldAreas => !!worldAreas),
);
}
hasTrial(key: string): boolean {
switch (key) {
case "1_1_7_1": return true;

@ -1,51 +0,0 @@
<div *ngIf="latestEntry && timeTrackerService.isActive" class="d-flex flex-column">
<div class="zone"><b>{{resolveZone(latestEntry.zone)}}</b></div>
<div class="d-flex flex-row">
Spent:
<app-wrap-value [value]="{current: currentSpent(), compare: compareSpent()}">
<ng-template let-v>
<span class="mx-2" [class]="classCurrentLowBetter(v.current, v.compare)">{{v.current | hms}}</span>
<span>{{v.compare | hms}}</span>
</ng-template>
</app-wrap-value>
</div>
<div class="d-flex flex-row">
First Entry:
<app-wrap-value [value]="{current: currentFirst(), compare: compareFirst()}">
<ng-template let-v>
<span class="mx-2" [class]="classCurrentLowBetter(v.current, v.compare)">{{v.current | hms}}</span>
<span>{{v.compare | hms}}</span>
</ng-template>
</app-wrap-value>
</div>
<div class="d-flex flex-row">
Last Entry:
<app-wrap-value [value]="{current: currentLast(), compare: compareLast()}">
<ng-template let-v>
<span class="mx-2" [class]="classCurrentLowBetter(v.current, v.compare)">{{v.current | hms}}</span>
<span>{{v.compare | hms}}</span>
</ng-template>
</app-wrap-value>
</div>
<div class="d-flex flex-row">
Number of Entries:
<app-wrap-value [value]="{current: currentEntries(), compare: compareEntries()}">
<ng-template let-v>
<span class="mx-2" [class]="classCurrentLowBetter(v.current, v.compare)">{{v.current}}</span>
<span>{{v.compare}}</span>
</ng-template>
</app-wrap-value>
</div>
</div>
<ng-container *ngIf="!latestEntry">
Awaiting zone entry to compare data
</ng-container>
<ng-container *ngIf="latestEntry && !timeTrackerService.isActive">
Awaiting stopwatch to start
</ng-container>

@ -1,9 +0,0 @@
.indeterminate {
color: orange;
}
.good-diff {
color: rgb(46, 179, 46);
}
.bad-diff {
color: rgb(199, 13, 13);
}

@ -1,21 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AggregateDisplayComponent } from './aggregate-display.component';
describe('AggregateDisplayComponent', () => {
let component: AggregateDisplayComponent;
let fixture: ComponentFixture<AggregateDisplayComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AggregateDisplayComponent]
});
fixture = TestBed.createComponent(AggregateDisplayComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -1,155 +0,0 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PlanService } from '../_services/plan.service';
import { EntryType, RunHistory, TimeTrackerService, TrackEntry } from '../_services/time-tracker.service';
import { Plan } from '../_models/plan';
import { ConfigService } from '../_services/config.service';
import { RunStatService, UnformattedAggregateRunStat, UnformattedAggregationData } from '../_services/run-stat.service';
import { WorldArea } from '../_models/world-area';
import { WorldAreaService } from '../_services/world-area.service';
import { HmsPipe } from "../hms/hms.pipe";
import { WrapValueComponent } from '../wrap-value/wrap-value.component';
import { timer } from 'rxjs';
@Component({
selector: 'app-aggregate-display',
standalone: true,
templateUrl: './aggregate-display.component.html',
styleUrls: ['./aggregate-display.component.scss'],
imports: [CommonModule, HmsPipe, WrapValueComponent]
})
export class AggregateDisplayComponent {
latestEntry?: TrackEntry;
private worldAreaMap?: Map<string, WorldArea>;
private currentRunHistory?: RunHistory;
private compareAggregate?: Map<string, UnformattedAggregateRunStat>;
private currentAggregate?: Map<string, UnformattedAggregateRunStat>;
constructor(
public timeTrackerService: TimeTrackerService,
private configService: ConfigService,
private runStatService: RunStatService,
private worldAreaService: WorldAreaService,
) {
this.timeTrackerService.getCurrentRunHistory().subscribe(history => {
this.onNewCurrent(history);
});
this.worldAreaService.getFullWorldAreas().subscribe((data) => {
this.worldAreaMap = data;
})
this.timeTrackerService.getLatestEntry().subscribe(entry => {
if (entry.type != EntryType.ZoneEnter) return;
if (this.latestEntry) {
this.expandCurrentAggregated(this.latestEntry, entry);
}
this.latestEntry = entry;
});
}
private onNewCurrent(history: RunHistory) {
this.currentRunHistory = history;
this.loadComparisonData();
const aggregatedRunHistory = this.runStatService.calcAggregated(history);
this.currentAggregate = new Map(aggregatedRunHistory.aggregation.map(agg => [agg.zoneId, agg]));
if (history.entries.length > 0) {
this.latestEntry = history.entries[history.entries.length - 1];
} else {
this.latestEntry = undefined;
}
}
private loadComparisonData() {
if (!this.configService.config.runCompareHistory) {
return;
}
this.timeTrackerService.loadHistory(this.configService.config.runCompareHistory).subscribe(history => {
if (history) {
this.compareAggregate = new Map<string, UnformattedAggregateRunStat>(
this.runStatService.calcAggregated(history).aggregation.map(agg => [agg.zoneId, agg])
);
}
});
}
private expandCurrentAggregated(oldEntry: TrackEntry, newEntry: TrackEntry) {
if (!this.currentAggregate) return;
//Expand with old entry
let aggregate: UnformattedAggregateRunStat = {
zoneId: oldEntry.zone,
aggregateFirstEntry: oldEntry.current_elapsed_millis,
aggregateLastExit: newEntry.current_elapsed_millis,
aggregateTimeSpent: (newEntry.current_elapsed_millis - oldEntry.current_elapsed_millis),
aggregateNumEntries: 1,
}
const existing = this.currentAggregate.get(oldEntry.zone);
if (existing) {
existing.aggregateLastExit = aggregate.aggregateLastExit;
existing.aggregateTimeSpent += aggregate.aggregateTimeSpent;
existing.aggregateNumEntries++;
}
this.currentAggregate.set(aggregate.zoneId, existing ?? aggregate);
}
hms(value?: number) {
if (value) {
return this.timeTrackerService.hmsTimestamp(value);
} else {
return "N/A";
}
}
resolveZone(zoneId: string) {
const area = this.worldAreaMap?.get(zoneId);
if (!area) {
return "Unknown zone: " + zoneId;
}
// Act might not be very reasonable but it's the best we have it..
return area.name + " (A" + area.act + ")"
}
classCurrentLowBetter(current?: number, compare?: number) {
if (compare == undefined || compare == null || current == undefined || current == null) return "indeterminate";
return current <= compare ? "good-diff" : "bad-diff";
}
currentSpent() {
return (this.currentAggregate?.get(this.latestEntry!.zone)?.aggregateTimeSpent ?? 0) + (this.currentRunHistory!.currentElapsedMillis - this.latestEntry!.current_elapsed_millis);
}
compareSpent() {
return this.compareAggregate?.get(this.latestEntry!.zone)?.aggregateTimeSpent;
}
compareEntries() {
return this.compareAggregate?.get(this.latestEntry!.zone)?.aggregateNumEntries ?? 0;
}
currentEntries() {
return (this.currentAggregate?.get(this.latestEntry!.zone)?.aggregateNumEntries ?? 0) + 1;
}
compareLast() {
return this.compareAggregate?.get(this.latestEntry!.zone)?.aggregateLastExit;
}
currentLast() {
return this.latestEntry?.current_elapsed_millis ?? 0;
}
compareFirst() {
return (this.compareAggregate?.get(this.latestEntry!.zone)?.aggregateFirstEntry) ?? 0;
}
currentFirst() {
return (this.currentAggregate?.get(this.latestEntry!.zone)?.aggregateFirstEntry) ?? this.latestEntry!.current_elapsed_millis;
}
}

@ -11,9 +11,6 @@
<mat-tab label="Editor">
<plan-editor class="content"></plan-editor>
</mat-tab>
<mat-tab label="Previous runs" *ngIf="configService.config.enableStopwatch">
<app-run-stats class="content"></app-run-stats>
</mat-tab>
</mat-tab-group>
<tooltip class="tooltip">

@ -22,7 +22,7 @@ export class AppComponent implements OnInit {
isBinding: boolean = false;
overlayShowSettings: boolean = false;
hasAttachedOnce: boolean = false;
selected = new FormControl(0);
constructor(
@ -33,24 +33,15 @@ export class AppComponent implements OnInit {
private events: EventsService,
) {
this.events.listen<String>("loadTab").subscribe(event => {
switch (event.payload) {
case "settings":
this.selected.setValue(0);
break;
case "editor":
this.selected.setValue(1);
break;
case "runstats":
this.selected.setValue(2);
break;
default:
this.selected.setValue(0);
break;
if (event.payload === "editor") {
this.selected.setValue(1);
} else {
this.selected.setValue(0);
}
})
}
ngOnInit(): void {
}
}

@ -16,9 +16,6 @@ import { MatTabsModule } from '@angular/material/tabs';
import { MAT_DIALOG_DEFAULT_OPTIONS } from "@angular/material/dialog";
import { TooltipComponent } from "./tooltip/tooltip.component";
import { HttpClientModule } from "@angular/common/http";
import { AngularSvgIconModule } from "angular-svg-icon";
import { RunStatsComponent } from "./run-stats/run-stats.component";
import { WrapValueComponent } from './wrap-value/wrap-value.component';
// import { GemFinderComponent } from "./gem-finder/gem-finder.component";
export function initializeApp(configService: ConfigService) {
@ -29,7 +26,7 @@ export function initializeApp(configService: ConfigService) {
@NgModule({
declarations: [
AppComponent
AppComponent,
],
imports: [
BrowserModule,
@ -43,9 +40,7 @@ export function initializeApp(configService: ConfigService) {
SettingsComponent,
MatTabsModule,
TooltipComponent,
HttpClientModule,
AngularSvgIconModule.forRoot(),
RunStatsComponent
HttpClientModule
],
providers: [
{

@ -56,7 +56,7 @@ export class CarouselComponent<T> implements OnInit, AfterViewInit, OnChanges {
@Input() offset: number = 0;
containerDirectionLength: number = 0;
private debouncedOnchange: Subject<void> = new Subject<void>();
constructor(private cdr: ChangeDetectorRef) {
this.visibleSlides = [];
this.debouncedOnchange.pipe(debounceTime(500)).subscribe(() => this.realOnChange());
@ -65,26 +65,27 @@ export class CarouselComponent<T> implements OnInit, AfterViewInit, OnChanges {
}
}
ngOnInit(): void {
this.afterInitSelf.next(this);
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
let changed = false;
entries.forEach(entry => {
const entryIndex = parseInt(entry.target.getAttribute('data-slideIndex')!);
if (!entryIndex && entryIndex != 0) {
return;
}
const entryIntersectingSlide = this.visibleSlides?.find(s => s.index == entryIndex);
if (!entryIntersectingSlide) {
return;
}
entryIntersectingSlide.currentlyIntersecting = entry.isIntersecting;
});
if (changed) {
const runIntersectionHandling = () => {
const entryIndex = parseInt(entry.target.getAttribute('data-slideIndex')!);
if (!entryIndex && entryIndex != 0) {
return;
}
const entryIntersectingSlide = this.visibleSlides?.find(s => s.index == entryIndex);
if (!entryIntersectingSlide) {
return;
}
entryIntersectingSlide.currentlyIntersecting = entry.isIntersecting;
};
runIntersectionHandling();
this.onChange();
}
})
})
}
@ -97,7 +98,7 @@ export class CarouselComponent<T> implements OnInit, AfterViewInit, OnChanges {
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['numVisible'] || changes['offset']) {
if(changes['numVisible'] || changes['offset']) {
this.reinitializeVisibleSlides();
}
}
@ -137,10 +138,10 @@ export class CarouselComponent<T> implements OnInit, AfterViewInit, OnChanges {
const start = Math.max(0, this.current - this.numExtraPrev());
const end = Math.min(this.current + this.numExtraNext(), this.slides!.length - 1);
for (let i = start; i <= end; i++) {
this.visibleSlides?.push({
index: i,
currentlyIntersecting: false,
});
this.visibleSlides?.push({
index: i,
currentlyIntersecting: false,
});
}
this.onChange();

@ -1,15 +0,0 @@
<ng-container *ngIf="init">
<ng-container *ngIf="rect">
<div class="target" [style.transform]="transform()" [style.width]="rect.width + 'px'"
[style.height]="rect.height + 'px'"
[style.background-color]="backgroundColor ? backgroundColor : 'rgba(0, 0, 0, 0.1)'" #targetRef>
<ng-content></ng-content>
</div>
<ngx-moveable #moveable [target]="targetRef" [draggable]="configurable" [resizable]="configurable"
(drag)="onDrag($event)" (resize)="onResize($event)" (dragEnd)="onDragEnd($event)"
(resizeEnd)="onResizeEnd($event)" [bounds]="bounds" [snappable]="true"
[style.visibility]="configurable ? 'visible' : 'hidden'">
</ngx-moveable>
</ng-container>
</ng-container>

@ -1,9 +0,0 @@
:host {
overflow: visible;
}
.target {
position: absolute;
min-width: 50px;
min-height: 50px;
}

@ -1,21 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DraggableWindowComponent } from './draggable-window.component';
describe('DraggableWindowComponent', () => {
let component: DraggableWindowComponent;
let fixture: ComponentFixture<DraggableWindowComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [DraggableWindowComponent]
});
fixture = TestBed.createComponent(DraggableWindowComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -1,88 +0,0 @@
import { AfterViewInit, ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnInit, Output, ViewChild } from '@angular/core';
import { Rect } from '../_models/generated/Rect';
import { NgxMoveableComponent, NgxMoveableModule, OnDrag, OnDragEnd, OnResize, OnResizeEnd } from 'ngx-moveable';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-draggable-window',
templateUrl: './draggable-window.component.html',
styleUrls: ['./draggable-window.component.scss'],
standalone: true,
imports: [CommonModule, NgxMoveableModule]
})
export class DraggableWindowComponent implements AfterViewInit {
rect?: Rect;
bounds: any = { "left": 0, "top": 0, "right": 0, "bottom": 0, "position": "css" };
@Input() public backgroundColor: string = "rgba(0, 0, 0, 0.1)";
@Input() public configurable: boolean = true;
@Input() public initialRect?: Rect;
@Output() savedRect: EventEmitter<Rect> = new EventEmitter();
@ViewChild("moveable") moveable?: NgxMoveableComponent;
init = false;
constructor(private cdr: ChangeDetectorRef, private zone: NgZone) {
window.addEventListener("resize", () => {
this.zone.run(() => {
this.windowInitHandler()
})
});
}
windowInitHandler() {
if (window.innerWidth > 0) {
const cfgRect = this.initialRect || { x: 0, y: 0, width: 0.1, height: 0.1 };
this.rect = {
x: cfgRect.x * window.innerWidth,
y: cfgRect.y * window.innerHeight,
width: cfgRect.width * window.innerWidth,
height: cfgRect.height * window.innerHeight,
}
this.moveable?.updateRect();
setTimeout(() => this.cdr.detectChanges(), 0);
this.init = true;
}
}
ngAfterViewInit(): void {
this.windowInitHandler();
}
transform() {
return `translate(${this.rect!.x}px, ${this.rect!.y}px)`;
}
onDrag(e: OnDrag) {
this.rect!.x = e.translate[0];
this.rect!.y = e.translate[1];
}
onDragEnd(_e: OnDragEnd) {
this.saveRect();
}
onResize(e: OnResize) {
this.rect!.width = e.width;
this.rect!.height = e.height;
this.onDrag(e.drag);
}
onResizeEnd(_e: OnResizeEnd) {
this.saveRect();
}
saveRect() {
if (this.rect) {
this.savedRect.emit({
x: this.rect.x / window.innerWidth,
y: this.rect.y / window.innerHeight,
width: this.rect.width / window.innerWidth,
height: this.rect.height / window.innerHeight,
});
}
}
}

@ -5,7 +5,7 @@
<button mat-raised-button color="primary" (click)="loadBasePlan()">Load base plan preset</button>
</div>
<div class="d-flex flex-column overflow-hidden p-4 grow">
<div class="d-flex flex-column overflow-hidden p-4">
<div class="row">
<div class="col-6">
<div class="row">
@ -28,27 +28,26 @@
<div class="row">
<mat-form-field>
<mat-label>Area filter</mat-label>
<input matInput type="text" [ngModel]="planSearchString" (ngModelChange)="planSearchStringChange($event)">
<input matInput type="text" [(ngModel)]="planSearchString">
</mat-form-field>
</div>
<div class="row">
<mat-form-field>
<mat-label>Act filter</mat-label>
<mat-select [value]="planFilterAct" (valueChange)="planFilterActChange($event)">
<mat-select [(value)]="planFilterAct">
<mat-option *ngFor="let item of acts" [value]="item">{{item.name}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="row">
<mat-slide-toggle class="col-5" color="accent" [(ngModel)]="autoScrollToEnd">
Auto scroll to latest
</mat-slide-toggle>
<mat-slide-toggle class="col-5" color="accent" [(ngModel)]="autoScrollToEnd">
Auto scroll to latest
</mat-slide-toggle>
<mat-slide-toggle class="col-4" color="accent" [ngModel]="reverseDisplay"
(ngModelChange)="reverseDisplayChange($event)">
Reverse display
</mat-slide-toggle>
<mat-slide-toggle class="col-4" color="accent" [(ngModel)]="reverseDisplay">
Reverse display
</mat-slide-toggle>
<div class="col-3 d-flex justify-content-end ">
<button class="" mat-stroked-button color="warn" (click)="clearPlan()">Clear</button>
</div>
@ -57,50 +56,37 @@
</div>
<div cdkDropListGroup class="row h-100 overflow-hidden">
<div cdkDropListGroup *ngIf="areas" class="row overflow-hidden">
<div class="col-6 d-flex flex-column h-100 grow">
<div class="col-6 d-flex flex-column h-100">
<h2>Campaign zones</h2>
<div cdkDropList [cdkDropListData]="filterAreas()" class="list h-100 areas" cdkDropListSortingDisabled
<div cdkDropList [cdkDropListData]="filterAreas()" class="list areas" cdkDropListSortingDisabled
(cdkDropListDropped)="dropHandler($event)">
<div class="box" *ngFor="let item of filterAreas(); index as boxindex" cdkDrag
(dblclick)="doubleClickArea(item)">
<div class="box" *ngFor="let item of filterAreas()" cdkDrag (dblclick)="doubleClickArea(item)">
<div class="zone-name">{{item.name}}</div>
<div class="act">Act {{item.act}}</div>
</div>
</div>
</div>
<div class="col-6 d-flex flex-column h-100 grow">
<div class="col-6 d-flex flex-column h-100">
<h2>Plan</h2>
<div appendOnly #planListElement cdkDropList [cdkDropListData]="(this.latestList | async)!" class="list h-100"
<div cdkDropList #planList [cdkDropListData]="filterPlanElements()" class="list"
(cdkDropListDropped)="dropHandler($event)" [cdkDropListDisabled]="disabledPlanDD"
[cdkDropListEnterPredicate]="canDrop" [cdkDropListSortPredicate]="sortPredicate.bind(this)">
<div class="box" *ngFor="let item of latestList | async; index as boxIndex" cdkDrag
[cdkDragDisabled]="(!!this.planFilterAct.value) && boxIndex == 0" (contextmenu)="addNote($event, item)">
[cdkDropListEnterPredicate]="canDrop"
[cdkDropListSortPredicate]="sortPredicate.bind(this)">
<div class="box" *ngFor="let item of filterPlanElements(); index as boxIndex" cdkDrag [cdkDragDisabled]="(!!this.planFilterAct.value) && boxIndex == 0" (contextmenu)="addNote($event, item)">
<div class="content">
<div class="content-left">
<div class="zone-name">{{areasMap?.get(item.area_key)?.name}}</div>
<div class="act">Act {{areasMap?.get(item.area_key)?.act}}
<mat-slide-toggle class="" color="accent" [(ngModel)]="item.checkpoint">
Checkpoint zone
</mat-slide-toggle>
</div>
</div>
<div class="zone-name">{{areasMap?.get(item.area_key)?.name}}</div>
<div class="act">Act {{areasMap?.get(item.area_key)?.act}}</div>
</div>
<div *ngIf="item.notes" class="notes">(Note)</div>
<div class="index">#{{planIndexOf(item)}}</div>
<div class="delete" (click)="remove(item)">+</div>
</div>
</div>
<div cdkDropList [cdkDropListData]="(this.latestList | async) !" class="list end"
<div cdkDropList [cdkDropListData]="planInEditing.plan" class="list end"
style="position: relative;display: flex; flex-direction: column; justify-content: center;"
(cdkDropListDropped)="dropEndHandler($event)">
<span style="position: absolute; ">Place at end of list</span>
@ -108,4 +94,7 @@
</div>
</div>
</div>
</div>

@ -30,11 +30,6 @@
width: 100%;
}
.grow {
flex-grow: 1;
}
.box {
position: relative;
border-bottom: solid 1px map.get(palette.$nothing-dark-map, 50);
@ -49,12 +44,9 @@
padding: 20px 20px 20px 5px;
width: 100%;
overflow-x: hidden;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
height: 80px;
}
.buttons {
@ -78,8 +70,8 @@
width: 15px;
height: 15px;
overflow: visible;
&:hover {
&:hover {
cursor: pointer;
}
}
@ -114,7 +106,6 @@
.list.cdk-drop-list-dragging .box:not(.cdk-drag-placeholder) {
transition: transform 125ms cubic-bezier(0, 0, 0.2, 1);
}
.right-settings {
gap: 8px;
}

@ -1,4 +1,4 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnChanges, OnInit, QueryList, SimpleChanges, ViewChild, ViewChildren } from '@angular/core';
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import {
CdkDrag,
@ -14,7 +14,7 @@ import { Plan, PlanElement } from '../_models/plan';
import { WorldAreaService } from '../_services/world-area.service';
import { FormsModule } from '@angular/forms';
import { Fuzzr } from '../fuzzr/fuzzr';
import { BehaviorSubject, first, from, skip } from 'rxjs';
import { from } from 'rxjs';
import { save } from '@tauri-apps/api/dialog';
import { PlanService } from '../_services/plan.service';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
@ -47,11 +47,12 @@ interface Act {
MatInputModule,
MatSelectModule,
MatButtonModule,
MatSlideToggleModule,
MatSlideToggleModule
],
providers: []
})
export class EditorComponent implements OnInit {
planInEditing: Plan;
areas?: WorldArea[];
planAreas: WorldArea[];
areasMap?: Map<String, WorldArea>;
@ -61,22 +62,22 @@ export class EditorComponent implements OnInit {
filterAct: Act;
planFilterAct: Act;
acts: Act[];
@ViewChild('planListElement') planListElement!: ElementRef;
@ViewChild('planList') planListElement!: ElementRef;
autoScrollToEnd: boolean;
reverseDisplay: boolean;
disabledPlanDD: boolean;
original: PlanElement[] = [];
latestList: BehaviorSubject<PlanElement[]> = new BehaviorSubject<PlanElement[]>([]);
constructor(public worldAreaService: WorldAreaService, private cdr: ChangeDetectorRef, private planService: PlanService, public dialog: MatDialog) {
this.planInEditing = new Plan({
plan: [],
current: 0
});
this.disabledPlanDD = false;
this.autoScrollToEnd = false;
this.latestList = new BehaviorSubject<any[]>([]);
this.planFuzzer = new Fuzzr(this.original, {
this.planFuzzer = new Fuzzr(this.planInEditing.plan, {
toString: (e: PlanElement) => {
return this.areasMap?.get(e.area_key)?.name;
}
@ -99,19 +100,6 @@ export class EditorComponent implements OnInit {
this.planFilterAct = this.acts[0];
}
planSearchStringChange(value: string) {
this.planSearchString = value;
this.filterPlanElements();
}
planFilterActChange(value: Act) {
this.planFilterAct = value;
this.filterPlanElements();
}
reverseDisplayChange(value: boolean) {
this.reverseDisplay = value;
this.filterPlanElements();
}
ngOnInit(): void {
this.worldAreaService.getWorldAreas().subscribe(worldAreas => {
this.areas = [...worldAreas.values()];
@ -125,11 +113,11 @@ export class EditorComponent implements OnInit {
dropHandler(event: CdkDragDrop<WorldArea[]> | CdkDragDrop<PlanElement[]>) {
if (event.previousContainer === event.container && !isWorldAreaEvent(event)) {
const realCurrent = this.original.indexOf(event.previousContainer.data[event.currentIndex]);
const realPrev = this.original.indexOf(event.previousContainer.data[event.previousIndex]);
moveItemInArray(this.original, realPrev, realCurrent);
} else {
if (this.areas && isWorldAreaEvent(event)) {
const realCurrent = this.planInEditing.plan.indexOf(event.previousContainer.data[event.currentIndex]);
const realPrev = this.planInEditing.plan.indexOf(event.previousContainer.data[event.previousIndex]);
moveItemInArray(this.planInEditing.plan, realPrev, realCurrent);
} else
if (this.planInEditing && this.areas && isWorldAreaEvent(event)) {
if (event.container.data.length > 0 && 'connections_world_areas_keys' in event.container.data[0]) {
return;
}
@ -139,31 +127,18 @@ export class EditorComponent implements OnInit {
if (bounds) {
index += bounds[0];
}
this.original.splice(index, 0, this.planItemFromArea(event.previousContainer.data[event.previousIndex]));
this.planInEditing.plan.splice(index, 0, this.planItemFromArea(event.previousContainer.data[event.previousIndex]));
}
}
this.latestList.pipe(skip(1)).pipe(first()).subscribe(() => {
this.scrollToEnd();
});
this.filterPlanElements();
}
dropEndHandler(event: CdkDragDrop<WorldArea[]> | CdkDragDrop<PlanElement[]> | null) {
if (event == null) return;
dropEndHandler(event: CdkDragDrop<WorldArea[]> | CdkDragDrop<PlanElement[]>) {
if (isWorldAreaEvent(event) && this.areas) {
this.original.splice(this.getEnd(), 0, this.planItemFromArea(event.previousContainer.data[event.previousIndex]));
this.planInEditing.plan.splice(this.getEnd(), 0, this.planItemFromArea(event.previousContainer.data[event.previousIndex]));
} else {
moveItemInArray(this.original, event.previousIndex, this.getEnd());
moveItemInArray(this.planInEditing.plan, event.previousIndex, this.getEnd());
}
this.latestList.pipe(skip(1)).pipe(first()).subscribe(() => {
this.scrollToEnd();
});
this.filterPlanElements();
this.scrollToEnd();
}
getEnd() {
@ -171,14 +146,12 @@ export class EditorComponent implements OnInit {
if (bounds) {
return bounds[1];
} else {
return this.original.length;
return this.planInEditing.plan.length;
}
}
remove(item: PlanElement) {
this.original.splice(this.planIndexOf(item), 1);
this.filterPlanElements();
this.planInEditing.plan.splice(this.planIndexOf(item), 1);
}
canDrop = () => {
@ -208,36 +181,28 @@ export class EditorComponent implements OnInit {
if (!this.autoScrollToEnd) {
return;
}
this.cdr.detectChanges();
if (!this.reverseDisplay) {
this.planListElement.nativeElement.scrollTop = this.planListElement.nativeElement.scrollHeight;
} else {
this.planListElement.nativeElement.scrollTop = 0;
}
}
doubleClickArea(item: WorldArea) {
this.original.splice(this.original.length, 0, this.planItemFromArea(item));
this.latestList.pipe(skip(1)).pipe(first()).subscribe((_) => {
this.scrollToEnd();
});
this.filterPlanElements();
this.planInEditing.plan.splice(this.planInEditing.plan.length, 0, this.planItemFromArea(item));
this.scrollToEnd();
}
planElemFilterBounds() {
if (this.planFilterAct.value !== 0) {
let bounds = this.original.filter(item => item.anchor_act === this.planFilterAct.value || this.planFilterAct.value + 1 === item.anchor_act).map((value) => this.planIndexOf(value));
let bounds = this.planInEditing.plan.filter(item => item.anchor_act === this.planFilterAct.value || this.planFilterAct.value + 1 === item.anchor_act).map((value) => this.planIndexOf(value));
if (bounds.length == 2) {
return bounds;
}
if (bounds.length == 1 && this.planFilterAct.value == 10) {
bounds[1] = this.original.length;
bounds[1] = this.planInEditing.plan.length;
return bounds;
}
}
@ -253,6 +218,7 @@ export class EditorComponent implements OnInit {
this.disabledPlanDD = false;
}
if (this.planSearchString !== "" || this.planFilterAct.value != 0) {
let bounds = this.planElemFilterBounds();
@ -271,22 +237,24 @@ export class EditorComponent implements OnInit {
}
} else {
return this.original;
return this.planInEditing.plan;
}
}
this.latestList.next([... (this.reverseDisplay ? value().slice().reverse() : value())]);
if (this.reverseDisplay) {
return value().slice().reverse();
} else {
return value();
}
}
planIndexOf(planElement: PlanElement) {
return this.original.indexOf(planElement);
const index = this.planInEditing.plan.indexOf(planElement);
return index;
}
clearPlan() {
while (this.original.length > 0) {
this.original.pop();
}
this.filterPlanElements();
this.planInEditing.plan.length = 0;
this.cdr.detectChanges();
}
@ -298,9 +266,7 @@ export class EditorComponent implements OnInit {
}]
})).subscribe(file => {
if (file) {
const plan = new Plan();
plan.plan = [...this.original];
this.planService.savePlanAtPath(file, plan).subscribe();
this.planService.savePlanAtPath(file, this.planInEditing).subscribe();
}
});
}
@ -318,11 +284,8 @@ export class EditorComponent implements OnInit {
if (file) {
// We disallow multiple but interface still says it can be multiple, thus the cast.
this.planService.loadPlanFromPath(file as string, false).subscribe(plan => {
while (this.original.length > 0) {
this.original.pop();
}
plan.plan.forEach(item => this.original.push(item));
this.filterPlanElements();
this.planInEditing.plan.length = 0;
plan.plan.forEach(p => this.planInEditing.plan.push(p));
});
}
});
@ -330,11 +293,8 @@ export class EditorComponent implements OnInit {
loadBasePlan() {
this.planService.getBasePlan().subscribe(plan => {
while (this.original.length > 0) {
this.original.pop();
}
plan.plan.forEach(item => this.original.push(item));
this.filterPlanElements();
this.planInEditing.plan.length = 0;
plan.plan.forEach(p => this.planInEditing.plan.push(p));
})
}
@ -344,12 +304,11 @@ export class EditorComponent implements OnInit {
const dialogRef = this.dialog.open(EditNotesComponentDialog, {
data: {
note: item.notes
},
disableClose: true
},)
}
})
dialogRef.afterClosed().subscribe(note => {
if (note != undefined && note != null) {
if (note) {
if (item.notes !== note) {
item.edited = true;
}

@ -1,15 +1,17 @@
<div class="container">
<span>Edit note </span><span style="color: grey; font-size: 0.9em;">(supports markdown)</span>
<div>
<textarea [(ngModel)]="note" cols="50" rows="10"></textarea>
</div>
<div class="NOTES_COMPONENT">
<div class="container">
<span>Edit note </span><span style="color: grey; font-size: 0.9em;">(supports markdown)</span>
<div class="left">
<textarea [(ngModel)]="note" cols="50" rows="10"></textarea>
</div>
<span><span>Preview </span><span style="color: grey; font-size: 0.9em;">(Unscaled)</span></span>
<div class="w-100">
<div class="display-component" [innerHTML]="md.render(note ?? '')"></div>
<span><span>Preview </span><span style="color: grey; font-size: 0.9em;">(Unscaled)</span></span>
<div class="right">
<notes [note]="note"></notes>
</div>
</div>
<div mat-dialog-actions>
<button mat-button color="warn" (click)="cancel()">Cancel</button>
<button mat-button [mat-dialog-close]="note" cdkFocusInitial>Save</button>
</div>
</div>
<div mat-dialog-actions>
<button mat-button color="warn" (click)="cancel()">Cancel</button>
<button mat-button (click)="save()" cdkFocusInitial>Save</button>
</div>

@ -1,24 +0,0 @@
.container {
display: flex;
flex-direction: column;
height: 700px;
width: 500px;
align-items: center;
}
img {
display: block;
width: 100%;
height: 100%;
max-height: 100%;
object-fit: contain;
}
.display-component {
max-height: 100%;
height: 100%;
display: grid;
grid-auto-flow: row;
grid-template-rows: repeat(auto-fit, minmax(50px, 1fr));
width: 100%;
}

@ -1 +1 @@
<div #ref *ngIf="note" class="display-component" [innerHTML]="md.render(this.note)"></div>
<div #ref *ngIf="note" class="NOTES_COMPONENT display-component" [innerHTML]="md.render(note)"></div>

@ -1,30 +1,30 @@
.container {
display: flex;
flex-direction: column;
height: 800px;
width: 1200px;
}
.NOTES_COMPONENT {
.container {
display: flex;
flex-direction: column;
}
img {
display: block;
width: 100%;
height: 100%;
max-height: 100%;
object-fit: contain;
}
img {
display: block;
width: 100%;
height: 100%;
max-height: 100%;
object-fit: contain;
}
&.display-component {
max-height: 100%;
height: 100%;
display: grid;
grid-auto-flow: row;
grid-template-rows: repeat(auto-fit, minmax(50px, 1fr));
}
.display-component {
max-height: 100%;
height: 100%;
display: grid;
grid-auto-flow: row;
grid-template-rows: repeat(auto-fit, minmax(50px, 1fr));
font-size: 1.3em;
}
& {
font-size: 1.3em;
}
.note-preview {
min-height: 400px;
max-height: 400px;
min-width: 600px;
max-width: 600px;
.note-preview {
min-height: 400px;
}
}

@ -1,4 +1,4 @@
import { Component, ElementRef, Inject, Input, ViewChild, } from '@angular/core';
import { AfterViewInit, Component, ElementRef, Inject, Input, ViewChild, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
@ -19,33 +19,32 @@ interface DialogData {
styleUrls: ['./notes.component.scss'],
standalone: true,
imports: [CommonModule, FormsModule, MatButtonModule],
encapsulation: ViewEncapsulation.None,
})
export class NotesComponent {
export class NotesComponent implements AfterViewInit {
@Input()
note?: string;
@ViewChild("ref")
ref?: ElementRef
realValue: any;
constructor(public md: MarkdownService) {}
constructor(public md: MarkdownService) {}
ngAfterViewInit(): void {
}
}
@Component({
selector: 'notes-editor',
templateUrl: 'edit-notes.component.html',
styleUrls: ['./edit-notes.component.scss'],
standalone: true,
imports: [CommonModule, FormsModule, MatDialogModule, MatFormFieldModule, MatInputModule, FormsModule, MatButtonModule, NotesComponent, ScalableComponent],
encapsulation: ViewEncapsulation.None,
})
export class EditNotesComponentDialog {
note?: string;
note: string;
constructor(
public dialogRef: MatDialogRef<EditNotesComponentDialog>,
@Inject(MAT_DIALOG_DATA) public data: DialogData,
public md: MarkdownService
) {
if (data.note) {
this.note = `${data.note}`;
@ -55,10 +54,6 @@ export class EditNotesComponentDialog {
}
cancel() {
this.dialogRef.close(undefined);
}
save() {
this.dialogRef.close(this.note);
this.dialogRef.close();
}
}

@ -1,12 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { hmsTimestamp } from '../_services/time-tracker.service';
@Pipe({
name: 'hms',
standalone: true
})
export class HmsPipe implements PipeTransform {
transform(value?: number): string {
return hmsTimestamp(value);
}
}

@ -1,39 +1,30 @@
<div #globalTopLeft style="position: fixed; top: 0; left: 0; z-index: -1;"></div>
<div class=""></div>
<ng-container *ngIf="currentPlan">
<app-draggable-window [initialRect]="configService.config.initialPlanWindowPosition"
[configurable]="overlayService.interactable"
(savedRect)="configService.config.initialPlanWindowPosition = $event"
[backgroundColor]="backgroundColor ? backgroundColor : 'rgba(0, 0, 0, 0.1)'" (wheel)="onScroll($event)">
<div class="plan-window waypoint trial" [class]="specialClasses()">
<ng-container *ngIf="init">
<div #globalTopLeft style="position: fixed; top: 0; left: 0; z-index: -1;"></div>
<ng-container *ngIf="rect && currentPlan">
<div class="target waypoint trial"
[style.background-color]="backgroundColor ? backgroundColor : 'rgba(0, 0, 0, 0.1)'"
[style.transform]="transform()" [style.width]="rect.width + 'px'" [style.height]="rect.height + 'px'"
[class]="specialClasses()" (wheel)="onScroll($event)" #targetRef>
<ng-container *ngIf="currentPlan">
<carousel class="zones" [initIndex]="currentPlan.current" [numVisible]="configService.config.numVisible"
[offset]="clampedOffset()" [slides]="currentPlan.plan" (afterInitSelf)="registerZoneSlides($event)"
<carousel class="zones" [initIndex]="currentPlan.current"
[numVisible]="configService.config.numVisible" [offset]="clampedOffset()"
[slides]="currentPlan.plan" (afterInitSelf)="registerZoneSlides($event)"
[ngStyle]="zonesStyle()">
<ng-template let-slide let-index="index">
<div class="zone-slide" [style.color]="configService.config.noteDefaultFg"
[style.border]="index == currentPlan.current ? '1px white solid' : 'none'">
<div class="text-marker-left d-flex flex-row"
*ngIf="configService.config.showLivesplit && showDiff(slide)">
<div style="margin: 0 3px;" [class]="yourDiffClass(slide)">{{yourDiff(slide)}}
</div>
</div>
<div style="margin: 0 5px">
{{displayZoneName(worldAreaMap!.get(slide.area_key)!.name)}}
</div>
{{worldAreaMap!.get(slide.area_key)!.name}}
<div class="text-marker d-flex flex-row">
<div *ngIf="configService.config.showLivesplit" style="margin: 0 3px;">
{{cpMillis(slide)}}</div>
<div class="waypoint-text" *ngIf="hasWaypoint(slide.area_key)">(W)</div>
<div class="trial-text" *ngIf="hasTrial(slide.area_key)">(T)</div>
</div>
</div>
</ng-template>
</carousel>
<carousel *ngIf="!configService.config.detachNotes" [initIndex]="currentPlan.current"
[slides]="currentPlan.plan" (afterInitSelf)="registerCurrentSlides($event)">
<carousel [initIndex]="currentPlan.current" [slides]="currentPlan.plan"
(afterInitSelf)="registerCurrentSlides($event)">
<ng-template let-slide>
<scalable [clamp]="2">
<notes class="p-1" [note]="slide.notes" [style.color]="configService.config.noteDefaultFg"
@ -50,63 +41,61 @@
<div class="d-flex flex-column help-area">
<span><span class="waypoint-text">(W)</span> = Waypoint</span>
<span><span class="trial-text">(T)</span> = Trial</span>
<span>The plan window's will have a glow in the corresponding color(s) above to help
indicate if
the
<span>The plan window's will have a glow in the corresponding color(s) above to help indicate if the
current zone has any of those.</span>
<span>You can scroll in the plan window (while it is in 'interactable' mode) to quickly
switch
many
<span>You can scroll in the plan window (while it is in 'interactable' mode) to quickly switch many
zones</span>
</div>
</tooltip>
<span *ngIf="shouldDisplayTimer()"
class="timer">{{timeTrackerService.hmsTimestamp(timeTrackerService.elapsedTimeMillis)}}</span>
<!-- </div> -->
</div>
</app-draggable-window>
<app-draggable-window *ngIf="configService.config.showLiveAggregate"
[initialRect]="configService.config.initialAggWindowPosition" [configurable]="overlayService.interactable"
(savedRect)="configService.config.initialAggWindowPosition = $event"
[backgroundColor]="backgroundColor ? backgroundColor : 'rgba(0, 0, 0, 0.1)'">
<app-aggregate-display></app-aggregate-display>
</app-draggable-window>
<app-draggable-window [hidden]="!configService.config.detachNotes"
[initialRect]="configService.config.initialNotesWindowPosition" [configurable]="overlayService.interactable"
(savedRect)="configService.config.initialNotesWindowPosition = $event"
[backgroundColor]="backgroundColor ? backgroundColor : 'rgba(0, 0, 0, 0.1)'">
<div *ngIf="configService.config.detachNotes" class="standalone-notes">
<carousel [initIndex]="currentPlan.current" [slides]="currentPlan.plan"
(afterInitSelf)="registerCurrentSlides($event)">
<ng-template let-slide>
<scalable [clamp]="2">
<notes class="p-1" [note]="slide.notes" [style.color]="configService.config.noteDefaultFg"
#noteSlide></notes>
</scalable>
</ng-template>
</carousel>
</div>
</app-draggable-window>
<ngx-moveable #moveable [target]="targetRef" [draggable]="draggable && overlayService.interactable"
[resizable]="true && overlayService.interactable" (drag)="onDrag($event)" (resize)="onResize($event)"
(dragEnd)="onDragEnd($event)" (resizeEnd)="onResizeEnd($event)" [bounds]="bounds" [snappable]="true"
[style.visibility]="overlayService.interactable ? 'visible' : 'hidden'"></ngx-moveable>
</ng-container>
</ng-container>
<ng-template cdkConnectedOverlay
[cdkConnectedOverlayOpen]="(settingsOpen || !currentPlan) && overlayService.interactable"
[cdkConnectedOverlayOrigin]="globalTopLeft" (detach)="settingsOpen = false">
<div class="overlay container-fluid vw-100">
<div class="row row-cols-2">
<div class="planChooser col-xs-6 col-sm-6 col-md-6 col-lg-4 col-xl-4">
<div class="d-flex justify-content-evenly">
<div class="col-xs-4">
<button class="" mat-raised-button color="accent" (click)="openDialog()">Browse
Plans
</button>
</div>
<div class="col-xs-4">
<button class="" mat-raised-button color="accent" (click)="loadBasePlan()">
Load base plan
</button>
</div>
<div class="col-xs-4">
<button class="" mat-raised-button color="accent" (click)="loadFromUrl()">
Import from url
</button>
</div>
</div>
<div class="enumerated">
<mat-list role="list">
<mat-list-item class="d-flex flex-column" role="listitem"
*ngFor="let plan of previousPlans">
<button (click)="loadPrevious(plan.stored_path!)">
<img *ngIf="plan.update_url" src="assets/public.svg">{{plan.name}}
</button>
</mat-list-item>
</mat-list>
<ng-template cdkConnectedOverlay
[cdkConnectedOverlayOpen]="(settingsOpen || !currentPlan) && overlayService.interactable"
[cdkConnectedOverlayOrigin]="globalTopLeft" (detach)="settingsOpen = false">
<div class="overlay container-fluid vw-100">
<div class="row row-cols-2 h-100">
<app-plan-selection class="planChooser col-xs-6 col-sm-6 col-md-6 col-lg-4 col-xl-4 d-flex flex-column"></app-plan-selection>
<settings class="col-xs-6 col-sm-6 offset-md-1 col-md-5 offset-lg-4 col-lg-4 offset-xl-4 col-xl-4">
</settings>
</div>
</div>
<settings class="col-xs-6 col-sm-6 offset-md-1 col-md-5 offset-lg-4 col-lg-4 offset-xl-4 col-xl-4">
</settings>
</div>
<button mat-icon-button class="exit" *ngIf="currentPlan"
(click)="settingsOpen = false"><span>+</span></button>
</div>
<button mat-icon-button class="exit" *ngIf="currentPlan" (click)="settingsOpen = false"><span>+</span></button>
</div>
</ng-template>
<ng-container *ngIf="overlayService.interactable">
<app-stopwatch-controls class="stop-stopwatch"></app-stopwatch-controls>
</ng-template>
</ng-container>

@ -7,39 +7,12 @@
}
}
.plan-window {
min-width: 50px;
min-height: 50px;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
user-select: none;
&>* {
flex: 1 1 200px;
&:first-child {
flex: 1 1 15px;
min-height: 50px;
max-height: 120px;
}
}
}
.standalone-notes {
height: 100%;
width: 100%;
overflow: hidden;
}
.target-aggregate {
.target {
min-width: 50px;
min-height: 50px;
display: flex;
flex-direction: column;
user-select: none;
z-index: -1;
&>* {
flex: 1 1 200px;
@ -58,13 +31,6 @@
gap: 2px;
}
.text-marker-left {
position: absolute;
left: 10px;
bottom: 0px;
gap: 2px;
}
.waypoint-text {
color: rgba(25, 255, 255, 0.5);
}
@ -120,7 +86,7 @@
}
.zone-slide {
position: relative;
display: relative;
height: 100%;
display: flex;
align-items: center;
@ -151,12 +117,6 @@ notes {
left: 0;
}
.timer {
position: absolute;
top: 0;
left: 32px;
}
.help-area {
background-color: rgba(50, 50, 50, 1);
border: 1px solid black;
@ -176,6 +136,7 @@ notes {
font-size: 10rem;
& span {
display: flex;
align-items: center;
@ -199,16 +160,16 @@ notes {
'opsz' 48
}
.stop-stopwatch {
position: absolute;
bottom: 0;
right: 0;
.planChooser {
overflow: hidden;
}
.negative-diff {
color: green;
}
.positive-diff {
color: red;
.enumerated {
display: block;
overflow: hidden;
mat-list {
max-height: 100%;
overflow-y: scroll;
}
}

@ -1,33 +1,40 @@
import { Component, Input, NgZone, OnInit } from '@angular/core';
import { AfterViewInit, ChangeDetectorRef, Component, Input, NgZone, OnInit, ViewChild } from '@angular/core';
import { NgxMoveableComponent, OnDragEnd, OnResize, OnResizeEnd } from 'ngx-moveable';
import { OnDrag } from 'ngx-moveable';
import { ConfigService } from '../_services/config.service';
import { Rect } from '../_models/generated/Rect';
import { ShortcutService } from '../_services/shortcut.service';
import { CarouselComponent } from '../carousel/carousel.component';
import { PlanService } from '../_services/plan.service';
import { Plan, PlanElement } from '../_models/plan';
import { Plan, PlanElement, PlanMetadata } from '../_models/plan';
import { WorldAreaService } from '../_services/world-area.service';
import { WorldArea } from '../_models/world-area';
import { Subscription, from } from 'rxjs';
import { open } from '@tauri-apps/api/dialog';
import { OverlayService, StateEvent } from '../_services/overlay.service';
import { appWindow } from '@tauri-apps/api/window';
import { EventsService } from '../_services/events.service';
import { Event } from '@tauri-apps/api/event';
import { MatDialog } from '@angular/material/dialog';
import { TimeTrackerService } from '../_services/time-tracker.service';
import { RunStatService } from '../_services/run-stat.service';
@Component({
selector: 'plan-display',
templateUrl: './plan-display.component.html',
styleUrls: ['./plan-display.component.scss']
})
export class PlanDisplayComponent implements OnInit {
@Input() backgroundColor?: string;
export class PlanDisplayComponent implements AfterViewInit, OnInit {
@Input() backgroundColor?: String;
draggable: boolean = true;
rect?: Rect;
bounds: any = { "left": 0, "top": 0, "right": 0, "bottom": 0, "position": "css" };
@ViewChild("moveable") moveable?: NgxMoveableComponent;
slideIndex: number = 0;
zoneSlides?: CarouselComponent<PlanElement>;
currentSlides?: CarouselComponent<PlanElement>;
worldAreaMap?: Map<String, WorldArea>;
settingsOpen: boolean = false;
init: boolean = false;
hasAttachedOnce: boolean = false;
overlayStateChangeHandle?: Subscription;
@ -36,73 +43,52 @@ export class PlanDisplayComponent implements OnInit {
prevBind?: Subscription;
currentPlan?: Plan;
previousPlans: PlanMetadata[] = [];
constructor(private events: EventsService, public configService: ConfigService, private cdr: ChangeDetectorRef, private shortcut: ShortcutService, public planService: PlanService, public worldAreaService: WorldAreaService, public overlayService: OverlayService, private zone: NgZone) {
window.addEventListener("resize", () => {
this.zone.run(() => {
this.windowInitHandler()
})
});
constructor(
public configService: ConfigService,
public planService: PlanService,
public worldAreaService: WorldAreaService,
public overlayService: OverlayService,
public dialog: MatDialog,
public timeTrackerService: TimeTrackerService,
private events: EventsService,
private shortcut: ShortcutService,
private zone: NgZone,
private runStatService: RunStatService,
) {
this.planService.enumerateStoredPlans().subscribe(plans => {
this.previousPlans = plans;
})
this.planService.getCurrentPlan().subscribe(plan => {
this.currentPlan = plan;
if (this.configService.config.enableStopwatch) {
this.loadComparisonData(this.currentPlan);
}
this.timeTrackerService.onNewRun(plan);
//Close settings anytime we get a new current plan.
this.settingsOpen = false;
setTimeout(() => this.setIndex(plan.current), 0);
})
this.registerOnZoneEnter();
}
loadComparisonData(plan: Plan) {
if (!this.configService.config.runCompareHistory) {
return;
}
this.timeTrackerService.loadHistory(this.configService.config.runCompareHistory).subscribe(history => {
if (history) {
this.runStatService.insertTimesAtCheckpoints(history, plan);
}
});
}
registerOnZoneEnter() {
appWindow.listen("entered", (entered) => {
if (this.currentPlan && typeof entered.payload == "string") {
if (this.currentPlan.isNext(entered.payload)) {
this.zone.run(() => this.next());
if (this.currentPlan) {
const current = this.currentPlan.current;
const length = this.currentPlan.plan.length;
if (current + 1 < length) {
if (entered.payload === this.currentPlan.plan[current + 1].area_key) {
this.zone.run(() => this.next());
}
}
}
});
}
windowInitHandler() {
if (window.innerWidth > 0) {
this.ngAfterViewInit();
}
}
ngOnInit() {
this.worldAreaService.getFullWorldAreas().subscribe(a => this.worldAreaMap = a);
this.worldAreaService.getWorldAreas().subscribe(a => this.worldAreaMap = a);
this.overlayStateChangeHandle = this.events.listen<StateEvent>("OverlayStateChange").subscribe(this.onOverlayStateChange.bind(this));
this.events.listen<boolean>("overlay_or_target_focus").subscribe((event: Event<boolean>) => {
if (!event.payload) {
this.destroyBinds();
} else {
this.setupBinds();
}
});
}
onOverlayStateChange(event: Event<StateEvent>) {
@ -125,6 +111,18 @@ export class PlanDisplayComponent implements OnInit {
return Math.abs(v);
}
transform() {
return `translate(${this.rect!.x}px, ${this.rect!.y}px)`;
}
width() {
return `${this.rect!.width}px`;
}
height() {
return `${this.rect!.height}px`;
}
hasWaypoint(key?: string): boolean {
if (!key) {
key = this.currentPlan!.plan[this.currentPlan!.current].area_key;
@ -141,6 +139,52 @@ export class PlanDisplayComponent implements OnInit {
return this.worldAreaService.hasTrial(key);
}
ngAfterViewInit(): void {
if (window.innerWidth > 0) {
const cfgRect = this.configService.config.initialPlanWindowPosition;
this.rect = {
x: cfgRect.x * window.innerWidth,
y: cfgRect.y * window.innerHeight,
width: cfgRect.width * window.innerWidth,
height: cfgRect.height * window.innerHeight,
}
this.moveable?.updateRect();
setTimeout(() => this.cdr.detectChanges(), 0);
this.init = true;
}
}
onDrag(e: OnDrag) {
this.rect!.x = e.translate[0];
this.rect!.y = e.translate[1];
}
onDragEnd(e: OnDragEnd) {
this.saveRect();
}
onResize(e: OnResize) {
this.rect!.width = e.width;
this.rect!.height = e.height;
this.onDrag(e.drag);
}
onResizeEnd(e: OnResizeEnd) {
this.saveRect();
}
saveRect() {
const toCfgRect = this.rect!;
this.configService.config.initialPlanWindowPosition = {
x: toCfgRect.x / window.innerWidth,
y: toCfgRect.y / window.innerHeight,
width: toCfgRect.width / window.innerWidth,
height: toCfgRect.height / window.innerHeight,
}
}
registerZoneSlides(carousel: CarouselComponent<PlanElement>) {
this.zoneSlides = carousel;
this.zoneSlides.setIndex(this.slideIndex);
@ -148,23 +192,8 @@ export class PlanDisplayComponent implements OnInit {
setupBinds() {
if (this.currentSlides && !this.bindsAreSetup) {
this.nextBind = this.shortcut.register(this.configService.config.prev).subscribe((_shortcut) => {
this.prev();
if (this.configService.config.enableStopwatch) {
this.timeTrackerService.onForcePrev(this.currentPlan!.plan[this.currentPlan!.current].area_key);
this.checkCheckpoint();
}
});
this.prevBind = this.shortcut.register(this.configService.config.next).subscribe((_shortcut) => {
this.next();
if (this.configService.config.enableStopwatch) {
this.timeTrackerService.onForceNext(this.currentPlan!.plan[this.currentPlan!.current].area_key);
this.checkCheckpoint();
}
});
this.nextBind = this.shortcut.register(this.configService.config.prev).subscribe((_shortcut) => this.prev());
this.prevBind = this.shortcut.register(this.configService.config.next).subscribe((_shortcut) => this.next());
this.bindsAreSetup = true;
}
}
@ -179,54 +208,11 @@ export class PlanDisplayComponent implements OnInit {
next() {
if (this.overlayService.visible) {
this.currentPlan!.next();
this.checkCheckpoint();
this.currentSlides?.next();
this.zoneSlides?.next();
}
}
checkCheckpoint() {
if (!this.currentPlan || !this.timeTrackerService.isActive) return;
const currentElem = this.currentPlan.plan[this.currentPlan.current];
if (currentElem.checkpoint && !currentElem.checkpoint_your_millis) {
currentElem.checkpoint_your_millis = this.timeTrackerService.elapsedTimeMillis;
this.timeTrackerService.reportCheckpoint(currentElem.uuid!);
}
}
yourDiff(element: PlanElement) {
if (!element.checkpoint || !element.checkpoint_your_millis || !element.checkpoint_millis) return "";
const diff = element.checkpoint_your_millis - element.checkpoint_millis;
const neg = diff <= 0;
const abs = Math.abs(diff);
if (diff == 0) {
return `${neg ? "-" : "+"}00:00:00`;
} else {
return `${neg ? "-" : "+"}${this.timeTrackerService.hmsTimestamp(abs)}`;
}
}
yourDiffClass(element: PlanElement): string {
if (!element.checkpoint || !element.checkpoint_your_millis || !element.checkpoint_millis) return "";
const diff = element.checkpoint_your_millis - element.checkpoint_millis;
const neg = diff <= 0;
return neg ? "negative-diff" : "positive-diff";
}
showDiff(element: PlanElement) {
return element.checkpoint && element.checkpoint_your_millis && element.checkpoint_millis;
}
cpMillis(element: PlanElement) {
if (!element.checkpoint) return "";
if (!element.checkpoint_millis) return "N/A";
return this.timeTrackerService.hmsTimestamp(element.checkpoint_millis);
}
prev() {
if (this.overlayService.visible) {
this.currentPlan!.prev();
@ -245,22 +231,55 @@ export class PlanDisplayComponent implements OnInit {
}
}
loadPrevious(path: string) {
this.planService.loadPlanFromPath(path, false).subscribe(plan => {
this.planService.setCurrentPlan(plan);
});
}
settingsClick(event: any) {
this.settingsOpen = !this.settingsOpen;
event.stopPropagation();
}
openDialog() {
from(open({
multiple: false,
filters: [
{
name: "JSON (.json)",
extensions: ['json']
}
]
})).subscribe(file => {
if (file) {
this.planService.loadPlanFromPath(file as string).subscribe(plan => {
if (plan) {
this.planService.setCurrentPlan(plan);
this.settingsOpen = false;
}
});
}
});
}
loadBasePlan() {
this.planService.getBasePlan().subscribe(plan => {
this.currentPlan = new Plan(plan);
if (this.zoneSlides) {
this.zoneSlides.setIndex(0);
}
if (this.currentSlides) {
this.currentSlides.setIndex(0);
}
})
}
onScroll(event: WheelEvent) {
if (event.deltaY < 0) {
this.prev();
this.timeTrackerService.onForcePrev(this.currentPlan!.plan[this.currentPlan!.current].area_key);
this.checkCheckpoint();
} else {
this.next();
this.timeTrackerService.onForceNext(this.currentPlan!.plan[this.currentPlan!.current].area_key);
this.checkCheckpoint();
}
}
@ -281,33 +300,18 @@ export class PlanDisplayComponent implements OnInit {
}
}
shouldDisplayTimer(): boolean {
if (!this.configService.config.enableStopwatch) return false;
return this.timeTrackerService.isActive;
}
displayZoneName(zoneName: string) {
if (this.configService.config.shortenZoneNames) {
return this.trim(this.trimUnneccesaryWords(zoneName));
} else {
return zoneName;
}
}
trimUnneccesaryWords(zoneName: string) {
if (zoneName.toLowerCase().startsWith("the ")) {
return zoneName.substring(4);
} else {
return zoneName;
}
}
trim(zoneName: string, letters: number = 12) {
if (zoneName.length > letters + 3) {
return zoneName.substring(0, letters) + "...";
}
return zoneName;
loadFromUrl() {
this.planService.loadFromUrl().subscribe(plan => {
console.log("plan", plan);
if (plan) {
this.planService.savePlanAtStore(plan.name!, plan).subscribe((path) => {
console.log("path", path);
if(path) {
plan.setPath(path);
}
});
this.planService.setCurrentPlan(plan);
}
});
}
}

@ -12,19 +12,11 @@ import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatListModule } from '@angular/material/list';
import { ScalableComponent } from '../Scalable/scalable.component';
import { MatTooltipModule } from '@angular/material/tooltip';
import {MatTooltipModule} from '@angular/material/tooltip';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { AngularSvgIconModule } from 'angular-svg-icon';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatDialogModule } from '@angular/material/dialog';
import { ResumeDialog } from './resume-dialog.component';
import { AggregateDisplayComponent } from '../aggregate-display/aggregate-display.component';
import { DraggableWindowComponent } from '../draggable-window/draggable-window.component';
import { StopwatchControlsComponent } from './stopwatch-controls/stopwatch-controls.component';
import { PlanSelectionComponent } from './plan-selection/plan-selection.component';
@NgModule({
declarations: [
PlanDisplayComponent,
PlanDisplayComponent
],
imports: [
CommonModule,
@ -41,15 +33,7 @@ import { PlanSelectionComponent } from './plan-selection/plan-selection.componen
MatListModule,
ScalableComponent,
MatTooltipModule,
TooltipComponent,
AngularSvgIconModule,
ScrollingModule,
MatDialogModule,
ResumeDialog,
AggregateDisplayComponent,
DraggableWindowComponent,
StopwatchControlsComponent,
PlanSelectionComponent,
TooltipComponent
],
exports: [
PlanDisplayComponent

@ -1,37 +0,0 @@
<div class="d-flex justify-content-evenly align-items-center mb-2 mt-2">
<div class="col-xs-4">
<button class="" mat-raised-button color="accent" (click)="openDialog()">Browse
Plans
</button>
</div>
<div class="col-xs-4">
<button class="" mat-raised-button color="accent" (click)="loadBasePlan()">
Load base plan
</button>
</div>
<div class="col-xs-4">
<button class="" mat-raised-button color="accent" (click)="loadFromUrl()">
Import from url
</button>
</div>
</div>
<div class="enumerated d-flex flex-column">
<mat-list role="list" class="d-flex flex-column h-100">
<cdk-virtual-scroll-viewport itemSize="10" class="h-100">
<mat-list-item class="d-flex flex-column" role="listitem" *cdkVirtualFor="let plan of plans">
<span>
<button [disabled]="disablePlans" (click)="loadPrevious(plan.stored_path!)">
<img *ngIf="plan.update_url" src="assets/public.svg">{{plan.name}}
</button><button *ngIf="plan.update_url" class="planActionButton" [disabled]="disablePlans"
(click)="checkForPlanUpdate(plan)" [title]="recentPlanTitle(plan)">
<svg-icon *ngIf="!hasUpdate(plan)" src="assets/material-sync.svg" />
<svg-icon *ngIf="hasUpdate(plan) && !isErr(plan)" src="assets/material-check.svg"
class="nice" />
<svg-icon *ngIf="hasUpdate(plan) && isErr(plan)" src="assets/material-warning.svg"
class="notnice" />
</button>
</span>
</mat-list-item>
</cdk-virtual-scroll-viewport>
</mat-list>
</div>

@ -1,34 +0,0 @@
.planChooser {
overflow: hidden;
}
.enumerated {
display: block;
overflow: hidden;
flex-grow: 1;
background-color: rgba(200, 200, 220, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
mat-list {
max-height: 100%;
overflow-y: scroll;
}
}
.nice {
svg {
fill: green;
}
}
.notnice {
svg {
fill: red;
}
}
.planActionButton {
align-items: center;
justify-content: center;
padding-bottom: 1px;
}

@ -1,21 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PlanSelectionComponent } from './plan-selection.component';
describe('PlanSelectionComponent', () => {
let component: PlanSelectionComponent;
let fixture: ComponentFixture<PlanSelectionComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [PlanSelectionComponent]
});
fixture = TestBed.createComponent(PlanSelectionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -1,143 +0,0 @@
import { Component } from '@angular/core';
import { Plan, PlanMetadata } from 'src/app/_models/plan';
import { PlanService, UrlError } from 'src/app/_services/plan.service';
import { open } from '@tauri-apps/api/dialog';
import { from } from 'rxjs';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatListModule } from '@angular/material/list';
import { AngularSvgIconModule } from 'angular-svg-icon';
@Component({
selector: 'app-plan-selection',
templateUrl: './plan-selection.component.html',
styleUrls: ['./plan-selection.component.scss'],
standalone: true,
imports: [CommonModule, MatButtonModule, ScrollingModule, MatListModule, AngularSvgIconModule]
})
export class PlanSelectionComponent {
plans: PlanMetadata[] = [];
checkingPlanUpdate: boolean = false;
recentUpdateAttempts: Map<string, Date | UrlError> = new Map<string, Date | UrlError>();
constructor(
private planService: PlanService,
) {
this.planService.getStoredPlans().subscribe(plans => {
this.plans = plans;
})
}
get disablePlans(): boolean {
return this.checkingPlanUpdate;
}
openDialog() {
from(open({
multiple: false,
filters: [
{
name: "JSON (.json)",
extensions: ['json']
}
]
})).subscribe(file => {
if (file) {
this.planService.loadPlanFromPath(file as string).subscribe(plan => {
if (plan) {
this.planService.setCurrentPlan(plan);
}
});
}
});
}
loadBasePlan() {
this.planService.getBasePlan().subscribe(plan => {
plan.current = 0;
this.planService.setCurrentPlan(plan);
});
}
loadFromUrl() {
this.planService.loadFromUrl().subscribe(plan => {
if (plan) {
this.planService.savePlanAtStore(plan.name!, plan).subscribe((path) => {
if (path) {
plan.setPath(path);
}
});
this.planService.setCurrentPlan(plan);
}
});
}
loadPrevious(path: string) {
this.planService.loadPlanFromPath(path, false).subscribe(plan => {
this.planService.setCurrentPlan(plan);
});
}
checkForPlanUpdate(plan: PlanMetadata) {
this.checkingPlanUpdate = true;
this.planService.checkForPlanUpdate(plan).subscribe({
next: updatedPlan => {
console.log("check update: ", updatedPlan);
this.planService.loadPlanFromPath(plan.stored_path!, false).subscribe(oldPlan => {
console.log("oldPlan", oldPlan);
const newCurrent = this.commonUUIDToNewCurrent(oldPlan, updatedPlan);
updatedPlan.current = newCurrent;
//TODO: Interface to determine if we want to save the plan or not (allow not doing it in case user regrets... Also needs position error fix).
this.planService.savePlanAtStore(oldPlan.name!, updatedPlan, true).subscribe();
})
},
complete: () => {
this.checkingPlanUpdate = false
this.recentUpdateAttempts.set(plan.update_url!, new Date());
},
error: (err) => {
this.recentUpdateAttempts.set(plan.update_url!, err);
if (err instanceof UrlError) {
alert("Error retrieving plan update (" + err.status + "): " + err.message);
} else if (err instanceof Error) {
alert("Unexpected error: " + err.message);
}
this.checkingPlanUpdate = false;
}
});
}
recentPlanTitle(plan: PlanMetadata) {
if (!this.hasUpdate(plan)) return "Check for update";
if (this.hasUpdate(plan) && !this.isErr(plan)) return "Up to date";
if (this.hasUpdate(plan) && this.isErr(plan)) return "Error updating";
return "";
}
isErr(plan: PlanMetadata) {
return this.recentUpdateAttempts.get(plan.update_url!) instanceof UrlError;
}
hasUpdate(plan: PlanMetadata): any {
return this.recentUpdateAttempts.has(plan.update_url!);
}
private commonUUIDToNewCurrent(oldPlan: Plan, updatedPlan: Plan): number {
let bestOldCurrent = oldPlan.current;
let newIndex = 0;
//Attempt current plan element's UUID first, otherwise step backwards until we find a match
while (bestOldCurrent >= 0) {
const tempNewIndex = updatedPlan.plan.findIndex(e => e.uuid === oldPlan.plan[bestOldCurrent].uuid);
if (tempNewIndex !== -1) {
newIndex = tempNewIndex;
break;
}
bestOldCurrent--;
}
return newIndex;
}
}

@ -1,6 +0,0 @@
You have an ongoing run history <b>(time tracking)</b> going, would you like to resume it?
<div mat-dialog-actions>
<button mat-raised-button color="color-resume-instant" (click)="instant()">Resume instantaneously</button>
<button mat-raised-button color="color-resume-next" (click)="next()" cdkFocusInitial>Resume on entering next zone</button>
<button mat-raised-button color="color-resume-no" (click)="discard()" cdkFocusInitial>Start new</button>
</div>

@ -1,33 +0,0 @@
import { Component } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { MatButtonModule } from "@angular/material/button";
import { MatDialogModule, MatDialogRef } from "@angular/material/dialog";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
export enum Resume {
Discard,
Next,
Instant
}
@Component({
selector: 'resume-dialog',
templateUrl: 'resume-dialog.component.html',
standalone: true,
imports: [MatDialogModule, MatFormFieldModule, MatInputModule, FormsModule, MatButtonModule],
})
export class ResumeDialog {
instant() {
this.dialogRef.close(Resume.Instant);
}
next() {
this.dialogRef.close(Resume.Next);
}
discard() {
this.dialogRef.close(Resume.Discard);
}
constructor(public dialogRef: MatDialogRef<ResumeDialog>) {}
}

@ -1,5 +0,0 @@
<button mat-raised-button color="warn" *ngIf="timeTrackerService.isActive"
(click)="timeTrackerService.stop()">Force stop stopwatch</button>
<button mat-raised-button color="warn"
*ngIf="!timeTrackerService.isActive && timeTrackerService.hasRunLoaded" (click)="timeTrackerService.startLoaded()">Start last
loaded</button>

@ -1,21 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { StopwatchControlsComponent } from './stopwatch-controls.component';
describe('StopwatchControlsComponent', () => {
let component: StopwatchControlsComponent;
let fixture: ComponentFixture<StopwatchControlsComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [StopwatchControlsComponent]
});
fixture = TestBed.createComponent(StopwatchControlsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -1,15 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { TimeTrackerService } from 'src/app/_services/time-tracker.service';
@Component({
selector: 'app-stopwatch-controls',
templateUrl: './stopwatch-controls.component.html',
styleUrls: ['./stopwatch-controls.component.scss'],
imports: [CommonModule, MatButtonModule],
standalone: true
})
export class StopwatchControlsComponent {
constructor(public timeTrackerService: TimeTrackerService) { }
}

@ -1,60 +0,0 @@
<button mat-stroked-button color="warn" (click)="reset()">Reset view</button>
<ng-container *ngIf="!shouldShowTable && cache">
<div class="cache-viewport">
<div *ngFor="let item of this.cache | keyvalue: myOrder" class="cache-item d-flex flex-row p-4">
<div class="d-flex flex-column">
<span>Plan: {{item.value.associatedName}}</span>
<span>Run time: {{hms(item.value.currentElapsedMillis)}}</span>
<span>Last updated: {{dateFormat(item.value.last_updated)}}</span>
</div>
<div class="d-flex flex-column justify-content-center align-items-center">
<button mat-stroked-button color="accent" (click)="loadMain(item.key)">Load</button>
<button mat-stroked-button color="accent" (click)="setComparison(item.key)">Live compare</button>
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="shouldShowTable">
<mat-slide-toggle [(ngModel)]="this.aggregate">Show aggregated results</mat-slide-toggle>
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="zoneName">
<th mat-header-cell *matHeaderCellDef> Zone </th>
<td mat-cell *matCellDef="let entry"> {{entry.zoneName}} </td>
</ng-container>
<ng-container matColumnDef="entryTime">
<th mat-header-cell *matHeaderCellDef> Time of entry </th>
<td mat-cell *matCellDef="let entry"> {{entry.entryTime}} </td>
</ng-container>
<ng-container matColumnDef="estimatedExit">
<th mat-header-cell *matHeaderCellDef> Time of estimated exit </th>
<td mat-cell *matCellDef="let entry"> {{entry.estimatedExit}} </td>
</ng-container>
<ng-container matColumnDef="estimatedTimeSpent">
<th mat-header-cell *matHeaderCellDef> Estimated time spent </th>
<td mat-cell *matCellDef="let entry"> {{entry.estimatedTimeSpent}} </td>
</ng-container>
<ng-container matColumnDef="aggregateFirstEntry">
<th mat-header-cell *matHeaderCellDef> Time of first entry </th>
<td mat-cell *matCellDef="let entry"> {{entry.aggregateFirstEntry}} </td>
</ng-container>
<ng-container matColumnDef="aggregateLastExit">
<th mat-header-cell *matHeaderCellDef> Time of estimated last exit </th>
<td mat-cell *matCellDef="let entry"> {{entry.aggregateLastExit}} </td>
</ng-container>
<ng-container matColumnDef="aggregateTimeSpent">
<th mat-header-cell *matHeaderCellDef> Estimated total time spent </th>
<td mat-cell *matCellDef="let entry"> {{entry.aggregateTimeSpent}} </td>
</ng-container>
<ng-container matColumnDef="aggregateNumEntries">
<th mat-header-cell *matHeaderCellDef> Number of entries </th>
<td mat-cell *matCellDef="let entry"> {{entry.aggregateNumEntries}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</ng-container>

@ -1,3 +0,0 @@
.cache-item {
gap: 12px;
}

@ -1,21 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RunStatsComponent } from './run-stats.component';
describe('RunStatsComponent', () => {
let component: RunStatsComponent;
let fixture: ComponentFixture<RunStatsComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RunStatsComponent]
});
fixture = TestBed.createComponent(RunStatsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -1,155 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule, KeyValue } from '@angular/common';
import { EntryType, RunHistory, RunHistoryMetadata, TimeTrackerService } from '../_services/time-tracker.service';
import { MatTableModule } from '@angular/material/table';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { FormsModule } from '@angular/forms';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatButtonModule } from '@angular/material/button';
import { WorldAreaService } from '../_services/world-area.service';
import { WorldArea } from '../_models/world-area';
import { AggregateRunStat, RunStat, RunStatService, RunStatType, UnformattedAggregationData, UnformattedRunStat } from '../_services/run-stat.service';
import { ConfigService } from '../_services/config.service';
@Component({
selector: 'app-run-stats',
standalone: true,
imports: [CommonModule, MatTableModule, MatSlideToggleModule, FormsModule, ScrollingModule, MatButtonModule],
templateUrl: './run-stats.component.html',
styleUrls: ['./run-stats.component.scss']
})
export class RunStatsComponent implements OnInit {
aggregated?: AggregateRunStat[];
direct?: RunStat[];
aggregate: boolean = true;
cache?: Map<string, RunHistoryMetadata>;
worldAreaMap?: Map<string, WorldArea>;
constructor(
private timeTrackerService: TimeTrackerService,
private worldAreaService: WorldAreaService,
private runStatService: RunStatService,
private configService: ConfigService
) {
this.worldAreaService.getFullWorldAreas().subscribe((data) => {
this.worldAreaMap = data;
})
}
ngOnInit(): void {
this.timeTrackerService.storedHistories.subscribe((data) => {
this.cache = data;
})
}
get dataSource(): RunStatType[] {
return (this.aggregate ? this.aggregated : this.direct) ?? [];
}
get hasInitializedData() {
return this.aggregated || this.direct;
}
get shouldShowTable() {
return this.hasInitializedData;
}
loadMain(uuid: string) {
this.timeTrackerService.loadHistory(uuid).subscribe((data => {
if (data) {
this.onLoad(data);
}
}));
}
formatAggregate(data: UnformattedAggregationData) {
const { aggregation, aggregateNAId } = data;
return aggregation.map((entry) => {
let aggregateTimeSpent;
if (aggregateNAId === entry.zoneId) {
aggregateTimeSpent = this.timeTrackerService.hmsTimestamp(entry.aggregateTimeSpent) + " + N/A"
} else {
aggregateTimeSpent = this.timeTrackerService.hmsTimestamp(entry.aggregateTimeSpent);
}
return {
zoneName: this.resolveZone(entry.zoneId),
aggregateFirstEntry: this.timeTrackerService.hmsTimestamp(entry.aggregateFirstEntry),
aggregateLastExit: aggregateNAId === entry.zoneId ? "N/A" : this.timeTrackerService.hmsTimestamp(entry.aggregateLastExit),
aggregateTimeSpent: aggregateTimeSpent,
aggregateNumEntries: entry.aggregateNumEntries.toString(),
}
});
}
formatDirect(direct: UnformattedRunStat[]) {
return direct.filter(entry => entry.entryType == EntryType.ZoneEnter).map(entry => {
return {
zoneName: this.resolveZone(entry.zoneId),
entryTime: this.timeTrackerService.hmsTimestamp(entry.entryTime),
estimatedExit: entry.estimatedExit ? this.timeTrackerService.hmsTimestamp(entry.estimatedExit) : "N/A",
estimatedTimeSpent: entry.estimatedTimeSpent ? this.timeTrackerService.hmsTimestamp(entry.estimatedTimeSpent) : "N/A",
}
})
}
dateFormat(value: number) {
return new Date(value).toLocaleString();
}
onLoad(data: RunHistory) {
this.direct = this.formatDirect(this.runStatService.calcDirect(data));
this.aggregated = this.formatAggregate(this.runStatService.calcAggregated(data));
}
hms(time: number) {
return this.timeTrackerService.hmsTimestamp(time);
}
private resolveZone(zoneId: string) {
const area = this.worldAreaMap?.get(zoneId);
if (!area) {
return "Unknown zone: " + zoneId;
}
// Act might not be very reasonable but it's the best we have it..
return area.name + " (A" + area.act + ")"
}
reset() {
this.timeTrackerService.loadCache();
this.aggregated = undefined;
this.direct = undefined;
}
myOrder(a: KeyValue<string, RunHistoryMetadata>, b: KeyValue<string, RunHistoryMetadata>): number {
return b.value.last_updated - a.value.last_updated;
}
setComparison(id: string) {
this.configService.config.runCompareHistory = id;
}
get displayedColumns() {
if (this.aggregate) {
return [
"zoneName",
"aggregateFirstEntry",
"aggregateLastExit",
"aggregateTimeSpent",
"aggregateNumEntries",
]
} else {
return [
"zoneName",
"entryTime",
"estimatedExit",
"estimatedTimeSpent",
]
}
}
}

@ -59,37 +59,4 @@
[max]="configService.config.numVisible - 1" step="1">
</mat-form-field>
</div>
<div class="d-flex flex-row justify-content-between">
<span class="d-block">Shorten zone names</span>
<mat-slide-toggle [color]="overlayService.isOverlay ? 'primary-on-dark' : 'primary'"
[(ngModel)]="configService.config.shortenZoneNames"></mat-slide-toggle>
</div>
<div class="d-flex flex-row justify-content-between">
<span class="d-block">Detach zones</span>
<mat-slide-toggle [color]="overlayService.isOverlay ? 'primary-on-dark' : 'primary'"
[(ngModel)]="configService.config.detachNotes"></mat-slide-toggle>
</div>
<div class="d-flex flex-row justify-content-between">
<span class="d-block">Enable stopwatch</span>
<mat-slide-toggle [color]="overlayService.isOverlay ? 'primary-on-dark' : 'primary'"
[(ngModel)]="configService.config.enableStopwatch"></mat-slide-toggle>
</div>
<ng-container *ngIf="configService.config.enableStopwatch">
<div class="d-flex flex-row justify-content-between">
<span class="d-block">Enable displaying scuffed livesplit</span>
<mat-slide-toggle [color]="overlayService.isOverlay ? 'primary-on-dark' : 'primary'"
[(ngModel)]="configService.config.showLivesplit"></mat-slide-toggle>
</div>
<div class="d-flex flex-row justify-content-between">
<span class="d-block">Enable aggregate-stats live compare</span>
<mat-slide-toggle [color]="overlayService.isOverlay ? 'primary-on-dark' : 'primary'"
[(ngModel)]="configService.config.showLiveAggregate"></mat-slide-toggle>
</div>
</ng-container>
</div>

@ -1,2 +0,0 @@
<ng-container *ngTemplateOutlet="template!; context: { $implicit: value }">
</ng-container>

@ -1,21 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WrapValueComponent } from './wrap-value.component';
describe('WrapValueComponent', () => {
let component: WrapValueComponent;
let fixture: ComponentFixture<WrapValueComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [WrapValueComponent]
});
fixture = TestBed.createComponent(WrapValueComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -1,14 +0,0 @@
import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-wrap-value',
standalone: true,
imports: [CommonModule],
templateUrl: './wrap-value.component.html',
styleUrls: ['./wrap-value.component.scss']
})
export class WrapValueComponent<T> {
@Input() value?: T;
@ContentChild(TemplateRef) template?: TemplateRef<any>;
}

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z"/></svg>

Before

Width:  |  Height:  |  Size: 159 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M160-160v-80h110l-16-14q-52-46-73-105t-21-119q0-111 66.5-197.5T400-790v84q-72 26-116 88.5T240-478q0 45 17 87.5t53 78.5l10 10v-98h80v240H160Zm400-10v-84q72-26 116-88.5T720-482q0-45-17-87.5T650-648l-10-10v98h-80v-240h240v80H690l16 14q49 49 71.5 106.5T800-482q0 111-66.5 197.5T560-170Z"/></svg>

Before

Width:  |  Height:  |  Size: 388 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m40-120 440-760 440 760H40Zm138-80h604L480-720 178-200Zm302-40q17 0 28.5-11.5T520-280q0-17-11.5-28.5T480-320q-17 0-28.5 11.5T440-280q0 17 11.5 28.5T480-240Zm-40-120h80v-200h-80v200Zm40-100Z"/></svg>

Before

Width:  |  Height:  |  Size: 295 B

@ -80,16 +80,4 @@ div.picker_wrapper.popup {
.mdc-notched-outline__notch {
clip-path: none !important;
}
.mat-color-resume-instant {
background-color: rgb(76, 146, 146);
}
.mat-color-resume-next {
background-color: rgb(120, 76, 146);
}
.mat-color-resume-no {
background-color: rgb(180, 41, 41);
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save