Moved some overlay handling to service. Fixed some issue with configs, implemented config storage, some other cleanup and fixes
parent
9fb81ca554
commit
bd8cfb573c
@ -0,0 +1,14 @@
|
||||
use poe_data::world_area::WorldArea;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Plan {
|
||||
plan: Vec<PlanElement>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PlanElement {
|
||||
area: WorldArea,
|
||||
notes: String,
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
use std::error::Error;
|
||||
|
||||
use directories::ProjectDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::plan::Plan;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Storage {
|
||||
pub config: Config,
|
||||
#[serde(skip)]
|
||||
pub plan: Option<Plan>,
|
||||
pub plan_name: Option<String>,
|
||||
}
|
||||
|
||||
const QUALIFIER: &'static str = "me";
|
||||
const ORGANIZATION: &'static str = "isark.poe";
|
||||
const APPLICATION: &'static str = "Nothing";
|
||||
const CONFIG_FILE: &'static str = "configuration.json";
|
||||
const SAVED_PLANS: &'static str = "plans";
|
||||
|
||||
fn proj_dir() -> Option<ProjectDirs> {
|
||||
ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION)
|
||||
}
|
||||
|
||||
impl Default for Storage {
|
||||
fn default() -> Self {
|
||||
let config = Self::load();
|
||||
match config {
|
||||
Some(config) => config,
|
||||
None => Self {
|
||||
config: Default::default(),
|
||||
plan: Default::default(),
|
||||
plan_name: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
fn load() -> Option<Self> {
|
||||
let mut storage: Option<Storage> = serde_json::from_str(
|
||||
&std::fs::read_to_string(proj_dir()?.data_dir().with_file_name(CONFIG_FILE)).ok()?,
|
||||
)
|
||||
.ok();
|
||||
|
||||
if let Some(storage) = &mut storage {
|
||||
if let Some(plan_name) = &storage.plan_name {
|
||||
storage.plan = Self::load_plan(plan_name);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Loaded storage: {:?}", storage);
|
||||
|
||||
storage
|
||||
}
|
||||
|
||||
fn load_plan<T: Into<String>>(file_name: T) -> Option<Plan> {
|
||||
serde_json::from_str(
|
||||
&std::fs::read_to_string(
|
||||
proj_dir()?
|
||||
.data_dir()
|
||||
.join(SAVED_PLANS)
|
||||
.with_file_name(file_name.into()),
|
||||
)
|
||||
.ok()?,
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
if let Ok(content) = serde_json::to_string_pretty(&self) {
|
||||
if let Some(dir) = proj_dir() {
|
||||
match std::fs::write(dir.data_dir().with_file_name(CONFIG_FILE), content) {
|
||||
Ok(_) => {
|
||||
if let Some(c) = dir.data_dir().with_file_name(CONFIG_FILE).to_str() {
|
||||
log::info!("Saved config to {}", c)
|
||||
}
|
||||
}
|
||||
Err(_) => log::error!("Could not write config"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +1,40 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { APP_INITIALIZER, NgModule } from "@angular/core";
|
||||
import { BrowserModule } from "@angular/platform-browser";
|
||||
|
||||
import { AppComponent } from "./app.component";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { RecordKeyChord } from "./directives/record-key-chord.directive";
|
||||
import { PlanDisplayModule } from "./plan-display/plan-display.module";
|
||||
import { ConfigService } from "./services/config.service";
|
||||
import { ColorPickerComponent } from './color-picker/color-picker.component';
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
|
||||
export function initializeApp(configService: ConfigService) {
|
||||
return (): Promise<any> => {
|
||||
return configService.init();
|
||||
};
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
RecordKeyChord
|
||||
RecordKeyChord, ColorPickerComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
PlanDisplayModule,
|
||||
MatButtonModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
// Makes sure we can get the config initialized ASAP as we might need it in lots of places really early.
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initializeApp,
|
||||
deps: [ConfigService],
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule { }
|
||||
|
@ -0,0 +1,3 @@
|
||||
<span #clickColorPickerElement>
|
||||
<button mat-raised-button color="primary"><ng-content></ng-content></button>
|
||||
</span>
|
@ -0,0 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ColorPickerComponent } from './color-picker.component';
|
||||
|
||||
describe('ColorPickerComponent', () => {
|
||||
let component: ColorPickerComponent;
|
||||
let fixture: ComponentFixture<ColorPickerComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ColorPickerComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(ColorPickerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,50 @@
|
||||
import { AfterContentInit, AfterViewInit, Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import Picker from 'vanilla-picker';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { Subject, delay, startWith } from 'rxjs';
|
||||
|
||||
export interface Color {
|
||||
rgba: number[];
|
||||
hsla: number[];
|
||||
rgbString: string;
|
||||
rgbaString: string;
|
||||
hslString: string;
|
||||
hslaString: string;
|
||||
hex: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'color-picker',
|
||||
templateUrl: './color-picker.component.html',
|
||||
styleUrls: ['./color-picker.component.scss'],
|
||||
})
|
||||
export class ColorPickerComponent implements AfterViewInit {
|
||||
|
||||
@ViewChild('clickColorPickerElement')
|
||||
clickColorPickerElement?: ElementRef;
|
||||
|
||||
@Output('color')
|
||||
outColor = new EventEmitter<Color>;
|
||||
|
||||
@Input('initialColor')
|
||||
initialColor?: string;
|
||||
|
||||
loaded: Subject<void> = new Subject<void>();
|
||||
|
||||
picker?: Picker;
|
||||
showPicker: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.loaded.pipe(delay(0)).subscribe(() => {
|
||||
this.picker = new Picker({
|
||||
onChange: this.outColor.next.bind(this.outColor),
|
||||
parent: this.clickColorPickerElement?.nativeElement,
|
||||
color: this.initialColor
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.loaded.next();
|
||||
}
|
||||
}
|
@ -1,3 +1,14 @@
|
||||
<div class="target" #targetRef>Hello!</div>
|
||||
<ngx-moveable [target]="targetRef" [draggable]="draggable" [resizable]="true" (drag)="onDrag($event)"
|
||||
(resize)="onResize($event)" #moveableRef></ngx-moveable>
|
||||
<div class="target" [style.background-color]="backgroundColor ? backgroundColor.rgbaString : 'rgba(0, 0, 0, 0.1)'" #targetRef>
|
||||
<ng-container *ngIf="plan">
|
||||
Has Plan!
|
||||
</ng-container>
|
||||
</div>
|
||||
<ngx-moveable
|
||||
[target]="targetRef"
|
||||
[draggable]="draggable"
|
||||
[resizable]="true"
|
||||
(drag)="onDrag($event)"
|
||||
(resize)="onResize($event)"
|
||||
(dragEnd)="onDragEnd($event)"
|
||||
(resizeEnd)="onResizeEnd($event)"
|
||||
#moveableRef></ngx-moveable>
|
@ -1,12 +1,55 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Config } from '../models/generated/Config';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { Subject, debounceTime } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ConfigService {
|
||||
constructor() {}
|
||||
//We're making sure this is loaded initially so let's not indicate it being nullable.
|
||||
config!: Config;
|
||||
private syncSubject: Subject<void> = new Subject<void>();
|
||||
|
||||
public get lastPlanWindowPosition() {
|
||||
return 0;
|
||||
constructor() {
|
||||
// Let's do some premature optimization! Now we can do lots of config changes quickly from FE without as many sync calls :D
|
||||
this.syncSubject.pipe(debounceTime(500)).subscribe(() => this.underlyingSync());
|
||||
}
|
||||
|
||||
init() {
|
||||
return invoke<Config>('load_config').then(cfg => {
|
||||
const _this = this;
|
||||
|
||||
|
||||
// Mostly to wrap setters so we can run the (debounced) sync function on any config changes!
|
||||
this.config = new Proxy(cfg, {
|
||||
get(target: any, property) {
|
||||
return target[property as keyof Config];
|
||||
},
|
||||
set(target: Config, property:any , value) {
|
||||
// target[property as keyof Config] = value;
|
||||
setConfig(target, property, value);
|
||||
_this.sync();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Loaded cfg:", this.config);
|
||||
console.log("Loaded x:", this.config.initialPlanWindowPosition.x);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
sync() {
|
||||
this.syncSubject.next();
|
||||
}
|
||||
|
||||
private underlyingSync() {
|
||||
console.log("saving config");
|
||||
invoke('set_config', { config: this.config });
|
||||
}
|
||||
}
|
||||
|
||||
function setConfig<T extends keyof Config>(target: Config, property: T, value: Config[T]) {
|
||||
target[property] = value;
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ShortcutService } from './shortcut.service';
|
||||
import { EventsService } from './events.service';
|
||||
import { Event } from "@tauri-apps/api/event";
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { ConfigService } from './config.service';
|
||||
|
||||
class StateEvent {
|
||||
Visible?: any;
|
||||
Interactable?: any;
|
||||
Hidden?: any;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OverlayService {
|
||||
interactable: boolean = false;
|
||||
isBinding: boolean = false;
|
||||
|
||||
|
||||
constructor(private shortcuts: ShortcutService, private events: EventsService, private configService: ConfigService) {
|
||||
console.log("cfg", configService);
|
||||
this.shortcuts.register(this.configService.config.toggleOverlay, this.onToggleOverlay);
|
||||
this.events.listen<StateEvent>("OverlayStateChange").subscribe(this.onOverlayStateChange);
|
||||
}
|
||||
|
||||
onOverlayStateChange(event: Event<StateEvent>) {
|
||||
this.interactable = event.payload.Interactable != null;
|
||||
}
|
||||
|
||||
onToggleOverlay = () => {
|
||||
if (this.interactable) {
|
||||
this.interactable = false;
|
||||
} else {
|
||||
this.interactable = true;
|
||||
}
|
||||
|
||||
invoke("set_interactable", { interactable: this.interactable }).then();
|
||||
}
|
||||
|
||||
onBindToggleOverlayFinish(keys: string[]) {
|
||||
this.isBinding = false;
|
||||
let chord = keys.reduce((acc, curr) => {
|
||||
if (acc === '') return curr;
|
||||
|
||||
return acc.concat('+').concat(curr);
|
||||
}, '');
|
||||
console.log(chord);
|
||||
|
||||
this.shortcuts.rebind(chord, this.onToggleOverlay);
|
||||
}
|
||||
}
|
Loading…
Reference in new issue