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 { BrowserModule } from "@angular/platform-browser";
|
||||||
|
|
||||||
import { AppComponent } from "./app.component";
|
import { AppComponent } from "./app.component";
|
||||||
import { FormsModule } from "@angular/forms";
|
import { FormsModule } from "@angular/forms";
|
||||||
import { RecordKeyChord } from "./directives/record-key-chord.directive";
|
import { RecordKeyChord } from "./directives/record-key-chord.directive";
|
||||||
import { PlanDisplayModule } from "./plan-display/plan-display.module";
|
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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
RecordKeyChord
|
RecordKeyChord, ColorPickerComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
PlanDisplayModule,
|
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],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
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>
|
<div class="target" [style.background-color]="backgroundColor ? backgroundColor.rgbaString : 'rgba(0, 0, 0, 0.1)'" #targetRef>
|
||||||
<ngx-moveable [target]="targetRef" [draggable]="draggable" [resizable]="true" (drag)="onDrag($event)"
|
<ng-container *ngIf="plan">
|
||||||
(resize)="onResize($event)" #moveableRef></ngx-moveable>
|
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 { Injectable } from '@angular/core';
|
||||||
|
import { Config } from '../models/generated/Config';
|
||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import { Subject, debounceTime } from 'rxjs';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ConfigService {
|
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>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
public get lastPlanWindowPosition() {
|
console.log("Loaded cfg:", this.config);
|
||||||
return 0;
|
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